[Fixed]-Can I count on the order of field validation in a Django form?

9👍

The Django docs claim that it’s in order of the field definition.

But I’ve found that it doesn’t always hold up to that promise.
Source: http://docs.djangoproject.com/en/dev/ref/forms/validation/

These methods are run in the order
given above, one field at a time. That
is, for each field in the form (in the
order they are declared in the form
definition), the Field.clean() method
(or its override) is run, then
clean_(). Finally, once
those two methods are run for every
field, the Form.clean() method, or its
override, is executed.

👤Wolph

9👍

Update

.keyOrder no longer works. I believe this should work instead:

from collections import OrderedDict


class MyForm(forms.ModelForm):
    …

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        field_order = ['has_custom_name', 'name']
        reordered_fields = OrderedDict()
        for fld in field_order:
            reordered_fields[fld] = self.fields[fld]
        for fld, value in self.fields.items():
            if fld not in reordered_fields:
                reordered_fields[fld] = value
        self.fields = reordered_fields

Previous Answer

There are things that can alter form order regardless of how you declare them in the form definition. One of them is if you’re using a ModelForm, in which case unless you have both fields declared in fields under class Meta they are going to be in an unpredictable order.

Fortunately, there is a reliable solution.

You can control the field order in a form by setting self.fields.keyOrder.

Here’s some sample code you can use:

class MyForm(forms.ModelForm):
    has_custom_name = forms.BooleanField(label="Should it have a custom name?")
    name = forms.CharField(required=False, label="Custom name")

    class Meta:
        model = Widget
        fields = ['name', 'description', 'stretchiness', 'egginess']

    def __init__(self, *args, **kwargs):
        super(MyForm, self).__init__(*args, **kwargs)
        ordered_fields = ['has_custom_name', 'name']
        self.fields.keyOrder = ordered_fields + [k for k in self.fields.keys() if k not in ordered_fields]

    def clean_name(self):
        data = self.cleaned_data
        if data.get('has_custom_name') and not data.get('name'):
            raise forms.ValidationError("You must enter a custom name.")
        return data.get('name')

With keyOrder set, has_custom_name will be validated (and therefore present in self.cleaned_data) before name is validated.

8👍

There’s no promise that the fields are processed in any particular order. The official recommendation is that any validation that depends on more than one field should be done in the form’s clean() method, rather than the field-specific clean_foo() methods.

3👍

The Form subclass’s clean() method. This method can perform any
validation that requires access to multiple fields from the form at
once. This is where you might put in things to check that if field A
is supplied, field B must contain a valid email address and the like.
The data that this method returns is the final cleaned_data attribute
for the form, so don’t forget to return the full list of cleaned data
if you override this method (by default, Form.clean() just returns
self.cleaned_data).

Copy-paste from https://docs.djangoproject.com/en/dev/ref/forms/validation/#using-validators

This means that if you want to check things like the value of the email and the parent_email are not the same you should do it inside that function. i.e:

from django import forms

from myapp.models import User

class UserForm(forms.ModelForm):
    parent_email = forms.EmailField(required = True)

    class Meta:
        model = User
        fields = ('email',)

    def clean_email(self):
        # Do whatever validation you want to apply to this field.
        email = self.cleaned_data['email']
        #... validate and raise a forms.ValidationError Exception if there is any error
        return email

    def clean_parent_email(self):
        # Do the all the validations and operations that you want to apply to the
        # the parent email. i.e: Check that the parent email has not been used 
        # by another user before.
        parent_email = self.cleaned_data['parent_email']
        if User.objects.filter(parent_email).count() > 0:
            raise forms.ValidationError('Another user is already using this parent email')
        return parent_email

    def clean(self):
        # Here I recommend to user self.cleaned_data.get(...) to get the values 
        # instead of self.cleaned_data[...] because if the clean_email, or 
        # clean_parent_email raise and Exception this value is not going to be 
        # inside the self.cleaned_data dictionary.

        email = self.cleaned_data.get('email', '')
        parent_email = self.cleaned_data.get('parent_email', '')
        if email and parent_email and email == parent_email:
            raise forms.ValidationError('Email and parent email can not be the same')
        return self.cleaned_data
👤Harph

Leave a comment