Fork me on GitHub

What problems are solved with this?

It is usually not a good idea to have two or more columns in the database where the value of one column is derived from other column(s). In some cases, it might not be possible to do this in a different way, or the derived column is used by another tool. In that case, we can work use the .pre_save(…) method which runs just before the model record(s) are saved, and therefore thus guarantee that the column is in sync with the columns it depends on. It can however not work cross-table.

What does this pattern look like?

In this particular case, We had to make a field that for another field named date, populated a field named month with the month represented as an integer with the year and month.

We can do this by creating a subclass of a PositiveIntegerField that then overrides the .pre_save(…) method as follows:

from django.db import models


class AutoMonthField(models.PositiveIntegerField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('editable', False)
        kwargs.setdefault('null', True)
        kwargs.setdefault('default', None)

    def pre_save(self, model_instance, add):
        date = model_instance.date
        value = None
        if date is not None:
            value = date.year * 100 + date.month
        setattr(model_instance, self.attname, value)
        return value

We can then inject this field into model with a field .date, like:

from django.db import models


class MyModel(models.Model):
    date = models.DateField()
    month = AutoMonthField()

The month field is here non-editable, since it thus derives the value from the .date field, and will, each time we save a MyModel object, look at the .date field, and adapt accordingly.

Extra tips

We can encapsulate the above logic in mixin that looks like this:

class AutoFieldMixin:
    def __init__(self, *args, function=None, **kwargs):
        kwargs.setdefault('editable', False)
        self.function = function
        super().__init__(*args, **kwargs)

    def determine_value(self, model_instance, add):
        return self.function(model_instance, add)

    def pre_save(self, model_instance, add):
        value = self.determine_value(model_instance, add)
        setattr(model_instance, self.attname, value)
        return self.get_prep_value(value)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs.pop('function', None)
        return name, path, args, kwargs

and for example work with:

from django.db import models


class AutoMonthField(AutoFieldMixin, models.PositiveIntegerField):
    def determine_value(self, model_instance, add):
        date = model_instance.date
        if date is not None:
            return date.year * 100 + date.month

This will normally work for .bulk_create(…), and .bulk_update(…) if you specify the month field as field to update. But this will not update the month field, if you use .update(date=my_date)