表单和字段验证 — Django 文档

来自菜鸟教程
Django/docs/3.1.x/ref/forms/validation
跳转至:导航、​搜索

表单和字段验证

清理数据时会进行表单验证。 如果您想自定义此过程,可以在多个地方进行更改,每个地方都有不同的用途。 在表单处理期间运行三种类型的清洁方法。 这些通常在您调用表单上的 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() 方法时有一些特殊注意事项。 (有关更多信息,请参阅 ModelForm 文档

这些方法按上面给出的顺序运行,一次一个字段。 也就是说,对于表单中的每个字段(按照它们在表单定义中声明的顺序),运行 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() 方法极大地受益于全功能的 ValidationError(带有 code名称和 params 字典)。

引发多个错误

如果您在清理方法期间检测到多个错误并希望将所有错误都发送给表单提交者,则可以将错误列表传递给 ValidationError 构造函数。

如上所述,建议使用 codes 和 params 传递 ValidationError 实例列表,但字符串列表也适用:

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

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

在实践中使用验证

前面的部分解释了验证通常如何对表单进行工作。 由于有时通过查看使用中的每个功能可以更容易地将东西放置到位,因此这里有一系列使用前面每个功能的小示例。

使用验证器

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

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() 和 [ X166X] 方法。


清理特定的字段属性

继续前面的示例,假设在我们的 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 字典的表单(这样做是可选的),则不要将 cleaned_data 分配给结果super() 调用并使用 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 的实例。 有关更多详细信息,请参阅 Raising ValidationError。 请注意,add_error() 会自动从 cleaned_data 中删除该字段。