12

I have a Django form with a username and email field. I want to check the email isn't already in use by a user:

def clean_email(self):
    email = self.cleaned_data["email"]
    if User.objects.filter(email=email).count() != 0:
        raise forms.ValidationError(_("Email not available."))
    return email

This works, but raises some false negatives because the email might already be in the database for the user named in the form. I want to change to this:

def clean_email(self):
    email = self.cleaned_data["email"]
    username = self.cleaned_data["username"]
    if User.objects.filter(email=email,  username__ne=username).count() != 0:
        raise forms.ValidationError(_("Email not available."))
    return email

The Django docs say that all the validation for one field is done before moving onto the next field. If email is cleaned before username, then cleaned_data["username"] won't be available in clean_email. But the docs are unclear as to what order the fields are cleaned in. I declare username before email in the form, does that mean I'm safe in assuming that username is cleaned before email?

I could read the code, but I'm more interested in what the Django API is promising, and knowing that I'm safe even in future versions of Django.

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • What if the user changed his/her username in the form? Then `username__ne=username` won't exclude that user because the old username will still be in the database. – Ben James Jul 21 '10 at 13:10

4 Answers4

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
  • 78,177
  • 11
  • 137
  • 148
  • that's the paragraph I read, and it doesn't claim the fields are process in order. "These methods are run in the order given above" refers to the bullet list of methods: to_python(), validate(), run_validators(), clean(), clean_(), form.clean(). – Ned Batchelder Jul 21 '10 at 13:18
  • 4
    That's not true Ned Batchelder, look at this part: `for each field in the form (in the order they are declared in the form definition)` – Wolph Jul 21 '10 at 13:22
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.

Jordan Reiter
  • 20,467
  • 11
  • 95
  • 161
  • I should add that credit goes to Selene's answer to a related SO question here: http://stackoverflow.com/a/1133470/255918 – Jordan Reiter Mar 22 '12 at 20:09
  • It really works. It isn't an exact answer to "can I count on the order of field validation", but it's a solution if you want to set the order. – Pedro Werneck May 14 '12 at 22:45
  • 1
    @Jonathan It looks like Django is now using an OrderedDict to store the fields. I'll put some updated code in my answer. – Jordan Reiter Nov 07 '17 at 16:30
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.

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • 2
    That's not correct. There is a promise on the processing order of the fields. It's just not kept well in all versions. Perhaps the newer Django versions do hold up to this promise but I haven't verified that. – Wolph Jul 21 '10 at 13:21
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
  • 2,250
  • 2
  • 17
  • 16