Django tips: using properties on models and managers

Published August 18, 2006. Filed under: Django, Programming.

While working on a little side project this week, I ran into a couple of very common use cases that often result in a lot of extra typing:

  1. Defining a BooleanField, or an IntegerField or CharField with choices which will, logically, break up instances of the model into certain groups which need to be accessed often.
  2. Repeatedly wanting to calculate a value based on the values of several fields of a model.

Let’s look at how to handle these common cases, while reducing the extra typing and making them behave in an extremely intuitive fashion.

The set-up

Consider as an example this model, which might be found in a project-management or customer-relations application:

ORG_TYPE_CHOICES = ((1, 'Internal'), (2, 'External'))

class Organization(models.Model):
    name = models.CharField(maxlength=200)
    slug = models.SlugField(prepopulate_from=('name',))
    contact_person = models.ForeignKey(User)
    mailing_address = models.TextField()
    add_date = models.DateTimeField(default=models.LazyDate())
    org_type = models.IntegerField(choices=ORG_TYPE_CHOICES)
    is_active = models.BooleanField(default=True)
    last_paid = models.DateField(blank=True, null=True)

Now, it’s going to be extremely common to want to fetch all the organizations of one type — “internal” or “external” — and do something with that list. That means typing things like this over and over again:

external_orgs = Organization.objects.filter(org_type__exact=2)

That’s a lot of typing. Worse, it’s a lot of repetitive typing. Fortunately, Django provides an easy way to save ourselves that typing: we can define a custom manager which will do the filtering for us:

class OrganizationManager(models.Manager):
    def internal(self):
        return self.get_query_set().filter(org_type__exact=1)
    
    def external(self):
        return self.get_query_set().filter(org_type__exact=2)

Then, since we didn’t override anything, it’s safe to make this the default manager for the Organization model, by adding this line in the model definition:

objects = OrganizationManager()

Now instead of doing lots of repetitive typing, we can just do, say,

Organization.objects.internal().filter(is_active__exact=True)

And we’ll get all the “internal” organizations that are currently active.

Now, suppose that for “external” organizations we want to charge a monthly subscription fee, and disable their login if they haven’t paid this month. We could define a method on the Organization model which checks that the organization is external, is active and has paid within the last thirty days:

def is_paid_up(self):
    return self.org_type == 1 and self.is_active and self.last_paid > \
           (datetime.date.today() - datetime.timedelta(30))

Then whenever we need to check this, we can grab the Organization object into a variable, say we’ll call it o, and do

if o.is_paid_up():
    # code here to let them into the application
else:
    # deny access

Let’s make that even better

At this point we’re rolling along pretty well and have saved ourselves quite a bit of typing: we no longer need to explicitly call filter every time we need to separate out “internal” or “external” organizations, and we don’t need to do lots of cumbersome attribute checks to find out if an organization should be allowed access to the application (since we can just check the return value of is_paid_up(). Cleaning up the resulting application code that much would make a pretty decent “Django tip” in itself (especially the custom manager bit — managers are quite possibly my favorite new feature from magic-removal/0.95), but saving ourselves some typing isn’t everything; we also want the code to make intuitive sense, and we can go one step better on that.

You’ll notice that internal and external are instance methods of the OrganizationManager, and that is_paid_up is an instance method of the Organization model; that means we have to remember to type parentheses every time we use them (except in a template), which is a minor annoyance because, really, these don’t feel like they should be methods that you have to call — it’d feel cleaner and more intuitive to just do

if o.is_paid_up:
    # let them in
else:
    # lock them out

So let’s fix that, by making all these methods we’ve defined into properties.

In Python, properties provide an easy way of wrapping up something that isn’t a “simple” attribute (i.e., one that doesn’t just hold a value) and making it look and behave like it is. Creating a property is extremly simple: depending on the exact type of property you want, you just define a couple of methods and pass them as arguments to property. So, for example, we could set up is_paid_up as a property by doing this:

def _get_is_paid_up(self):
    return self.org_type == 1 and self.is_active and self.last_paid > \
           (datetime.date.today() - datetime.timedelta(30))

is_paid_up = property(_get_is_paid_up)

In this case we only want to get the value of is_paid_up, not set or delete it, so we only need to define one method to pass into property, but for other situations you might want to define extra methods to handle those (the Django model documentation includes an example of a property which has both “get” and “set” methods. Also, note that the method used to create the property has an underscore in front of its name — so it’s _get_is_paid_up — which is a convention in Python for marking something as “private” to the class (Python doesn’t really enforce private attributes or methods, but using the underscore is a way of telling those who read your code that something is meant only for internal use).

Now we can just do if o.is_paid_up everywhere in our code, and it will work — we don’t have to remember everywhere that we’re actually getting the return value of a method or type the parentheses to call it, because making it into a property makes it behave like any attribute that holds a simple value.

And we can rewrite our custom manager along the same lines:

class OrganizationManager(models.Manager):
    def _get_internal(self):
        return self.get_query_set().filter(org_type__exact=1)
    
    def _get_external(self):
        return self.get_query_set().filter(org_type__exact=2)
    
    internal = property(_get_internal)
    external = property(_get_external)

Now we can do Organization.objects.internal.filter(is_active__exact=True), or any other type of operation that a QuerySet can do (just don’t try do to Organization.objects.internal.all()Organization.objects.internal already is the set of all “internal” organizations, and trying to do all() on it will raise an AttributeError).

This doesn’t save us tons of typing like the initial setup of the custom manager and the is_paid_up method did, but it does make our code cleaner and makes internal, external and is_paid_up behave more intuitively, and that’s a big, big win.