Last active
June 16, 2017 19:31
-
-
Save LowerDeez/359faaf4f92da1af73aee03efef0ca8f to your computer and use it in GitHub Desktop.
Creating a model mixin to handle generic relations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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, } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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