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 Contact
Contact 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.