表单和字段验证

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

一般来说,任何清理方法都可以在处理的数据出现问题时引发 ValidationError,将相关信息传递给 ValidationError 构造函数。 参见下文 关于引发 ``ValidationError` 的最佳实践。如果没有引发 ValidationError,该方法应该将清理后(规范化)的数据作为 Python 对象返回。

大多数验证可以使用 validators —— 可以重复使用的辅助功能来完成。验证器是函数(或可调用对象),它只接受一个参数,并在无效输入时引发 ValidationError。验证器在字段的 to_pythonvalidate 方法被调用后运行。

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

  • Field 上的 to_python() 方法是每次验证的第一步。它强制将值转换为正确的数据类型,并在不可能的情况下引发 ValidationError。该方法接受来自部件的原始值并返回转换后的值。例如,一个 FloatField 将把数据变成 Python float 或引发 ValidationError

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

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

  • Field 子类上的 clean() 方法负责以正确的顺序运行 to_python()validate()run_validators() 并传播它们的错误。如果在任何时候,任何一个方法引发了 ValidationError,验证就会停止,并引发该错误。该方法返回干净的数据,然后将其插入到表单的 cleaned_data 字典中。

  • clean_<fieldname>() 方法是在表单子类上调用的——其中 <fieldname> 被替换为表单字段属性的名称。这个方法做任何特定属性的清理工作,与字段的类型无关。这个方法不传递任何参数。你需要在 self.cleaned_data 中查找字段的值,并且记住,此时它将是一个 Python 对象,而不是在表单中提交的原始字符串(它将在 cleaned_data 中,因为上面的一般字段 clean() 方法已经清理了一次数据)。

    例如,如果你想验证一个叫 serialnumberCharField 的内容是唯一的,clean_serialnumber() 就可以做这件事。你不需要一个特定的字段(它是一个 CharField),但你需要一个特定字段的验证,可能的话,清理/规范数据。

    这个方法的返回值会替换 cleaned_data 中的现有值,所以它必须是 cleaned_data 中的字段值(即使这个方法没有改变它)或一个新的干净值。

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

    因为在调用 clean() 时,字段验证方法已经运行,所以你也可以访问表单的 errors 属性,它包含了所有清理单个字段时产生的错误。

    请注意,任何由 Form.clean() 覆盖引起的错误都不会与任何特定的字段相关联。它们会进入一个特殊的“字段”(称为 __all__),如果需要的话,你可以通过 non_field_errors() 方法来访问。如果你想将错误附加到表单中的某个特定字段,你需要调用 add_error()

    还需要注意的是,当覆盖 ModelForm 子类的 clean() 方法时,有一些特殊的注意事项。(更多信息见 模型表单文档

这些方法按上述顺序运行,每次只运行一个字段。 也就是说,对于表单中的每一个字段(按照表单定义中声明的顺序),先运行 Field.clean() 方法(或其覆盖),然后运行 clean_()。最后,一旦这两个方法对每个字段都被运行,无论前面的方法是否出错,都会执行 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() 方法极大地受益于功能齐全的 ValidationError (带有 code 名称和 params 字典)。

引发多个错误

如果在清理方法中检测到多个错误,并希望向表单提交者发出所有错误信号,可以将错误列表传递给 ValidationError 构造函数。

如上所述,建议传递一个带有 codeparamsValidationError 实例列表,但一个字符串列表也可以:

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

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

在实践中使用验证

前面的章节解释了一般表单的验证是如何工作的。由于有时通过看到每个功能的使用,可以更容易地将事情落实到位,这里有一系列使用前面每个功能的小例子。

使用验证器

Django 的表单(和模型)字段支持使用被称为验证器的实用函数和类。验证器是一个可调用对象或函数,它接收一个值,如果该值有效则不返回任何内容,如果无效则引发一个 ValidationError。这些可以通过字段的 validators 参数传递给字段的构造函数,或者在 Field 类本身的 default_validators 属性中定义。

验证器可以用来验证字段内部的值,我们来看看 Django 的 SlugField

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

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

正如你所看到的,SlugField 是一个带有自定义验证器的 CharField,它可以验证提交的文本是否符合某些字符规则。这也可以在字段定义中完成:

slug = forms.SlugField()

相当于:

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

常见的情况下,如对电子邮件或正则表达式进行验证,可以使用 Django 中现有的验证器类来处理。例如,validators.validate_slug 是一个 RegexValidator 的实例,它的第一个参数是模式:^[-a-zA-Z0-9_]+$`。参见 编写验证器 一节,查看已有验证器的列表,以及如何编写验证器的例子。

表单字段默认清理

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

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().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。当在表单上调用 is_valid() 方法时,MultiEmailField.clean() 方法将作为清理过程的一部分被运行,它将反过来调用自定义的 to_python()validate() 方法。

清理一个特定的字段属性

继续上一个例子,假设在我们的 ContactForm 中,我们想确保 recipients 字段总是包含地址 "fred@example.com"。这是我们的表单所特有的验证,所以我们不想把它放到一般的 MultiEmailField 类中。取而代之的是,我们写了一个清理方法,对 recipients 字段进行操作,就像这样:

from django import forms
from django.core.exceptions import ValidationError

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

    def clean_recipients(self):
        data = self.cleaned_data['recipients']
        if "fred@example.com" not in data:
            raise 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 将被填充到目前为止存活的任何数据中。所以你还需要记住,要验证的字段可能没有通过最初的单个字段检查。

有两种方法可以报告这一步的任何错误。最常见的方法可能是在表单顶部显示错误。要创建这样一个错误,你可以从 clean() 方法中引发一个 ValidationError。例如:

from django import forms
from django.core.exceptions import ValidationError

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

    def clean(self):
        cleaned_data = super().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 ValidationError(
                    "Did not send for 'help' in the subject despite "
                    "CC'ing yourself."
                )

在这段代码中,如果出现验证错误,表单将在表单顶部显示错误信息(通常),描述问题。这种错误属于非字段错误,在模板中用 {form.non_field_errors }} 显示。

示例代码中对 super().clean() 的调用确保了父类中的任何验证逻辑得到了维护。如果你的表单继承了另一个没有在其 clean() 方法中返回 cleaned_data 字典的表单(这样做是可选的),那么不要给 super() 调用的结果分配 cleaned_data,而使用 self.cleaned_data 来代替:

def clean(self):
    super().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().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 的一个实例。更多细节请参见 引发 ValidationError。请注意,add_error() 会自动从 cleaned_data 中删除该字段。