表單與欄位驗證

表單驗證發生在資料被清理時。如果您想自訂此過程,有許多地方可以進行變更,每個地方都有不同的用途。在表單處理期間會執行三種類型的清理方法。這些方法通常會在您呼叫表單上的 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)

# Good
raise ValidationError(
    [
        ValidationError(_("Error 1"), code="error1"),
        ValidationError(_("Error 2"), code="error2"),
    ]
)

# Bad
raise ValidationError(
    [
        _("Error 1"),
        _("Error 2"),
    ]
)

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


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

slug = forms.SlugField()

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

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)

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    message = forms.CharField()
    sender = forms.EmailField()
    recipients = MultiEmailField()
    cc_myself = forms.BooleanField(required=False)

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

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."
                )

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)