October 13, 2008

Django Tip: Field.pre_save()

Here's a little thing that can help you to improve the flexibility of your models.

Rather than trying to extend or override Model.save(), if the special action you require only affects one field then define a subclass of the required Field type and define a pre_save() method on the subclass. This will override the default method, which simply returns the attribute value.

I spotted this by following up a discussion of the planned removal of the DateField's auto_now and auto_now_add attributes, which you can assert to set a modification or creation date respectively on rows. Some developers suggested putting the functionality in a decorator that could be used on the model's save() method, but Jacob Kaplan-Moss preferred a trivial subclass:

class AutoDateTimeField(models.DateTimeField):
def pre_save(self, model_instance, add):
return datetime.datetime.now()

Unfortunately this is just a little bit too trivial, since pre_save() is required to set the attribute value - every time for auto_now and on first save for auto_now_add. Fortunately the extra code required is pretty small. pre_save() receives three arguments: the first is the field instance, the second is the model instance and the third is a flag which is True only when the save() call is adding a new row to the associated table.

class AutoNowDateTimeField(models.DateTimeField):
def pre_save(self, instance, add):
val = datetime.datetime.now()
setattr(instance, self.attname, val)
return val

class AutoNowAddDateTimeField(AutoNowDateTimeField):
def pre_save(self, instance, add):
if not add:
return getattr(instance, self.attname)
return AutoNowDateTimeField.pre_save(self, instance, add)

Technically it would be cleaner to use super() to access the superclass methods from AutoNowAddDateTimeField, but as a proof of concept this works fine.

As it happens the auto_now and auto_now_add attributes of DateTimeField were not removed before the release of Django 1.0, so we are likely to be stuck with them until 2.0. But the same technique is useful for any field that needs to receive an automatic value when a model instance is saved.


drawk said...

Excellent, I was looking for a good use of this. I am using UUIDs for keys and was having an issue using it in Django with default. And the django-extensions UUIDField was having an issue with auto. So I got uuidgen and now call uuidgen.generate() in the presave of an object to provide this. Otherwise when it is used as default it pulls the same value when editing classes in the django admin. btw, using UUIDs due to the size of the data and replication.

ramiro said...

A similar technique is used in django-command-extensions's CreationDateTimeField and ModificationDateTimeField do. It also defines a handy TimeStampedModel ABC model that already contains fields of both types.

Steve said...

I just linked to this example as a positive contribution to encourage the Ypit guys, who after two years must have a wealth of stuff to share. Go, Ypit!.

Ah, the smell of the startup, the roar of the crowd.