开发者

Can I count on the order of field validation in a Django form?

开发者 https://www.devze.com 2023-01-08 10:19 出处:网络
I have a Django form with a username and email field.I want to 开发者_如何学Ccheck the email isn\'t already in use by a user:

I have a Django form with a username and email field. I want to 开发者_如何学Ccheck 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.


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.


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.


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.


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
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号