表单和字段验证

表单验证发生在数据验证之后。 如果你想自定义这个过程,有不同的地方可以进行更改,每个都有不同的用途。 表单处理过程中要运行三种类别的验证方法。 它们通常在你调用表单的is_valid() 方法时执行。 还有其他一些事情也可以触发清理和验证(访问errors属性或直接调用full_clean()),但通常不需要它们。

一般情况下,如果处理的数据有问题,每个类别的验证方法都会引发ValidationError,并将相关信息传递给ValidationError See below中引发ValidationError 的最佳实践。 如果没有引发ValidationError,这些方法应该返回验证后的(规整化的)数据的Python 对象。

大部分应该可以使用validators 完成,它们可以很容易地重用。 Validators 是简单的函数(或可调用对象),它们接收一个参数并对非法的输入抛出ValidationError Validators 在字段的to_pythonvalidate 方法调用之后运行。

表单的验证分为几个步骤,可以自定义或覆盖:

  • Field上的to_python()方法是每次验证的第一步。 它强制该值为正确的数据类型,并引发ValidationError,如果这是不可能的。 这个方法从Widget 接收原始的值并返回转换后的值。 例如,一个FloatField将数据转换成一个Python float或者提起一个ValidationError

  • Field上的validate()方法处理不适合验证器的字段特定验证。 它需要一个被强制为正确的数据类型的值,并在任何错误上引发ValidationError 这个方法不返回任何东西且不应该改变任何值。 当你遇到不可以或不想放在validator 中的验证逻辑时,应该覆盖它来处理验证。

  • Field上的run_validators()方法运行所有字段的验证器,并将所有错误聚合到单个ValidationError中。 你应该不需要覆盖这个方法。

  • Field子类的clean()方法负责运行to_python()validate()run_validators()以正确的顺序传播错误。 如果任何时刻、任何方法引发ValidationError,验证将停止并引发这个错误。 这个方法返回验证后的数据,这个数据在后面将插入到表单的 cleaned_data 字典中。

  • 在表单子类中调用clean_<fieldname>()方法,其中<fieldname>替换为表单域属性的名称。 这个方法完成于特定属性相关的验证,这个验证与字段的类型无关。 这个方法没有任何传入的参数。 你需要查找clean() 中该字段的值,记住此时它已经是一个Python 对象而不是表单中提交的原始字符串(它位于cleaned_data 中是因为字段的self.cleaned_data 方法已经验证过一次数据)。

    例如,如果你想验证名为clean_serialnumber()serialnumber 的内容是否唯一, CharField 将是实现这个功能的理想之处。 你需要的不是一个特别的字段(它只是一个CharField),而是一个特定于表单字段特定验证,并规整化数据。

    此方法的返回值将替换cleaned_data中的现有值,因此它必须是来自cleaned_data的字段值(即使此方法未更改)或新的清洁价值。

  • 表单子类的clean()方法可以执行需要访问多个表单字段的验证。 这是您可以在哪里进行检查,例如“如果提供了字段A,字段B必须包含有效的电子邮件地址”。 这个方法可以返回一个完全不同的字典,该字典将用作cleaned_data

    因为字段的验证方法在调用clean() 时会运行,你还可以访问表单的errors 属性,它包含验证每个字段时的所有错误。

    注意,你覆盖的Form.clean() 引发的任何错误将不会与任何特定的字段关联。 它们位于一个特定的“字段”(叫做__all__)中,如果需要可以通过 non_field_errors() 方法访问。 如果你想添加一个特定字段的错误到表单中,需要调用 add_error()

    还要注意,覆盖clean() 子类的ModelForm 方法需要特殊的考虑。 (更多信息参见ModelForm documentation)。

这些方法按以上给出的顺序执行,一次验证一个字段。 也就是说,对于表单中的每个字段(按它们在表单定义中出现的顺序),先运行Field.clean() ,然后运行clean_<fieldname>() 每个字段的这两个方法都执行完之后,最后运行Form.clean() 方法,无论前面的方法是否抛出过异常。

下面有上面每个方法的示例。

我们已经提到过,所有这些方法都可以抛出ValidationError 对于任何字段,如果Field.clean()方法引发了一个ValidationError,则不会调用任何字段特定的清除方法。 但是,剩余的字段的验证方法仍然会执行。

ValidationError

为了让错误信息更加灵活或容易重写,请考虑下面的准则:

  • 给构造函数提供一个富有描述性的错误码code

    # Good
    ValidationError(_('Invalid value'), code='invalid')
    
    # Bad
    ValidationError(_('Invalid value'))
    
  • 不要将变量强加到消息中;使用占位符和构造函数的params参数:

    # Good
    ValidationError(
        _('Invalid value: %(value)s'),
        params={'value': '42'},
    )
    
    # Bad
    ValidationError(_('Invalid value: %s') % value)
    
  • 使用字典参数而不要用位置参数。 这使得重写错误信息时不用考虑变量的顺序或者完全省略它们:

    # Good
    ValidationError(
        _('Invalid value: %(value)s'),
        params={'value': '42'},
    )
    
    # Bad
    ValidationError(
        _('Invalid value: %s'),
        params=('42',),
    )
    
  • gettext 封装错误消息使得它可以翻译:

    # Good
    ValidationError(_('Invalid value'))
    
    # Bad
    ValidationError('Invalid value')
    

所有的准则放在一起就是:

raise ValidationError(
    _('Invalid value: %(value)s'),
    code='invalid',
    params={'value': '42'},
)

如果你想编写可重用的表单、表单字段和模型字段,遵守这些准则是非常必要的。

如果你在验证的最后(例如,表单的clean() 方法)且知道永远 不需要重新错误信息,虽然不提倡但你仍然可以选择重写不详细的信息:

ValidationError(_('Invalid value: %s') % value)

Form.errors.as_data()Form.errors.as_json() 方法很大程度上受益于code(利用params 名和ValidationError 字典)。

抛出多个错误

如果在一个验证方法中检查到多个错误并且希望将它们都反馈给表单的提交者,可以传递一个错误的列表给ValidationError 构造函数。

和上面一样,建议传递的列表中的params 实例都带有 codeValidationError,但是传递一个字符串列表也可以工作:

# Good
raise ValidationError([
    ValidationError(_('Error 1'), code='error1'),
    ValidationError(_('Error 2'), code='error2'),
])

# Bad
raise ValidationError([
    _('Error 1'),
    _('Error 2'),
])

在实践中使用验证

前面几节解释在一般情况下表单的验证是如何工作的。 因为有时直接看功能在实际中的应用会更容易掌握,下面是一些列小例子,它们用到前面的每个功能。

使用验证器

Django 的表单(以及模型)字段支持使用简单的函数和类用于验证,它们叫做Validator。 Validator 是可调用对象或函数,它接收一个值,如果该值合法则什么也不返回,否则抛出ValidationError 它们可以通过字段的validators 参数传递给字段的构造函数,或者定义在Field 类的default_validators 属性中。

简单的Validator 可以用于在字段内部验证值,让我们看下Django 的SlugField

from django.forms import CharField
from django.core import validators

class SlugField(CharField):
    default_validators = [validators.validate_slug]

正如你所看到的,SlugField 只是一个带有自定义Validator 的CharField,它们验证提交的文本符合某些字符规则。 这也可以在字段定义时实现,所以:

slug = forms.SlugField()

等同于:

slug = forms.CharField(validators=[validators.validate_slug])

常见的情形,例如验证邮件地址和正则表达式,可以使用Django 中已经存在的Validator 类处理。 例如,validators.validate_slugRegexValidator 的一个实例,它构造时的第一个参数为:^[-a-zA-Z0-9_]+$ writing validators 一节可以查到已经存在的Validator 以及如何编写Validator 的一个示例。

表单域默认清除

让我们首先创建一个自定义的表单字段,它验证其输入是一个由逗号分隔的邮件地址组成的字符串。 完整的类像这样:

from django import forms
from django.core.validators import validate_email

class MultiEmailField(forms.Field):
    def to_python(self, value):
        """Normalize data to a list of strings."""
        # Return an empty list if no input was given.
        if not value:
            return []
        return value.split(',')

    def validate(self, value):
        """Check if value consists only of valid emails."""
        # Use the parent's handling of required fields, etc.
        super(MultiEmailField, self).validate(value)
        for email in value:
            validate_email(email)

使用这个字段的每个表单都将在处理该字段数据之前运行这些方法。 这个验证特定于该类型的字段,与后面如何使用它无关。

让我们来创建一个简单的ContactForm 来向你演示如何使用这个字段:

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField()
    sender = forms.EmailField()
    recipients = MultiEmailField()
    cc_myself = forms.BooleanField(required=False)

只需要简单地使用MultiEmailField,就和其它表单字段一样。 当调用表单的to_python() 方法时,MultiEmailField.clean() 方法将作为验证过程的一部分运行,它将调用自定义的is_valid()validate() 方法。

清理特定字段属性

继续前面的例子,假设在ContactForm中,我们要确保recipients字段始终包含地址"fred@example.com" 这是对我们表单特定的验证,所以我们不想把它放在一般的MultiEmailField类中。 相反,我们写一个在recipients字段上运行的清理方法,像这样:

from django import forms

class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean_recipients(self):
        data = self.cleaned_data['recipients']
        if "fred@example.com" not in data:
            raise forms.ValidationError("You have forgotten about Fred!")

        # Always return a value to use as the new cleaned data, even if
        # this method didn't change it.
        return data

清理和验证相互依赖的字段

假设我们向联系表单添加了另一个要求:如果cc_myself字段是True,则subject必须包含单词"help" 我们一次在多个字段上执行验证,因此表单的clean()方法是一个很好的选择。 请注意,我们正在谈论这里的表单上的clean()方法,而较早的我们在一个字段上写了一个clean()方法。 在确定哪些地方进行验证时,保持领域和形式差异很重要。 字段是单个数据点,表单是字段的集合。

在调用表单clean() 方法的时候,所有字段的验证方法已经执行完(前两节),所以self.cleaned_data 填充的是目前为止已经合法的数据。 所以你需要记住这个事实,你需要验证的字段可能没有通过初试的字段检查。

在这一步,有两种方法报告错误。 最简单的方法是在表单的顶端显示错误。 你可以在ValidationError 方法中抛出clean() 来创建错误。 像这样:

from django import forms

class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super(ContactForm, self).clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject:
            # Only do something if both fields are valid so far.
            if "help" not in subject:
                raise forms.ValidationError(
                    "Did not send for 'help' in the subject despite "
                    "CC'ing yourself."
                )

在这段代码中,如果抛出验证错误,表单将在表单的顶部显示(通常是)描述该问题的一个错误信息。

在示例代码中调用super(ContactForm, self).clean()可以确保父类中的任何验证逻辑都被维护。 If your form inherits another that doesn’t return a cleaned_data dictionary in its clean() method (doing so is optional), then don’t assign cleaned_data to the result of the super() call and use self.cleaned_data instead:

def clean(self):
    super(ContactForm, self).clean()
    cc_myself = self.cleaned_data.get("cc_myself")
    ...

报告验证错误的第二种方法可能包括将错误消息分配给其中一个字段。 在这种情况下,让我们在表单的显示中分别关联一个错误信息到“subject” 和“cc_myself” 行。 在实际应用中要小心,因为它可能导致表单的输出变得令人困惑。 我们只是向你展示这里可以怎么做,在特定的情况下,需要你和你的设计人员确定什么是好的方法。 我们的新代码(代替前面的示例)像这样:

from django import forms

class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super(ContactForm, self).clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject and "help" not in subject:
            msg = "Must put 'help' in subject when cc'ing yourself."
            self.add_error('cc_myself', msg)
            self.add_error('subject', msg)

add_error() 的第二个参数可以是一个简单的字符串,但更倾向是ValidationError 的一个实例。 更多细节参见Raising ValidationError 注意,add_error() 将从cleaned_data 中删除相应的字段。