Defying Classification

by Malcolm Tredinnick

Wed 15 Oct 2008

Django Tip: Poor Man's Model Validation

Posted at 14:52 +1100 (last edited: 15 Oct 2008, 17:53)

In an effort to show I'm still blogging regularly (as opposed to frequently), a really short Django tip that can act as a light substitute for a number of cases where model-aware validation is really required. Basically, using ModelForms in a non-form environment. This isn't something I discovered — it was pointed out to me a couple of months ago by Jacob Kaplan-Moss and no doubt independently discovered by any number of people sinc ethen. It could do with being a bit more well-known, however.

Model-aware validation has been one of those things that has been on the "we should have that" list in Django since practically forever. It's kind of annoying that it wasn't finished for 1.0, but since the main reason it wasn't finished is we were occupied working on other features and fixes, it's also understandable when viewed in perspective. Every time you think "I wish model validation was present", turn it around to be "I'm glad XYZ is in Django" (for favorite feature XYZ), since that might have been the thing we traded away in another universe. That doesn't stop me feeling annoyed it isn't there, but it stops me blaming anybody else for the omission.

Anyway, onto the tip...

Our Toy Scenario

Imagine we have a model that tracks limits on something. A minimum and maximum value for a named object. Something like this:

from django.db import models

class Limit(models.Model):
    name = models.CharField(max_length=50)
    maximum = models.IntegerField()
    minimum = models.IntegerField()

Adding A Constraint

We want to ensure that the maximum is always greater than the minimum, no matter how we enter data into the model (that is, even if it's not coming from an HTML form submission). Django's ModelForms can be used here. There's nothing to say that the data for a form class has to come from an HTML form. It's just an object that accepts a dictionary of data to initialise itself and then validates and normalises the results, after all. So we can take that dictionary from anywhere.

Creating an appropriate form for validating our max/min constraint is fairly easy:

from django import forms

class LimitForm(forms.ModelForm):
    class Meta:
        model = Limit

    def clean(self):
        maximum = self.cleaned_data.get("maximum")
        minimum = self.cleaned_data.get("minimum")
        if maximum is not None and minimum is not None and maximum < minimum:
            raise forms.ValidationError("maximum (%d) must be greater "
                     "than minimum (%d)." % (maximum, minimum))
        return self.cleaned_data

Teaching The Model To Validate

We can use this in our model in a couple of ways. My current preference is to add a method to the model that I can call explicitly to check for validity. It also saves any errors from the form class in case I want to check them for more details (the _errors attribute in the following fragment). Thus, we can add something like this to the model class:

from django import forms       # Needed for model_to_dict()

class Limit:
    # Everything as above and then add...

    def is_valid(self):
        validity_check = LimitForm(forms.model_to_dict(self))
        if not validity_check.is_valid():
            self._errors = validity_check._errors
        return validity_check.is_valid()

    def save(self, *args, **kwargs):
        if not self.is_valid():
            raise Exception("Attempting to save invalid model.")
        return super(Limit, self).save(*args, **kwargs)

I've also hooked up the is_valid() method to the save() method to avoid any accidental saving. But usually I use this sort of pattern in code by just checking my_object.is_valid() before trying to save it.

Thoughts

This pattern isn't a perfect solution, since raising validation errors from model saving isn't really a good design. Ideally, we'd only have to worry about database errors there, but since I know which models I'm dealing with like this, I can manage the slight wart. There's also no way to have, say, the admin interface or a random ModelForm class use this validation sensibly. That is, there's no way to feed back validation problems to something else unless it knows about the particular modifications I've made to add is_valid().

That's why Django itself will have proper model-aware validation in the near future. We've worked out what it should look like and Honza Kral is working on a revised implementation. It's not an entirely trivial problem because of the "model-aware" portion of it (knowing about "self" when validating an instance)) and a requirement to allow validators and data cleaners to be useful both on forms and models.

Still, for a fairly large number of situations, the above ideas are useful enough. They act as a good fallback to ensure an exception is raised when you know that the data should be valid, but want to check that it actually is valid. This is precisely the case when you're creating data programmatically, rather than having it entered by humans into browser-based forms.

Topics: software/django/tips