Skip to main content

表单和字段验证

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

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

大多数验证可以使用 validators - 简单的帮助,可以轻松地重复使用。验证器是简单函数(或可调用),它接受单个参数,并在无效输入上提高 ValidationError。验证器在字段的 to_pythonvalidate 方法被调用之后运行。

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

  • Fieldto_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),但你想要一个formfield特定的验证,并可能,清洗/规范化数据。

    此方法应返回从 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() 方法)的结尾,并且您知道您将 never 需要覆盖您的错误消息,您仍然可以选择较少冗长:

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

Form.errors.as_data()Form.errors.as_json() 方法极大地受益于全功能的 ValidationError (具有 code 名称和 params 字典)。

提高多个错误

如果在清除方法期间检测到多个错误,并希望将其全部发送到表单提交器,那么可以将错误列表传递给 ValidationError 构造函数。

如上所述,建议传递一个包含 code s和 paramsValidationError 实例列表,但字符串列表也会工作:

# 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 类本身上定义。

简单的验证器可以用来验证字段中的值,让我们来看看Django的 SlugField:

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

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

正如你所看到的,SlugField 只是一个具有自定义验证器的 CharField,它验证提交的文本是否遵守某些字符规则。这也可以在字段定义上这样做:

slug = forms.SlugField()

相当于:

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

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

表单字段默认清洗

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

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。当在表单上调用 is_valid() 方法时,MultiEmailField.clean() 方法将作为清理过程的一部分运行,并且它将调用自定义 to_python()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 the cleaned data, whether you have changed it or
        # not.
        return data

清洁和验证彼此依赖的字段

假设我们向联系表单添加另一个要求:如果 cc_myself 字段是 True,则 subject 必须包含单词 "help"。我们一次对多个字段执行验证,因此表单的 clean() 方法是一个很好的做法。注意,我们在这里的表单上谈论 clean() 方法,而早期我们在字段上写了一个 clean() 方法。当工作在哪里验证东西时,保持字段和形式差异很重要是很重要的。字段是单个数据点,表单是字段的集合。

当表单的 clean() 方法被调用时,所有单独的字段clean方法都将运行(前两个部分),因此 self.cleaned_data 将填充迄今为止存活的任何数据。所以你还需要记住,你想要验证的字段可能没有幸存的初始单个字段检查的事实。

有两种方法可以报告此步骤中的任何错误。可能最常见的方法是在窗体顶部显示错误。要创建这样的错误,您可以从 clean() 方法提出 ValidationError。例如:

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() 的调用确保维护父类中的任何验证逻辑。如果你的表单继承了另一个不在其 clean() 方法中返回 cleaned_data 字典(这样做是可选的),那么不要将 cleaned_data 分配给 super() 调用的结果,而改用 self.cleaned_data:

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

用于报告验证错误的第二种方法可能涉及将错误消息分配给其中一个字段。在这种情况下,让我们向表单显示中的“主题”和“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 的实例。有关详细信息,请参阅 提高 ValidationError。请注意,add_error() 会自动从 cleaned_data 中删除该字段。