表单和字段验证
一般来说,任何清理方法都可以在处理的数据出现问题时引发 ValidationError
,将相关信息传递给 ValidationError
构造函数。 参见下文 关于引发 ``ValidationError` 的最佳实践。如果没有引发 ValidationError
,该方法应该将清理后(规范化)的数据作为 Python 对象返回。
大多数验证可以使用 —— 可以重复使用的辅助功能来完成。验证器是函数(或可调用对象),它只接受一个参数,并在无效输入时引发 ValidationError
。验证器在字段的 to_python
和 validate
方法被调用后运行。
表单的验证分为几个步骤,可以自定义或覆盖:
Field
上的to_python()
方法是每次验证的第一步。它强制将值转换为正确的数据类型,并在不可能的情况下引发ValidationError
。该方法接受来自部件的原始值并返回转换后的值。例如,一个FloatField
将把数据变成 Pythonfloat
或引发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()
方法已经清理了一次数据)。例如,如果你想验证一个叫
serialnumber
的CharField
的内容是唯一的,clean_serialnumber()
就可以做这件事。你不需要一个特定的字段(它是一个CharField
),但你需要一个特定字段的验证,可能的话,清理/规范数据。这个方法的返回值会替换
cleaned_data
中的现有值,所以它必须是cleaned_data
中的字段值(即使这个方法没有改变它)或一个新的干净值。表单子类的
clean()
方法可以执行需要访问多个表单字段的验证。在这里,你可以放入诸如“如果提供了字段A
,字段B
必须包含一个有效的电子邮件地址”这样的检查。如果你愿意,这个方法可以返回一个完全不同的字典,这个字典将被用作cleaned_data
。因为在调用
clean()
时,字段验证方法已经运行,所以你也可以访问表单的errors
属性,它包含了所有清理单个字段时产生的错误。请注意,任何由 Form.clean() 覆盖引起的错误都不会与任何特定的字段相关联。它们会进入一个特殊的“字段”(称为
__all__
),如果需要的话,你可以通过 方法来访问。如果你想将错误附加到表单中的某个特定字段,你需要调用 add_error()。还需要注意的是,当覆盖
ModelForm
子类的clean()
方法时,有一些特殊的注意事项。(更多信息见 )
以下是这些方法的例子。
如上所述,这些方法中的任何一种都可能引起 ValidationError
。对于任何字段,如果 Field.clean()
方法引起 ValidationError
,则不调用任何特定字段的清理方法。但是,所有其余字段的清理方法仍然会被执行。
为了使错误信息灵活且易于覆盖,请考虑以下准则:
为构造函数提供一个描述性错误
code
:不要在信息中强行加入变量;使用占位符和构造函数的
params
参数:# Good
ValidationError(
_('Invalid value: %(value)s'),
params={'value': '42'},
)
# Bad
使用映射键代替位置格式化。这样可以在重写信息时,将变量按任何顺序排列或完全省略:
# Good
ValidationError(
_('Invalid value: %(value)s'),
params={'value': '42'},
)
# Bad
ValidationError(
_('Invalid value: %s'),
params=('42',),
)
把它放在一起:
raise ValidationError(
_('Invalid value: %(value)s'),
code='invalid',
params={'value': '42'},
)
如果你写的是可重用的表单、表单字段和模型字段,那么遵循这些准则是特别必要的。
虽然不建议使用,但如果你处于验证链的末端(即你的表单 clean()
方法),并且你知道你将永远不需要覆盖你的错误信息,你仍然可以选择不那么啰嗦的:
Form.errors.as_data() 和 方法极大地受益于功能齐全的 ValidationError
(带有 code
名称和 params
字典)。
如果在清理方法中检测到多个错误,并希望向表单提交者发出所有错误信号,可以将错误列表传递给 ValidationError
构造函数。
如上所述,建议传递一个带有 code
和 params
的 ValidationError
实例列表,但一个字符串列表也可以:
# Good
raise ValidationError([
ValidationError(_('Error 1'), code='error1'),
ValidationError(_('Error 2'), code='error2'),
])
# Bad
raise ValidationError([
_('Error 1'),
_('Error 2'),
])
在实践中使用验证
前面的章节解释了一般表单的验证是如何工作的。由于有时通过看到每个功能的使用,可以更容易地将事情落实到位,这里有一系列使用前面每个功能的小例子。
验证器可以用来验证字段内部的值,我们来看看 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
是一个 的实例,它的第一个参数是模式:^[-a-zA-Z0-9_]+$`
。参见 编写验证器 一节,查看已有验证器的列表,以及如何编写验证器的例子。
首先让我们创建一个自定义表单字段,验证其输入是包含逗号分隔的电子邮件地址的字符串。完整的类是这样的:
每个使用该字段的表单在对字段的数据进行任何操作之前,都会运行这些方法。这是专门针对这种类型的字段进行的清理,不管它随后如何使用。
让我们创建一个 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()
方法时, 方法将作为清理过程的一部分被运行,它将反过来调用自定义的 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 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()
方法被调用时,所有的单独字段清理方法都会被运行(前面两节),所以 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")
add_error()
的第二个参数可以是一个字符串,或者最好是 ValidationError
的一个实例。更多细节请参见 引发 ValidationError。请注意, 会自动从 cleaned_data
中删除该字段。