表单和字段验证

    一般来说,任何清理方法都可以在处理的数据出现问题时引发 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 属性,它包含了所有清理单个字段时产生的错误。

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

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

    以下是这些方法的例子。

    如上所述,这些方法中的任何一种都可能引起 ValidationError。对于任何字段,如果 Field.clean() 方法引起 ValidationError,则不调用任何特定字段的清理方法。但是,所有其余字段的清理方法仍然会被执行。

    为了使错误信息灵活且易于覆盖,请考虑以下准则:

    • 为构造函数提供一个描述性错误 code

    • 不要在信息中强行加入变量;使用占位符和构造函数的 params 参数:

      1. # Good
      2. ValidationError(
      3. _('Invalid value: %(value)s'),
      4. params={'value': '42'},
      5. )
      6. # Bad
    • 使用映射键代替位置格式化。这样可以在重写信息时,将变量按任何顺序排列或完全省略:

      1. # Good
      2. ValidationError(
      3. _('Invalid value: %(value)s'),
      4. params={'value': '42'},
      5. )
      6. # Bad
      7. ValidationError(
      8. _('Invalid value: %s'),
      9. params=('42',),
      10. )

    把它放在一起:

    1. raise ValidationError(
    2. _('Invalid value: %(value)s'),
    3. code='invalid',
    4. params={'value': '42'},
    5. )

    如果你写的是可重用的表单、表单字段和模型字段,那么遵循这些准则是特别必要的。

    虽然不建议使用,但如果你处于验证链的末端(即你的表单 clean() 方法),并且你知道你将永远不需要覆盖你的错误信息,你仍然可以选择不那么啰嗦的:

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

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

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

    1. # Good
    2. raise ValidationError([
    3. ValidationError(_('Error 1'), code='error1'),
    4. ValidationError(_('Error 2'), code='error2'),
    5. ])
    6. # Bad
    7. raise ValidationError([
    8. _('Error 1'),
    9. _('Error 2'),
    10. ])

    在实践中使用验证

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

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

    1. from django.core import validators
    2. from django.forms import CharField
    3. class SlugField(CharField):
    4. default_validators = [validators.validate_slug]

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

    1. slug = forms.SlugField()

    相当于:

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

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

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

    每个使用该字段的表单在对字段的数据进行任何操作之前,都会运行这些方法。这是专门针对这种类型的字段进行的清理,不管它随后如何使用。

    让我们创建一个 ContactForm 来演示如何使用这个字段:

    1. class ContactForm(forms.Form):
    2. subject = forms.CharField(max_length=100)
    3. message = forms.CharField()
    4. sender = forms.EmailField()
    5. recipients = MultiEmailField()
    6. cc_myself = forms.BooleanField(required=False)

    像使用其他表单字段一样使用 。当在表单上调用 is_valid() 方法时,MultiEmailField.clean() 方法将作为清理过程的一部分被运行,它将反过来调用自定义的 to_python()validate() 方法。

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

    1. from django import forms
    2. class ContactForm(forms.Form):
    3. # Everything as before.
    4. ...
    5. def clean_recipients(self):
    6. data = self.cleaned_data['recipients']
    7. if "fred@example.com" not in data:
    8. raise ValidationError("You have forgotten about Fred!")
    9. # Always return a value to use as the new cleaned data, even if
    10. # this method didn't change it.
    11. return data

    假设我们在联系表单中添加了另一个要求:如果 cc_myself 字段为 True,则 subject 必须包含 "help" 一词。我们同时对多个字段进行验证,所以表单的 方法是一个很好的地方。注意,我们这里说的是表单上的 clean() 方法,而前面我们是在一个字段上写一个 clean() 方法。在研究验证东西的位置时,明确字段和表单的区别是很重要的。字段是单个数据点,表单是字段的集合。

    当表单的 clean() 方法被调用时,所有的单独字段清理方法都会被运行(前面两节),所以 self.cleaned_data 将被填充到目前为止存活的任何数据中。所以你还需要记住,要验证的字段可能没有通过最初的单个字段检查。

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

    1. from django import forms
    2. from django.core.exceptions import ValidationError
    3. class ContactForm(forms.Form):
    4. # Everything as before.
    5. ...
    6. def clean(self):
    7. cleaned_data = super().clean()
    8. cc_myself = cleaned_data.get("cc_myself")
    9. subject = cleaned_data.get("subject")
    10. if cc_myself and subject:
    11. # Only do something if both fields are valid so far.
    12. if "help" not in subject:
    13. raise ValidationError(
    14. "Did not send for 'help' in the subject despite "
    15. "CC'ing yourself."
    16. )

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

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

    1. def clean(self):
    2. super().clean()
    3. cc_myself = self.cleaned_data.get("cc_myself")

    add_error() 的第二个参数可以是一个字符串,或者最好是 ValidationError 的一个实例。更多细节请参见 引发 ValidationError。请注意, 会自动从 cleaned_data 中删除该字段。