django 表單驗證和字段驗證
表單驗證和字段驗證
表單驗證發生在數據驗證之後。如果你需要自定義這個過程,有幾個不同的地方可以修改,每個地方的目的不一樣。表單處理過程中要運行三種類別的驗證方法。它們通常在你調用表單的is_valid() 方法時執行。還有其它方法可以觸發驗證過程(訪問errors 屬性或直接調用full_clean() ),但是通用情況下不需要。
一般情況下,如果處理的數據有問題,每個類別的驗證方法都會引發ValidationError,並將相關信息傳遞給ValidationError。 參見下文中引發ValidationError 的最佳實踐。如果沒有引發ValidationError,這些方法應該返回驗證後的(規整化的)數據的Python 對象。
大部分應該可以使用validators 完成,它們可以很容易地重用。Validators 是簡單的函數(或可調用對象),它們接收一個參數並對非法的輸入拋出ValidationError。 Validators 在字段的to_python 和validate 方法調用之後運行。
表單的驗證劃分成幾個步驟,它們可以定制或覆蓋:
-
字段的to_python() 方法是驗證的第一步。它將值強制轉換為正確的數據類型,如果不能轉換則引發ValidationError。 這個方法從Widget 接收原始的值並返回轉換後的值。例如,FloatField 將數據轉換為Python 的float 或引發ValidationError
-
字段的validate() 方法處理字段特異性的驗證,這種驗證不適合位於validator 中。它接收一個已經轉換成正確數據類型的值, 並在發現錯誤時引發ValidationError。這個方法不返回任何東西且不應該改變任何值。當你遇到不可以或不想放在validator 中的驗證邏輯時,應該覆蓋它來處理驗證。
-
字段的run_validators() 方法運行字段的所有Validator,並將所有的錯誤信息聚合成一個單一的ValidationError。你應該不需要覆蓋這個方法。
-
Field子類的clean() 方法。它負責以正確的順序運行to_python、validate 和 run_validators
-
表單子類中的clean_<fieldname>() 方法 —— <fieldname> 通過表單中的字段名稱替換。這個方法完成於特定屬性相關的驗證,這個驗證與字段的類型無關。這個方法沒有任何傳入的參數。你需要查找self.cleaned_data 中該字段的值,記住此時它已經是一個Python 對象而不是表單中提交的原始字符串(它位於cleaned_data 中是因為字段的clean() 方法已經驗證過一次數據)。
例如,如果你想驗證名為serialnumber 的CharField 的內容是否唯一, clean_serialnumber() 將是實現這個功能的理想之處。你需要的不是一個特別的字段(它只是一個CharField),而是一個特定於表單字段特定驗證,並規整化數據。
這個方法返回從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,那麽將不會調用該字段對應的clean_<fieldname>()方法。 但是,剩余的字段的驗證方法仍然會執行。
拋出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)New in Django 1.7.
Form.errors.as_data() 和Form.errors.as_json() 方法很大程度上受益於ValidationError(利用code 名和params 字典)。
拋出多個錯誤
如果在一個驗證方法中檢查到多個錯誤並且希望將它們都反饋給表單的提交者,可以傳遞一個錯誤的列表給ValidationError 構造函數。
和上面一樣,建議傳遞的列表中的ValidationError 實例都帶有 code 和params,但是傳遞一個字符串列表也可以工作:
# Good raise ValidationError([ ValidationError(_(‘Error 1‘), code=‘error1‘), ValidationError(_(‘Error 2‘), code=‘error2‘), ]) # Bad raise ValidationError([ _(‘Error 1‘), _(‘Error 2‘), ])
實踐驗證
前面幾節解釋在一般情況下表單的驗證是如何工作的。因為有時直接看功能在實際中的應用會更容易掌握,下面是一些列小例子,它們用到前面的每個功能。
使用Validator
Django 的表單(以及模型)字段支持使用簡單的函數和類用於驗證,它們叫做Validator。Validator 是可調用對象或函數,它接收一個值,如果該值合法則什麽也不返回,否則拋出ValidationError。它們可以通過字段的validators 參數傳遞給字段的構造函數,或者定義在Field 類的default_validators 屬性中。
簡單的Validator 可以用於在字段內部驗證值,讓我們看下Django 的SlugField:
from django.forms import CharField from django.core import validators class SlugField(CharField): default_validators = [validators.validate_slug]
正如你所看到的,SlugField 只是一個帶有自定義Validator 的CharField,它們驗證提交的文本符合某些字符規則。這也可以在字段定義時實現,所以:
slug = forms.SlugField()
等同於:
slug = forms.CharField(validators=[validators.validate_slug])
常見的情形,例如驗證郵件地址和正則表達式,可以使用Django 中已經存在的Validator 類處理。例如,validators.validate_slug 是RegexValidator 的一個實例,它構造時的第一個參數為:^[-a-zA-Z0-9_]+$。編寫Validator 一節可以查到已經存在的Validator 以及如何編寫Validator 的一個示例。
表單字段的默認驗證
讓我們首先創建一個自定義的表單字段,它驗證其輸入是一個由逗號分隔的郵件地址組成的字符串。完整的類像這樣:
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 字段始終包含"[email protected]"。這是特定於我們這個表單的驗證,所以我們不打算將它放在通用的MultiEmailField 類中。我們將編寫一個運行在recipients 字段上的驗證方法,像這樣:
from django import forms class ContactForm(forms.Form): # Everything as before. ... def clean_recipients(self): data = self.cleaned_data[‘recipients‘] if "[email protected]" 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() 方法的時候,所有字段的驗證方法已經執行完(前兩節),所以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.")Changed in Django 1.7:
在以前版本的Django 中,要求form.clean() 返回cleaned_data 的一個字典。現在,這個方法仍然可以返回將要用到的數據的字典,但是不再是強制的。
在這段代碼中,如果拋出驗證錯誤,表單將在表單的頂部顯示(通常是)描述該問題的一個錯誤信息。
註意,示例代碼中super(ContactForm, self).clean() 的調用時為了保證維持父類中的驗證邏輯。
第二種方法涉及將錯誤消息關聯到某個字段。在這種情況下,讓我們在表單的顯示中分別關聯一個錯誤信息到“subject” 和“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 中刪除相應的字段。
django 表單驗證和字段驗證