Avoiding query code duplication in Django with custom model managers

For a second, please imagine we are building the next big social network. We decide that our “revolutionary” new app shall allow its users to create profiles. Besides a mandatory user name and avatar, we consider a profile complete only if the user also supplies an email or physical address (or both). In other words, a profile is not complete as long as we can’t contact the user in some way (via their email or physical address).

Based on the above requirements, our lead developer comes up with the following models to represent our use case:

from django.db import models
 
 
class User(models.Model):
    name = models.CharField(max_length=50)
    avatar = models.ImageField()
    email_address = models.EmailField(blank=True, null=True)
    address = models.ForeignKey('Address', blank=True, null=True)
 
 
class Address(models.Model):
    street = models.CharField(max_length=50)
    city = models.CharField(max_length=50)
    country = models.CharField(max_length=50)

Next, suppose that for some reason we would like to distinguish between users with complete profiles from those with incomplete ones. Our developer comes up with the following query to make things happen:

from django.db.models import Q
 
User.objects.filter(Q(email_address__isnull=False) | Q(address__isnull=False))

It’s not hard to imagine that this query might be relevant in several situations. For example, we might need it for displaying a list of complete user profiles but we might also need it for filtering the users that can be contacted. Of course we could just go ahead and copy the query to multiple places, but in the spirit of DRY it makes a lot of sense not to do that.

Luckily, Django offers a built-in alternative to copying query code. Thanks to the concept of custom model managers, we can define a query once and use it over and over again in different places.

class CustomUserManager(models.Manager):
    def with_complete_profiles(self):
        return self.get_queryset().filter(Q(email_address__isnull=False) | Q(address__isnull=False))
 
 
class User(models.Model):
    name = models.CharField(max_length=50)
    avatar = models.ImageField()
    email_address = models.EmailField(blank=True, null=True)
    address = models.ForeignKey('Address', blank=True, null=True)
 
    objects = CustomUserManager()

In the previous code example, we are effectively overriding Django’s default manager for the User model by redefining the objects attribute. From now on, we can readably and cleanly retrieve all users with complete profiles by calling the with_complete_profiles() method on the manager:

User.objects.with_complete_profiles()

Neat!