表单和字段验证

表单验证在数据清理时发生。 如果你想定制这个过程,有不同的地方做出改变,每一个都有不同的目的。 在表格处理过程中运行三种清洁方法。 这些通常在您调用表单上的is_valid()方法时执行。 还有一些其他的东西也可以触发清理和验证(直接访问errors属性或调用full_clean()),但通常不需要。

In general, any cleaning method can raise ValidationError if there is a problem with the data it is processing, passing the relevant information to the ValidationError constructor. See below for the best practice in raising ValidationError. 如果没有引发ValidationError,则该方法应该将已清理(规范化)的数据作为Python对象返回。

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

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

  • Field上的to_python()方法是每个验证的第一步。 它将值强制转换为正确的数据类型,如果不可能,则引发ValidationError 此方法接受来自窗口小部件的原始值并返回转换的值。 例如,一个FloatField会将数据转化为Python float或引发ValidationError

  • Field上的validate()方法处理不适合验证器的字段特定验证。 它的值被强制转换为正确的数据类型,并在任何错误时引发ValidationError 这个方法不会返回任何东西,也不应该改变这个值。 你应该覆盖它来处理你不能或不想放入验证器的验证逻辑。

  • Field上的run_validators()方法运行字段的所有验证器,并将所有错误集中到一个ValidationError中。 您不应该需要重写此方法。

  • The clean() method on a Field subclass is responsible for running to_python(), validate(), and run_validators() in the correct order and propagating their errors. If, at any time, any of the methods raise ValidationError, the validation stops and that error is raised. 此方法返回干净的数据,然后将其插入表单的cleaned_data字典中。

  • 在表单子类上调用clean_<fieldname>()方法,其中<fieldname>被替换为表单字段属性的名称。 此方法执行特定于该特定属性的任何清理,与它所在的字段类型无关。 这个方法没有传入任何参数。 您需要在self.cleaned_data中查找字段的值,并记住此时它将是一个Python对象,而不是表单中提交的原始字符串(它将在cleaned_data,因为上面的通用字段clean()方法已经清除了一次数据)。

    For example, if you wanted to validate that the contents of a CharField called serialnumber was unique, clean_serialnumber() would be the right place to do this. 你不需要一个特定的字段(它只是一个CharField),但你需要一个特定于表单字段的验证,并且可能需要清理/标准化数据。

    这个方法的返回值将替换cleaned_data中的现有值,所以它必须是来自cleaned_data的字段值(即使此方法没有改变它)或新的清洁的价值。

  • 表单子类的clean()方法可以执行需要访问多个表单域的验证。 您可以在这里输入“if field A”字段,B字段必须包含有效的电子邮件地址“。 这个方法可以返回一个完全不同的字典,如果它愿意的话,它将被用作cleaned_data

    由于在调用clean()时已经运行了字段验证方法,因此您还可以访问表单的errors属性,该属性包含由个人清理引发的所有错误领域。

    请注意,您的Form.clean()覆盖引发的任何错误都不会与任何字段关联。 它们进入一个特殊的“字段”(称为__all__),如果需要,可以通过non_field_errors()方法访问。 如果要将错误附加到表单中的特定字段,则需要调用add_error()

    还要注意,在覆盖ModelForm子类的clean()方法时有特殊的考虑。 (请参阅ModelForm documentation了解更多信息)

这些方法按照上面给出的顺序运行,每次一个字段。 也就是说,对于表单中的每个字段(按照它们在表单定义中声明的顺序),运行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)

The Form.errors.as_data() and Form.errors.as_json() methods greatly benefit from fully featured ValidationErrors (with a code name and a params dictionary).

引发多个错误

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

如上所述,建议使用code s和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类中定义。

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

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

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

As you can see, SlugField is just a CharField with a customized validator that validates that submitted text obeys to some character rules. 这也可以在字段定义上完成:

slug = forms.SlugField()

相当于:

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

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

表单字段默认清理

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

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" This is validation that is specific to our form, so we don’t want to put it into the general MultiEmailField class. 相反,我们编写一个清理方法,在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 a value to use as the new cleaned data, even if
        # this method didn't change it.
        return data

清理和验证相互依赖的字段

Suppose we add another requirement to our contact form: if the cc_myself field is True, the subject must contain the word "help". 我们一次对多个字段进行验证,因此表单的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().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().clean()的调用确保了父类中的任何验证逻辑都被保留。 If your form inherits another that doesn’t return a cleaned_data dictionary in its clean() method (doing so is optional), then don’t assign cleaned_data to the result of the super() call and use self.cleaned_data instead:

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中删除该字段。