Adding Custom Django Model Validation


Question

I have a Django model with a start and end date range. I want to enforce validation so that no two records have overlapping date ranges. What's the simplest way to implement this so that I don't have to repeat myself writing this logic?

e.g. I don't want to re-implement this logic in a Form and a ModelForm and an admin form and the model's overridden save().

As far as I know, Django doesn't make it easy to globally enforce these types of criteria.

Googling hasn't been very helpful, since "model validation" typically refers to validating specific model fields, and not the entire model contents, or relations between fields.

1
38
3/26/2013 10:30:47 PM

Accepted Answer

The basic pattern I've found useful is to put all my custom validation in clean() and then simply call full_clean() (which calls clean() and a few other methods) from inside save(), e.g.:

class BaseModel(models.Model):

    def clean(self, *args, **kwargs):
        # add custom validation here
        super(BaseModel, self).clean(*args, **kwargs)

    def save(self, *args, **kwargs):
        self.full_clean()
        super(BaseModel, self).save(*args, **kwargs)

This isn't done by default, as explained here, because it interferes with certain features, but those aren't a problem for my application.

46
5/23/2017 11:54:25 AM

I would override the validate_unique method on the model. To make sure you ignore the current object when validating, you can use the following:

from django.db.models import Model, DateTimeField
from django.core.validators import NON_FIELD_ERRORS, ValidationError

class MyModel(Model):
    start_date = DateTimeField()
    end_date = DateTimeField()

    def validate_unique(self, *args, **kwargs):
        super(MyModel, self).validate_unique(*args, **kwargs)

        qs = self.__class__._default_manager.filter(
            start_date__lt=self.end_date,
            end_date__gt=self.start_date
        )

        if not self._state.adding and self.pk is not None:
            qs = qs.exclude(pk=self.pk)

        if qs.exists():
            raise ValidationError({
                NON_FIELD_ERRORS: ['overlapping date range',],
            })

ModelForm will automatically call this for you through a full_clean(), which you can use manually too.

PPR has a nice discussion of a simple, correct range overlap condition.


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon