Skip to content

Instantly share code, notes, and snippets.

@LowerDeez
Last active June 16, 2017 19:31
Show Gist options
  • Save LowerDeez/359faaf4f92da1af73aee03efef0ca8f to your computer and use it in GitHub Desktop.
Save LowerDeez/359faaf4f92da1af73aee03efef0ca8f to your computer and use it in GitHub Desktop.
Creating a model mixin to handle generic relations
How it works…
As you can see, this snippet is more complex than the previous ones. The object_
relation_mixin_factory object is not a mixin itself; it is a function that generates a
model mixin, that is, an abstract model class to extend from. The dynamically created mixin
adds the content_type and object_id filds and the content_object generic foreign
key that points to the related instance.
Why couldn't we just defie a simple model mixin with these three attributes? A dynamically
generated abstract class allows us to have prefies for each fild name; therefore, we can
have more than one generic relation in the same model. For example, the Like model, which
was shown previously, will have the content_type, object_id, and content_object
filds for the favorite object and owner_content_type, owner_object_id, and owner_
content_object for the one (user or institution) who liked the object.
The object_relation_mixin_factory() function adds a possibility to limit the content
type choices by the limit_content_type_choices_to parameter. The preceding
example limits the choices for owner_content_type only to the content types of the User
and Institution models. Also, there is the limit_object_choices_to parameter that
can be used by custom form validation to limit the generic relations only to specifi objects, for
example, the objects with published status.
# The following is an example of how to use two generic relationships in your app (put
# this code in demo_app/models.py), as shown in the following:
# demo_app/models.py
# -*- coding: UTF-8 -*-
from __future__ import nicode_literals
from django.db import models
from utils.models import object_relation_mixin_factory
from django.utils.encoding import python_2_unicode_compatible
FavoriteObjectMixin = object_relation_mixin_factory( is_required=True,)
OwnerMixin = object_relation_mixin_factory(
prefix="owner",
prefix_verbose=_("Owner"),
add_related_name=True,
limit_content_type_choices_to={'model__in': ('user', 'institution')},
is_required=True,
)
@python_2_unicode_compatible
class Like(FavoriteObjectMixin, OwnerMixin):
class Meta:
verbose_name = _("Like")
verbose_name_plural = _("Likes")
def __str__(self):
return _("%(owner)s likes %(obj)s") % {"owner": self.owner_content_object, "obj": self.content_object, }
# utils/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import escape
from django.utils.safestring import mark_safe
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldError
def object_relation_mixin_factory(
prefix=None,
prefix_verbose=None,
add_related_name=False,
limit_content_type_choices_to={},
limit_object_choices_to={},
is_required=False
):
"""
returns a mixin class for generic foreign keys using "Content type - object Id" with dynamic field names.
This function is just a class generator
Parameters:
prefix : a prefix, which is added in front of the fields
prefix_verbose : a verbose name of the prefix, used to generate a title for the field column of the
content object in the Admin.
add_related_name : a boolean value indicating, that a related name for the generated content type
foreign key should be added.
This value should be true, if you use more than one ObjectRelationMixin in your model.
The model fields are created like this:
<<prefix>>_content_type : Field name for the "content type"
<<prefix>>_object_id : Field name for the "object Id"
<<prefix>>_content_object : Field name for the "content object"
"""
p = ''
if prefix:
p = '%s_' % prefix
content_type_field = '%scontent_type' % p
object_id_field = '%sobject_id' % p
content_object_field = "%scontent_object" % p
class TheClass(models.Model):
class Meta:
abstract = True
if add_related_name:
if not prefix:
raise FieldError("if add_related_name is set to True, a prefix must be given")
related_name = prefix
else:
related_name = None
optional = not is_required
ct_verbose_name = (
_("%s's type (model)") % prefix_verbose
if prefix_verbose
else _("Related object's type (model)")
)
content_type = models.ForeignKey(
ContentType,
verbose_name=ct_verbose_name,
related_name=related_name,
blank=optional,
null=optional,
help_text=_("Please select the type (model) for the relation, you want to build."),
limit_choices_to=limit_content_type_choices_to,
)
fk_verbose_name = (prefix_verbose or _("Related object"))
object_id = models.CharField(
fk_verbose_name,
blank=optional,
null=False,
help_text=_("Please enter the ID of the related object."),
max_length=255,
default="", # for south migrations
)
object_id.limit_choices_to = limit_object_choices_to
# can be retrieved by
# MyModel._meta.get_field("object_id").limit_choices_to
content_object = GenericForeignKey(
ct_field=content_type_field,
fk_field=object_id_field,
)
TheClass.add_to_class(content_type_field, content_type)
TheClass.add_to_class(object_id_field, object_id)
TheClass.add_to_class(content_object_field, content_object)
return TheClass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment