Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Django 3 Web Development Cookbook

You're reading from   Django 3 Web Development Cookbook Actionable solutions to common problems in Python web development

Arrow left icon
Product type Paperback
Published in Mar 2020
Publisher Packt
ISBN-13 9781838987428
Length 608 pages
Edition 4th Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Jake Kronika Jake Kronika
Author Profile Icon Jake Kronika
Jake Kronika
Aidas Bendoraitis Aidas Bendoraitis
Author Profile Icon Aidas Bendoraitis
Aidas Bendoraitis
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Getting Started with Django 3.0 2. Models and Database Structure FREE CHAPTER 3. Forms and Views 4. Templates and JavaScript 5. Custom Template Filters and Tags 6. Model Administration 7. Security and Performance 8. Hierarchical Structures 9. Importing and Exporting Data 10. Bells and Whistles 11. Testing 12. Deployment 13. Maintenance 14. Other Books You May Enjoy

Creating a model mixin to handle generic relations

Aside from normal database relationships, such as a foreign-key relationship or a many-to-many relationship, Django has a mechanism to relate a model to an instance of any other model. This concept is called generic relations. For each generic relation, we save the content type of the related model as well as the ID of the instance of that model.

In this recipe, we will look at how to abstract the creation of generic relations in the model mixins.

Getting ready

For this recipe to work, you will need to have the contenttypes app installed. It should be in the INSTALLED_APPS list in the settings, by default, as shown in the following code:

# myproject/settings/_base.py

INSTALLED_APPS = [
# contributed
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third-party
# ...
# local
"myproject.apps.core",
"myproject.apps.categories",
"myproject.apps.ideas",
]

Again, make sure that you have already created the myproject.apps.core app for your model mixins.

How to do it...

To create and use a mixin for generic relations follow these steps:

  1. Open the models.py file in the core package in a text editor, and insert the following content there:
# myproject/apps/core/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import FieldError


def object_relation_base_factory(
prefix=None,
prefix_verbose=None,
add_related_name=False,
limit_content_type_choices_to=None,
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 ObjectRelationBase in your
model.

The model fields are created using this naming scheme:
<<prefix>>_content_type
<<prefix>>_object_id
<<prefix>>_content_object
"""
p = ""
if prefix:
p = f"{prefix}_"

prefix_verbose = prefix_verbose or _("Related object")
limit_content_type_choices_to = limit_content_type_choices_to
or {}

content_type_field = f"{p}content_type"
object_id_field = f"{p}object_id"
content_object_field = f"{p}content_object"

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 = _(f"{prefix_verbose}'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,
on_delete=models.CASCADE)

fk_verbose_name = prefix_verbose

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 migrations

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
  1. The following code snippet is an example of how to use two generic relationships in your app (put this code in ideas/models.py):
# myproject/apps/ideas/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _

from myproject.apps.core.models import (
object_relation_base_factory as generic_relation,
)


FavoriteObjectBase = generic_relation(
is_required=True,
)


OwnerBase = generic_relation(
prefix="owner",
prefix_verbose=_("Owner"),
is_required=True,
add_related_name=True,
limit_content_type_choices_to={
"model__in": (
"user",
"group",
)
}
)


class Like(FavoriteObjectBase, OwnerBase):
class Meta:
verbose_name = _("Like")
verbose_name_plural = _("Likes")

def __str__(self):
return _("{owner} likes {object}").format(
owner=self.owner_content_object,
object=self.content_object
)

How it works...

As you can see, this snippet is more complex than the previous ones.

The object_relation_base_factory function, which we have aliased to generic_relation, for short, in our import, 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 fields and the content_object generic foreign key that points to the related instance.

Why can't we just define a simple model mixin with these three attributes? A dynamically generated abstract class allows us to have prefixes for each field 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 fields for the favorite object, and owner_content_type, owner_object_id, and owner_content_object for the one (user or group) that liked the object.

The object_relation_base_factory function, which we have aliased
to generic_relation for short, adds the 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 to only the content types of the User and Group models.

See also

  • The Creating a model mixin with URL-related methods recipe
  • The Creating a model mixin to handle creation and modification dates recipe
  • The Creating a model mixin to take care of meta tags recipe
  • The Implementing the Like widget recipe in Chapter 4, Templates and JavaScript
You have been reading a chapter from
Django 3 Web Development Cookbook - Fourth Edition
Published in: Mar 2020
Publisher: Packt
ISBN-13: 9781838987428
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime