Toast Driven

← Back to November 2, 2008

Abstract Model Metadata

Another recurring and important topic for me is metadata. Where possible, I like to add extra data about how the data came to be and what has happened to it. There are a lot of ways to tackle this (such as creating audit trails, versioning data, etc.) but something I always try to provide is a create date and last modified date. In conjunction with tying the change to a user, this can go a long way in providing useful information back to the user.

Not so long ago, I would manually add these kinds of fields to my objects. However, in the weeks before Django 1.0, model inheritance was one of the improvements that landed with QuerySet-refactor. It turns out that this is a very good way to add our metadata.

We'll start with the same initial code from yesterday's entry.

from django.db import model
from django.contrib.auth.models import User

class Contact(models.Model):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=255)
    slug = models.SlugField()
    email = models.EmailField()

Instead of manually adding the created/updated fields to this model (and having to do it again for every model we create after this), let's build something we can reuse. We'll create a StandardMetadata class that we can inherit from in our models. The new code would look something like this:

import datetime
from django.db import model
from django.contrib.auth.models import User

class StandardMetadata(models.Model):
    created = models.DateTimeField(default=datetime.datetime.now)
    updated = models.DateTimeField(default=datetime.datetime.now)
    
    class Meta:
        abstract = True

class Contact(StandardMetadata):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=255)
    slug = models.SlugField()
    email = models.EmailField()

Our changes are relatively straight-forward. We're now importing datetime, we've built a new StandardMetadata model that contains the fields we want and we've changed what class ContactContact inherits from, we've made no other changes to the model. However, that model newly has created/updated fields.

The trick here lies in StandardMetadata's inner Meta class. The abstract = True declaration tells Django not to create a table for this model but to add those fields to any table that inherits from this class. Without this, Django would create a table for StandardMetadata and perform a OneToOneField join on any inheriting objects.

This is an improvement, but let's take this a step further and make sure that updated gets automatically handled when any subclassing model gets saved. The code would now look like:

import datetime
from django.db import model
from django.contrib.auth.models import User

class StandardMetadata(models.Model):
    created = models.DateTimeField(default=datetime.datetime.now)
    updated = models.DateTimeField(default=datetime.datetime.now)
    
    class Meta:
        abstract = True
    
    def save(self, *args, **kwargs):
        self.updated = datetime.datetime.now()
        super(StandardMetadata, self).save(*args, **kwargs)

class Contact(StandardMetadata):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=255)
    slug = models.SlugField()
    email = models.EmailField()

We've overridden Model's built-in save() method to automatically update the updated field. Now any subclass can save and also have its updated set to the current date & time.

Finally, we can take this even one step further and use this concept to improve on yesterday's "safer delete" functionality. We'll push the handling of "active" into our StandardMetadata class, allowing any subclass to also inherit this.

import datetime
from django.db import model
from django.contrib.auth.models import User

class StandardMetadata(models.Model):
    created = models.DateTimeField(default=datetime.datetime.now)
    updated = models.DateTimeField(default=datetime.datetime.now)
    is_active = models.BooleanField(default=True)
    
    class Meta:
        abstract = True
    
    def save(self, *args, **kwargs):
        self.updated = datetime.datetime.now()
        super(StandardMetadata, self).save(*args, **kwargs)
    
    def delete(self, *args, **kwargs):
        self.is_active = False
        self.save()

class ActiveManager(models.Manager):
    def get_query_set(self):
        return super(ContactActiveManager, self).get_query_set().filter(is_active=True)

class Contact(StandardMetadata):
    user = models.ForeignKey(User)
    name = models.CharField(max_length=255)
    slug = models.SlugField()
    email = models.EmailField()
    
    objects = models.Manager()
    active = ActiveManager()

We've put the is_active field into the StandardMetadata class and pulled the delete() method in as well. We've also renamed the ContactActiveManager class to ActiveManager, as we can now reuse this same manager on any class that inherits from StandardMetadata.

This is a lot more code for a simple example, but once you start adding in more objects, the amount of typing (and worse, copy/pasting) you'll save makes it very worthwhile.

Toast Driven