表单和字段验证

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

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

大多数验证可以使用验证器来完成——可以重复使用的辅助程序。验证器是函数(或可调用对象),它们接受单个参数,并在输入无效时引发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 构造函数。

如上所述,建议传递带有 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_slugRegexValidator 的一个实例,其第一个参数是模式:^[-a-zA-Z0-9_]+\Z。请参阅有关 编写验证器 的部分,以查看已提供的列表以及如何编写验证器的示例。

表单字段默认清理

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

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 字典(这样做是可选的),则不要将 cleaned_data 分配给 super() 调用的结果,而应使用 self.cleaned_data

def clean(self):
    super().clean()
    cc_myself = self.cleaned_data.get("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 中删除该字段。

返回顶部