Django autogenerated models and relationships doesn't show up in model meta
I have the below code to create a Vote model for any given other model that I want to attach this relation to.
from functools import lru_cache
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.common.models.base import BaseModel
from apps.common.utils import camel_to_snake
@lru_cache(maxsize=None)
def create_vote_class(klass):
class Vote(BaseModel):
class VoteType(models.IntegerChoices):
UPVOTE = 1, _("Upvote")
DOWNVOTE = -1, _("Downvote")
user = models.ForeignKey(
get_user_model(),
on_delete=models.CASCADE,
related_name=f"{camel_to_snake(klass.__name__)}_votes",
verbose_name=_("User"),
help_text=_("The voter."),
)
post = models.ForeignKey(
klass,
on_delete=models.CASCADE,
related_name=f"{camel_to_snake(klass.__name__)}_votes",
verbose_name=_("Post"),
help_text=_("The post this vote is for."),
)
body = models.IntegerField(
choices=VoteType.choices,
verbose_name=_("Body"),
help_text=_("The actual vote. -1 or +1"),
)
def type(self):
return self.get_body_display()
class Meta:
abstract = True
app_label = klass._meta.app_label # NOQA
klass_name = f"{klass.__name__}Vote"
bases = (Vote,)
class Meta:
app_label = klass._meta.app_label # NOQA
verbose_name = _(klass_name)
verbose_name_plural = _(f"{klass_name}s")
klass_dict = {"__module__": klass.__module__, "Meta": Meta}
vote_class = (type(klass_name, bases, klass_dict) # NOQA
return vote_class
class VoteMaker:
is_relation = True
many_to_many = True
def __init__(self, **kwargs):
self.related_name = kwargs.get("related_name")
self.vote_class_attribute_name = kwargs.get("vote_class_attribute_name")
def contribute_to_class(self, cls, name):
setattr(cls, self.vote_class_attribute_name, create_vote_class(cls))
setattr(cls, name, self.get_votes_field(cls))
def get_votes_field(self, cls):
field = models.ManyToManyField(
get_user_model(),
through=create_vote_class(cls),
related_name=self.related_name or f"voted_{camel_to_snake(cls.__name__)}s",
verbose_name=_("Votes"),
help_text=_("Votes for this post."),
)
return field
def votes(related_name=None, vote_class_attribute_name="vote_class"):
vote_maker = VoteMaker(
related_name=related_name,
vote_class_attribute_name=vote_class_attribute_name,
)
return vote_maker
I'm using it like this:
class Post(BaseModel):
votes = votes()
The Vote model is being generated, the related names and attribute names on the models are correct, I can query the Vote model, everything good there.
But now when I try to create an admin page for this Post model:
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
fields = ["votes"]
I'm getting an error:
Unknown field(s) (votes) specified for Post. Check fields/fieldsets/exclude attributes of class PostAdmin.
I've tried to dig deep into the source code but so far I only got to the
model._meta._get_fields(reverse=False)
being called, which should have my m2m field, votes, but doesn't.
Anyone can point me to the right direction on how to solve this issue?
Edit 1:
Extra:
In [1]: from apps.core.models import *
In [2]: Post.votes
Out[2]: <django.db.models.fields.related.ManyToManyField>
In [3]:
As you can see, Post.votes is indeed a ManyToMany field
Edit 2:
Moving forward I discovered that it shouldn't look like in Edit 1 above.
For example, in user model:
ipdb> User.groups
<django.db.models.fields.related_descriptors.ManyToManyDescriptor object at 0x1334e5a90>
So I have changed my original code slightly and now it looks like this:
def contribute_to_class(self, cls, name):
setattr(cls, self.vote_class_attribute_name, create_vote_class(cls))
self.get_votes_field(cls).contribute_to_class(cls, name)
which helped with some of the problem.
ipdb> Post._meta.many_to_many
(<django.db.models.fields.related.ManyToManyField: comments>, <django.db.models.fields.related.ManyToManyField: votes>)
Now I can see the many to many fields I have, but now I am still having some issues in other parts of django, still related to the admin.
The full traceback:
Internal Server Error: /core/post/add/
Traceback (most recent call last):
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/forms/forms.py", line 183, in __getitem__
field = self.fields[name]
KeyError: 'votes'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 220, in _get_response
response = response.render()
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/response.py", line 114, in render
self.content = self.rendered_content
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/response.py", line 92, in rendered_content
return template.render(context, self._request)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/backends/django.py", line 62, in render
return self.template.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 175, in render
return self._render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
return self.nodelist.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/loader_tags.py", line 157, in render
return compiled_parent._render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
return self.nodelist.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/loader_tags.py", line 157, in render
return compiled_parent._render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
return self.nodelist.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/loader_tags.py", line 63, in render
result = block.nodelist.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/loader_tags.py", line 63, in render
result = block.nodelist.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/defaulttags.py", line 238, in render
nodelist.append(node.render_annotated(context))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/loader_tags.py", line 208, in render
return template.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 177, in render
return self._render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 167, in _render
return self.nodelist.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 1005, in <listcomp>
return SafeString("".join([node.render_annotated(context) for node in self]))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/defaulttags.py", line 238, in render
nodelist.append(node.render_annotated(context))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/base.py", line 966, in render_annotated
return self.render(context)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/template/defaulttags.py", line 193, in render
values = list(values)
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/contrib/admin/helpers.py", line 156, in __iter__
yield AdminField(self.form, field, is_first=(i == 0))
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/contrib/admin/helpers.py", line 170, in __init__
self.field = form[field] # A django.forms.BoundField instance
File "/Users/isikkaplan/Desktop/code/personal/wsd/wsd/venv/lib/python3.8/site-packages/django/forms/forms.py", line 185, in __getitem__
raise KeyError(
KeyError: "Key 'votes' not found in 'PostForm'. Choices are: image, title, user."