This website is made possible by displaying online advertisements to our visitors.
Please consider supporting us by disabling your ad blocker. Thank you for your support.
This website is made possible by displaying online advertisements to our visitors.
Please consider supporting us by disabling your ad blocker.

How to Use Django's Generic Foreign Key

Aug. 6 2021 Yacine Rouizi
Model Django
How to Use Django's Generic Foreign Key

Introduction

Django provides the contenttypes framework that allows us to create generic relationships between models. This can be useful when you want to implement, for example,  a comment system where you can comment on posts, user profiles, images, and even comments themselves.

In this tutorial, we will use the ContentType model, GenericForeignKey field, and GenericRelation to implement a comment system where users can leave comments on posts and user profiles.

Download the Code

If you want to play with the code while following this tutorial, just run the commands below:

git clone git@github.com:Rouizi/django-generic-relations.git
cd django-generic-relations/

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python manage.py runserver

To access the admin, use this superuser:

  • username: rouizi
  • password: 1234

The ContentType Model and Generic Relations

Let's start with the Comment model:

# core/models.py
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType


class Comment(models.Model):
    author = models.CharField(max_length=50)
    content = models.TextField()

    # the required fields to enable a generic relation
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey()

    def __str__(self):
        return self.author

So here we added a ForeignKey to the ContentType model. We also added a PositiveIntegerField which will be the field that will store the primary key values of the models to which this model will be linked.

And the interesting part here is the GenericForeignKey. This special field allows this model to have relationship with any model (the model can have generic relation with other models).

And now let's create the Post and Profile models:

# core/models.py
from django.contrib.contenttypes.fields import GenericRelation

# ...

class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=100, unique=True)
    body = models.TextField()
    comments = GenericRelation(Comment)
    
    def __str__(self):
        return self.title
    
    
class Profile(models.Model):
    about = models.TextField()
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    comments = GenericRelation(Comment)

    def __str__(self):
        return f'profile of {self.user.username}'

By just adding a GenericRelation field pointing to the Comment model, we can now add comments for these two models.

Let's open the shell and play with these models:

$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> from core.models import Comment, Profile, Post

>>> post = Post.objects.get(title='Test post')
# add a comment for the post
>>> post.comments.create(author='sofiane', content='Nice article')

# we can also add a comment this way
>>> c1 = Comment.objects.create(content_object=post, author='sofiane', content='cool')

# get all the comments for this post
>>> post.comments.all()
<QuerySet [<Comment: Nice article>, <Comment: cool>]>

>>> c1.content_object # this will return the object this comment is related to
<Post: Test post>
>>> c1.content_type
<ContentType: core | post>

>>> user = User.objects.get(username='rouizi')
# get the user's profile
>>> profile = Profile.objects.get(user=user)
# add a comment on the user's profile rouizi
>>> c2 = profile.comments.create(author='sofiane', content='you have a great profile')
>>> c2.content_object # this will return the object this comment is related to
<Profile: profile of rouizi>
>>> c2.content_type
<ContentType: core | profile>
>>> c2.object_id # the id of the object this comment is related to
1

If you have cloned the project, you can navigate to 127.0.0.1://post/test-post to see the comments on the post 'Test post':

Comment on post

you can also visit 127.0.0.1://profile/rouizi to see the comments on the profile of the user 'rouizi':

Comment on profile

If you want to make your life easier, you can add the related_query_parameter parameter to the GenericRelation class.

So, for example, if we add it to the Post model:

class Post(models.Model):
    # ...
    comments = GenericRelation(Comment, related_query_name='post')

This will create a relation from the related object (comment) back to this one (post) and now you can filter from the related object (comment) :

# Get all comments for posts that were posted by the user 'rouizi'
>>> Comment.objects.filter(post__author=user)
<QuerySet [<Comment: Nice article>, <Comment: cool>]>

Note that there exists a BaseGenericInlineFormSet and a GenericInlineModelAdmin to use generic relations in forms and the admin. Please refer to the documentation for more information.

Grab the code from my GitHub repo.

Leave a comment

(Your email address will not be published)