The Missing Manager for Django Models with GFK Relationships

Generic relations are a powerful piece of the Django toolkit that allow you to create model-agnostic foreign key relationships with other parts of your Django project. If you aren’t familiar with how they work, have a read through the ContentType documentation as their functionality is outside the scope of this post.

The only downside is that they require a little bit of boilerplate code to get up and running and you sometimes find yourself writing the same code numerous times, particularly if you are using GFKs heavily in your application. To make things DRYer, let’s outsource some of this boilerplate code.

First let’s look at an example. A simple GFK relationship might be used to create a voting application that allows you to easily attach a positive or negative vote to a variety of different content:

class Vote(models.Model):
  content_type = models.ForeignKey(ContentType)
  object_id = models.PositiveIntegerField()
  content_object = GenericForeignKey('content_type', 'object_id')

  author = models.ForeignKey("auth.User")
  value = models.IntegerField()  # +1 or -1

  #...

class Comment(models.Model):
  author = models.ForeignKey("auth.User")
  content = models.TextField()

  #...

class Review(models.Model):
  author = models.ForeignKey("auth.User")
  content = models.TextField()

  #...

Django provides a GenericForeignKey model field that makes it easy to traverse the relationship from one side to the other (in this case from a Vote to a Comment or Review and visa versa). It also provides some useful methods on the ContentType model that allow us to switch easily between ContentType model instances and the actual class objects those instance represent.

Even with these helpers, there are three very common queries I find myself doing again and again with GFKs:

  • get all vote objects related to a particular user (i.e. “get all of John’s votes”)
  • get all vote objects related to particular model (i.e. “get all the votes in our review sections”)
  • get all vote objects for a particular instance (i.e. “get all the votes for review X”)

We should write a model manager to take care of these for us!

The first thing we can do is to move the GFK relationship itself into an abstract model that can be inherited:

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericForeignKey

class GKFModel(models.Model):
    """
    Adds the required fields for a model to implement a GFK relationship and
    also adds a simple 'create' method that can be used to create the relationship.
    """
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        abstract = True

class Vote(GFKModel):
    author = models.ForeignKey("auth.User")
    value = models.IntegerField()  # +1 or -1

    #...   

Then we can create a simple model manager that provides three common queries for objects implementing a GFK relationship:

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import SafeText
from django.db.models.loading import get_model

class GFKManager(models.Manager):
    user_field = "author"

    def for_user(self, user, queryset=None):
        if not queryset:
            queryset = self.get_queryset()
        return queryset.filter(**{self.user_field: user})

    def for_model(self, model, queryset=None):
      	"""
      	Returns a queryset that will fetch all instances that
      	are related to a paricular model (for example all Votes
        on Comment objects) The default queryset is used but can
        be replaced using the "queryset" kwarg (for example if
      	you wanted to add "prefetch_related" or "select_related"
      	"""
        if isinstance(model, (str, SafeText)):
            model = get_model(*model.rsplit('.', 1))
        ct = ContentType.objects.get_for_model(model)
        if not queryset:
            queryset = self.get_queryset()
       	return queryset.filter(content_type=ct)

    def for_object(self, obj, queryset=None):
      	"""
      	Returns a queryset that will fetch all instance that are
      	related to a particular instance (for example all
      	Votes on a single particular Comment object). Like
        above,the default queryset
      	"""
        return self.for_model(obj.__class__, queryset=queryset).filter(object_id=obj.pk)    

class Vote(models.Model):
    objects = GFKManager()
    #...
> Vote.objects.for_model(Comment)
> Vote.objects.for_object(Comment.objects.get(pk=1))

The beauty of this is that these can be reused for all models that want to implement a GFK relationship. In a larger project that might have numerous models that implement GFK relationships (commenting, liking, voting, flagging content etc.) this can save us a lot of repetitive code and make our views elsewhere much cleaner.