1. 程式人生 > >Django學習之八:forms元件【對form舒心了】

Django學習之八:forms元件【對form舒心了】

目錄

Django forms元件

    Handle (掌控)一個form是非常複雜的工程,需要做很多功能:不同的型別的資料要有不同的渲染;校驗資料;獲取檢驗後的乾淨資料,並將資料反序列化為相應資料型別如時間物件;儲存傳遞給處理程式等等。Django的forms元件就完成了這些複雜的工作,提供方便的操作form的介面API給我們。

    Form物件有很多的API,參考本文API段落。其實每個API都是對應了Form的一個特點。如:form.auto_id 對應就是設定form中表單標籤的id屬性;form.errors 對應了form的校驗和錯誤資訊。等等。官方文件在講解Form物件API時,也是按照form的功能和特點,來分類介紹每種API的。

bound and unbound form instance

繫結資料的form例項和沒繫結資料的form例項,他們之間的區別是非常重要的,這影響到了,同一個api或者屬性,在templates 引擎渲染和weight作用時所表現出來的內容是不同的。
一個form instance 要麼繫結要麼沒繫結

  • 綁定了資料的: 可以呼叫is_valid等校驗api;並且通過該例項可以渲染出html。並且包括inline error messages 校驗失敗的錯誤可以渲染到表單後,已提示表單提交使用者。
  • 沒繫結資料的:不能校驗(因為沒繫結資料),but it can still render the blank form as html但是它還是能渲染空白內容的表單空間。沒有繫結的form是沒有cleaned_data屬性的。訪問的話會丟擲異常。
  • 什麼是繫結資料行為?通過form類例項化form物件時,需要提供一個字典型別(對映型別)的資料作為第一個位置引數,如{'name':'ZJQ', 'age': 300} 或 request.POST等。這樣初始化的form物件就是綁定了資料的form例項,即使提供一個空的{} 也算是提供了。 沒有提供這樣一個引數,則例項化出來的是一個沒有繫結資料的form例項。
  • 繫結資料的form物件或者沒有繫結資料的form物件,可以改變其繫結資料值或者新增繫結資料嗎?答案是:NO!. 一旦一個Form 例項物件建立了,要知道它的資料是immutable不可改變的,無論是繫結還是非繫結資料from物件。

forms渲染有關

注意:form物件迭代出來的資料型別。form物件是可迭代的物件,迭代出的是boundfield物件。form物件又是字典型別物件,key是欄位名,value是boundfield物件。所以要獲取boundfield物件有兩種途徑,通過for迭代,或者通過字典key訪問。以下的field名字沒特殊說明,都是boundfield物件。至於獲取boundfield物件,剛剛也提到了。下面就來使用它的屬性和方法吧:

  1. field.label 是label值,不包括label標籤
  2. field.label_tag() 就是一個返回label標籤的方法,包含了label值;在渲染標籤是指定引數attrs={'class':'foo'} 就能指定標籤css class,還可以指定label_suffix=‘::’ 來設定新增label值的字尾.
  3. form相當於整個表單,列印form物件就是一個HTML字串。
  4. field 列印就是一個表單控制元件的HTML字串。
  5. form是可以迭代的,迭代出就是boundfield物件。迭代順序就是form定義的field的順序。如果要訪問某個具體的定義form時的field物件(非boundfield物件)通過form.fields['欄位名'] 可以得到。通過boundfield.field也可以拿到對應的欄位物件。(區分form定義時的欄位物件,和例項化後的boundfield物件)
  6. 關於檢驗失敗的錯誤資訊: 通過field.errors拿到。這個拿到的是一個錯誤集合(或者說錯誤列表),通過訪問改錯誤列表才能拿到錯誤。其它拿錯誤的方式也是一樣的。
  7. forms物件在template中的渲染是不會有<\form>標籤的。因為form不止可以渲染成表單,還可以渲染成table({{ form.as_table }});如下:
There are other output options though for the <label>/<input> pairs:

{{ form.as_table }} will render them as table cells wrapped in <tr> tags
{{ form.as_p }} will render them wrapped in <p> tags
{{ form.as_ul }} will render them wrapped in <li> tags
相應的,都必須自己提供table或這ul
  1. field.id_for_label 這個是獲取label應該設定的對應input的id。
  2. field.errors 列印的話會渲染表單錯誤為一個無序列表,列表的ul會有一個class='errorlist' ,這個需要使用者來定義這個 css class 應該這樣顯示。 由於這個其實是一個錯誤列表,所以迴圈來自己渲染錯誤,通過迴圈迭代,拿到具體的錯誤字串。
  3. form.non_field_errors()表示表單校驗時的非field錯誤,即全域性鉤子錯誤或自己新增的錯誤。
  4. field.value() 就可以拿到表單具體的value所對應的值或非繫結設定的初始化值。
  5. 在python程式碼中列印form物件都是由<\tr><\th>包裹的,而template中使用是沒有這些標籤包裹的。
  6. form.errors 是一個字典(區別對比field.errors),包含所有欄位的錯誤,key就是欄位名,對應的value是一個錯誤列表。特別注意一個全域性鉤子的錯誤放在一個key叫做'__all__'中。注意獲取form.errors就會觸發form的校驗,類似is_valid() 觸發一樣。同時校驗過程也只會發生過一次,對於一個form物件。form.errors有很多的介面,可以獲取為json字串form.errors.as_json();參考:https://docs.djangoproject.com/en/2.0/ref/forms/api/#django.forms.Form.errors
  7. 更多模版中渲染有關可以參考:https://docs.djangoproject.com/en/2.0/topics/forms/#looping-over-the-form-s-fields
  8. form 渲染時的注意事項:

隱藏一個欄位,不渲染它

pass

form 校驗

所謂校驗,就是繫結到form物件的資料,校驗其是否符合定義的約束條件。
關於校驗方面,要明白的點:

  1. 對於綁定了資料的form物件,可以進行校驗其繫結的資料是否匹配form定義的欄位的型別和其它約束。form_obj.is_valid() 和 form_obj.errors 的呼叫執行 都會隱式觸發form_obj的校驗過程;而想顯示的呼叫校驗可以通過from_obj.full_clean()。要明白一個form的校驗過程只會進行一次,校驗過了就不會在校驗了,直接返回結果。所以form_obj.is_valid() 和 form_obj.errors 誰先呼叫,誰觸發有且僅有一次的校驗過程。如果有單個欄位物件,可以測試呼叫單個欄位物件的clean(校驗資料) 方式,引數就是要校驗的資料。
  2. 校驗資料完後,結果無非兩種情況:a) 校驗通過,django對校驗過了的form_obj,會將通過了的資料放入form_obj.cleaned_data 字典中。b) 校驗不通過, django對這沒通過的form_obj 也會將部分校驗通過的放入from_obj.cleaned_data 。對於校驗失敗了的欄位,會將錯誤存放到form_obj.errors 字典中,字典{'欄位名1':['錯誤資訊1','錯誤資訊2']}。
  3. 問題:綁定了資料了的欄位,會校驗哪些方面?第一,定義欄位時的一些約束;第二,區域性鉤子;第三,全域性鉤子;所以錯誤資訊的字典,主鍵key是欄位,錯誤資訊是一個列表。而全域性不是單個欄位的,所以Key是不是一個欄位名而是‘__all__’ 作為字典key。
  4. 沒校驗通過的form_obj可以用於渲染,將錯誤資訊和驗證過的資訊都渲染到form表單中,不會出現form表單沒有校驗通過,就將部分校驗通過的資料也清空掉,會保留校驗通過的資料,只清空沒有通過的欄位的資料。而且沒通過的錯誤資訊還用於渲染到表單頁面中,提醒提交表單的使用者錯誤。
  5. 除了form.errors存放全部的校驗錯誤資訊外。每個field物件也有一個errors屬性,裡面存放了欄位物件的錯誤資訊,是以一個list列表存放的。
  6. form校驗的錯誤資訊的返回格式還可以有多種,有form.errros.as_json()得到一個json字串,特別是對於ajax提交的form資料,響應錯誤通過這種方式。
  7. form錯誤是會有一個ul標籤來組織錯誤資訊的。
  8. 可以通過直接例項化一個Field物件,通過呼叫其clean(傳入值) 來校驗資料是否符合。

form類

繼承關係:

  1. from django import forms 匯入模組
  2. 繼承Form類,構造一個自己的表單類。類似於Models類,django通過model操作資料庫表。Form物件這是一個表單物件,通過該物件來操縱表單處理的過程,如校驗表單欄位,渲染表單欄位。主要就對這兩方面進行操縱。
  3. 關於提交的表單資料的校驗,提供了自定義全域性和區域性鉤子,提供了豐富的內建Field類和其對應的widget來約束表單提交的資料。(插曲:所謂鉤子,就是訪問入口規定好了,我們就新增入口裡面的東西就可以了)
  4. 區域性鉤子注意獲取到校驗值,進行校驗後,符合要返回該值,不符合丟擲一個指定的異常 ValidationError 異常
  5. 全域性鉤子主要用於每個空間的值都區域性校驗後,進行一個全域性校驗,比如兩次密碼是否一樣。這樣就不必在從clean_data取出來比較了。如果校驗成功過,注意返回的是clean_data,失敗同樣丟擲ValidationError異常。全域性校驗錯誤資訊是存在form.errrors 的__all__的一個key對應的列表中。
  6. is_valid clean_name errors
  7. 關於渲染表單 form為每個field提供了相對應的一個預設widget。當然也可以自定義,在定義form欄位是,可以帶入引數widget指定widget類或該類的例項物件。如果傳入的是widget類,那麼會自動例項一個預設的widget物件用於欄位渲染。如果傳入的是例項,就按照例項的渲染方式進行渲染。
  8. 表單渲染主要就是field對應的widget的作用。當然內建的多種widget都可以傳入相同的引數來改變渲染效果,如attrs={'class':'form-control'} 就會給相應標籤新增屬性。
  9. 表單渲染新增css class可以通過widget。而<\label> 和 錯誤 通過定義form類是新增類屬性 error_css_class 和 required_css_class 明天實驗這兩個hook鉤子????
  10. 其實還是不要用完整的,就用他們的label值和錯誤值,只用field的渲染就好了。
  11. ValidationError匯入使用from django.coreexceptions import ValidationError
  12. 內建widget都在forms模組中。

form 的例項,可以是空,也可以提前填充資料。歸納總結form例項化資料主要來自三個方面:

  1. 來自model instance
  2. 來自其它資料來源
  3. 來自使用者提交的表單資料。這種情況通常給使用者一個空form後,使用者提交,如果無效,再返回一個綁定了資料的form給使用者。

ModelForm

出現modelform 這種form類的情況是這樣的:
     如果你正在開發基於資料庫的web app, 很有可能, 你會建立一個forms 是幾乎對映到一個django models的。例如,你可能有一個BlogComment model, 然後,你想建立一個form 讓使用者通過這個form提交部落格評論到BlogComment model的表中。在這個例子中,定義form的field types是一種很冗餘的做法(django 哲學之避免冗餘),因為你已經定義了model的field type了,可以複用給form用。

     因為這個原因, django 提供了一個很有幫助的 class 可以讓我們建立一個Form class 通過一個django 的model。這樣就複用了django model 中的field的定義。

程式碼例項:

>>> from django.forms import ModelForm
>>> from myapp.models import Article  # 匯入自己建好的django model  

# 建立form class
>>> class ArticleForm(ModelForm):
...     class Meta:
...         model = Article
...         fields = ['pub_date', 'headline', 'content', 'reporter']

# 建立一個form 用於新增文章
>>> form = ArticleForm()

# 建立一個form 用於改變一個存在的文章
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)  # 

# 上面建立的兩個form都可以用於渲染到模版中,分別用於新增和修改。

小結:注意這裡modelform和普通form例項化的不同。這裡傳入一個model object instance作為將用於初始化顯示的資料。也可以像普通 form一樣,傳入initail引數。如果兩個引數都傳遞了的話,那麼就變成了第三種情況,不過initial會覆蓋instance的初始化。
如果例項化繫結資料時,提供了instance引數,那麼在save時就是一個update操作資料庫。如果只是給了一個類字典的資料沒有instance,那麼就是insert新增資料到資料庫。
所以,在例項化modelform時,instance引數除了會影響save()的行為,還會影響初始化引數initail的效果。

利用ModelForm關鍵就在於model's field mapping to form's field

     每一個model field 有一個與之對應的預設form field。例如, 一個在model中的CharField 被表示為 一個在form中的CharField. 而一個model ManyToManyField 被表示為 一個form的ModelMultipleChoiceField.

預設對應關係如下圖:

正如你所想的,ForeignKey 和 ManyToManyField model field 型別是特殊情況(OneToOne這則不會有這樣的特殊情況):

  • ForeignKey 通過django.forms.ModelChoiceField所表示,這個實際是一個Choice Field,特殊是它的choices 是一個model QuerySet 也就是一個查詢出的queryset結果。對於這種ModelChoiceField。在modelform物件層面和ModelChoiceField層面,進行資料繫結和資料clean()校驗是不同的。modelform層面例項化是要提供一個queryset作為代替choice引數,利用queryset生成choices。

  • ManyToManyField 通過django.forms.ModelMutipleChoiceField 所表示,這個實際是一個MultipleChoiceField,只是它的choices引數是變為了queryset引數,提供一個queryset物件。因為這個queryset物件可以構建出choice。
  • 至於兩種Field在校驗後,會將單個model物件(對於ModelChoiceField) 和 多個model物件(對於ModelMutipleChoiceField) 存入到cleand_data中。供後續使用。
  • 而且這兩類Field物件,就有了queryset屬性,這是一個queryset物件。通過這個queryset的API就可以得到對應關聯的model的資訊了。如:BoundField.field.queryset.model 就是model class了。這個是訪問關聯model的重要途徑了。
  • ModelMutipleChoiceField和ModelChoiceField 對應的choice顯示呼叫的queryset中model物件的__str__() 方法的結果,所以model定義時,約定都是要定義__str()__方法。
  • ModelMutipleChoiceField和ModelChoiceField都有一個可選引數,empty_label 主要用於控制對應select表單的一個空白選項的顯示。預設是'-------------'

此外,每一個通過model方式生成的modelform field 會設定如下屬性:

  1. 如果 model field 有 blank=True, 相應的form將設定required=False。否則,required=True.
  2. 這個繼承自ModelForm的form的field 的label屬性會被設定為model field的verbose_name屬性,並且值將是首字母大寫
  3. 而help_text 屬性值兩者都有,就一一對應了。
  4. 如果 model field 有choices 屬性設定,這是fomr field's widget 將被設定為Select,該form欄位的choices將來自model欄位的choices。這個choices 一般會包括一個blank choice代表的是model的預設值。如果field是required,將強制使用者做出選擇。The blank choice will not be included if the model field has blank=False and an explicit default value(the default value will be initially selected instead).

一個完整的例項定義ModelForm:

# model的 
from django.db import models
from django.forms import ModelForm

TITLE_CHOICES = (
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
)

class Author(models.Model):
    name = models.CharField(max_length=100)
    title = models.CharField(max_length=3, choices=TITLE_CHOICES) # 注意model和form的欄位中都有choices這個屬性,理解不同與相關。
    birth_date = models.DateField(blank=True, null=True)

    def __str__(self):
        return self.name
 
class Book(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']

class BookForm(ModelForm):
    class Meta:
        model = Book
        fields = ['name', 'authors']

ModelForm.save() 詳解

  • modelform由於和model產生了關係,所以多出了api來操作model。比如說form.save()可以儲存或者更新form資料到資料庫中。這裡研究研究。這是modelform非常重要的一點,因為我們得到form正確資料後,是要同步到資料庫中的,我們不可能將資料再一一ORM操作到資料庫中。所以對於form提交的資料提供了這個save() 同步操作到資料庫中。但是要注意對應資料庫的新增修改操作,在save時是有不同邏輯的,你自己想想也是,如果是提交的資料和庫中存量的資料有約束衝突,那必須解決這個衝突;至於新增就簡單了直接插入insert就可以了。
  • save() 建立並儲存一個model object instance (利用繫結到該modelform的資料)。繫結到modelform資料有兩種方式,一種是普通方式,一種是繫結一個相同model類的例項物件(這種方式多用於修改檢視的表單)。對於普通方式,save() 將建立一個new instance of the specificed model 也就是利用提供的資料,例項出一個model object,然後save就會儲存新增加一個。對於綁定了物件的方式,如果提供了資料且提供了instance例項,這是update這個例項物件。對於普通模式在ORM操作時出現了主鍵等衝突,就會save()操作報錯。
  • 通過modelform的save操作對應的model物件,關更新操作在例項化modelform時必須帶上instance引數指明是更新的哪個model 物件資料,不然會程式設計新增,失去了想要修改操作的意圖。如form(request.POST, instance=model_obj) 這樣例項化modelform表單物件才行。
  • 自定義整合成ModelForm的類,其class Meta中的fields是使用哪些對應model的欄位應用到modelform中。如果fields = ‘__all__'就是全部model欄位應用到。
  • save() 接受一個可選的引數commit,引數取值可以是True or False,如果是False,那麼方法這個save方法會返回一個model 物件,而不會同步到資料庫中,這是就只有手動呼叫model物件的save() 方法去同步到資料庫中。這樣就提供了一個方式,可以修改物件,再提交到庫中。還有一種情況,如果有一個manytomany欄位,建立物件,建立關係可能需要先構建關聯表中的資料後,才能儲存。這時候可以呼叫modelform.save_m2m()方法儲存物件並建立關係資料到中間表。對於commit=True,就沒有上面說的兩種情況,就直接同步資料庫中。

小結:modelform初始化時可以使用initial初始化資料嗎?可以的,如果還提供了instance引數用於初始化的話,那麼initial優先於instance引數中的值。
modelform的方法和屬性除了增加save和save_m2m區別之外,其它和普通form物件API一樣。

class Meta !!!重寫覆蓋預設的modelField欄位(即自定義一些modelform屬性)

通過class Meta可以定義覆蓋預設的一些modelField的元素。
大致在Meta中的屬性有:
model = 對映的model class
fields = ['fieldname1', 'fieldname2'...] 全部可以設定為['__all__']
widgets = {'fieldname': widget_obj,...}
labels = {'fieldname':label_value} 設定渲染時的label值
help_text = {'fieldname': help_string}
error_messages = {'fieldname': {'校驗code':錯誤資訊}} 通過這個可以改變錯誤資訊為自定義中文
field_classes = {'fieldname': FieldClass}

如果要完全覆蓋一個欄位,就在modelfrom中建立一個欄位的定義就會完全覆蓋modelform預設生成的。

參考:https://docs.djangoproject.com/en/2.0/topics/forms/modelforms/#overriding-the-default-fields

form有關多選擇Field的使用

form表單中可以使用input-checkbox 和 select-option 及 input-radio 來實現多值或者提供選擇項給使用者展示。
form表單基本上可以對應資料庫中一個表的一條資料。因為資料庫中一個表的資料可能關聯到其它表的資料(就是常說的manyTomany,manyToone,oneToone)。要通過form表單,操控資料庫一條資料,那麼表單就要有展示或者操控資料關係的方式。這種方式就是表單的上面提到的三種表單控制元件了。

再看回django的form元件。主要就是ChoiceField/ModelChoiceField/ModelMultipleChoiceField的使用。
三者的區別:

  • ChoiceField對應引數choice,就是要提供一個choice引數。
  • ModelChoiceField對應的是一個queryset引數。**主要利用的是queryset中model物件的pk和物件的__str__的輸出。**這個很重要,開始在使用時,要提供一個queryset引數。
  • ModelMultipleChoiceField對應的也是queryset引數,類似ModelChoiceField只不其widget是一個MultipleSelectWidget罷了。

上面三種Field對應的表單控制元件預設都是Select,
而對於要使用input-check,就要給欄位重新賦值widget引數為一個Check型別的widget。

form's fields are themselves classes

Field class

Field類例項化物件時,核心引數就五個:

  1. required 是否是必須有資料。用於校驗
  2. label 表單的的貼條。主要用於貼示 資料是什麼資訊。預設是欄位名。
  3. widget 主要是表單的渲染,和部分校驗。
  4. initial 初始化資料。用於初始化預設值。為後續has_changed()提供對比依據。
  5. help_text
    通過form訪問Field物件:form.fields['field名字']

將form校驗錯誤資訊改為中文。

由於錯誤提示校驗是分類的,每種型別欄位有哪幾種校驗錯誤,可以到官網查詢https://docs.djangoproject.com/en/2.1/ref/forms/fields/#built-in-field-classes。
知道要改變哪種型別的錯誤提示後,就在定義field是設定error_messages={'錯誤型別': 錯誤資訊!}

BoundField class

這個BoundField 類 ,主要用於展示HTML 或者 用於訪問form例項的一個Field物件 屬性。

BoundField Used to display HTML or access attributes for a single field of a Form instance.
通過BoundField.field 訪問到Field物件。
這個類的__str__() 就是展示 欄位的HTML。所以列印BoundField物件就輸出了HTML。
通過form訪問BoundField物件,可以遍歷,也可通過字典key操作,因為form是一個類字典的型別。key就是欄位字串啦。

form中的field負責管理表單資料和表單資料的校驗當一個表單被提交後。

FileField /ImageField /DateField

和其它的Field不同,有兩個特別的Field型別:DateField類與FileField(類似於model中的FileField和ImageField欄位比較特別,因為都涉及到檔案物件)

在前端頁面,需要通過form上傳檔案,就需要確定form標籤的enctype定義了正確的值“multipart/form-data” 現代瀏覽器對於有檔案的上傳都會使用這種編碼。
這樣,才能使用正確的格式編碼 form表單中的檔案物件和其它資料 到http body中,然後通過http協議傳輸到服務端,服務端也能正確通過編碼方式進行解碼,才能正確解析出檔案物件和其它資料。

  • DateField https://docs.djangoproject.com/en/2.0/ref/forms/fields/#django.forms.DateField

    在normalized轉化到python 物件時,需要特別注意該欄位。這個欄位會將使用者表單中填入的字串,轉化為date物件。(使用者的鍵入只能是字串形式)。
    這個轉化過程肯定也是要有依據的,得按照依據規則來,不可能使用者隨便輸入什麼字元都能轉換換成date物件是吧。所以這個欄位在初始化時,需要一個可選引數就是input_formats。提供專業的to_python和to_html 的格式。提供了這些格式,使用者輸入的時間字串,就需要按照列表中的格式化提供時間字串。同時,綁定了值的渲染到頁面也是按照其中的格式來的。由於這個時間格式的表示正規化,全球各地是不同的,所以會根據整個django專案的F10N引數,來判定預設的input_formats規則是什麼。如果F10N=True ,那麼input_formats只能用全球同一認可的格式。如下:
    ['%Y-%m-%d', # '2006-10-25'
    '%m/%d/%Y', # '10/25/2006'
    '%m/%d/%y'] # '10/25/06'
    如果F10N=False,那麼就會有更多的格式支援。
    ['%b %d %Y', # 'Oct 25 2006'
    '%b %d, %Y', # 'Oct 25, 2006'
    '%d %b %Y', # '25 Oct 2006'
    '%d %b, %Y', # '25 Oct, 2006'
    '%B %d %Y', # 'October 25 2006'
    '%B %d, %Y', # 'October 25, 2006'
    '%d %B %Y', # '25 October 2006'
    '%d %B, %Y'] # '25 October, 2006'

  • ImageField

    對於該欄位,例項化時除了帶入request.POST外,還需要request.FILES. 也就是要通過form 來handle 上傳的檔案,需要將檔案繫結到form相應的imagefield。

  • FileField https://docs.djangoproject.com/en/2.0/ref/forms/fields/#django.forms.FileField

    對於該欄位,例項化時除了帶入request.POST外,還需要request.FILES. 也就是要通過form 來handle 上傳的檔案,需要將檔案繫結到form相應的filefield。

FileField可選引數max_length限制檔案物件的檔名。allow_empty_file,檔案內容可以為空。

  • 由於FiledField 和ImageField處理類似,這裡就已ImageField的form繫結 上傳檔案為例,來演示例項化這一個form:
# Bound form with an image field
>>> from django.core.files.uploadedfile import SimpleUploadedFile
>>> data = {'subject': 'hello',
...         'message': 'Hi there',
...         'sender': '[email protected]',
...         'cc_myself': True}
>>> file_data = {'mugshot': SimpleUploadedFile('face.jpg', <file data>)}
## 這裡的file_data中的value是一個SimpleuploadedFile物件,物件例項,提供了檔案的名字和檔案的控制代碼作為引數。
>>> f = ContactFormWithMugshot(data, file_data)

而在實戰中,request為我們handle好了像SimpleuploadFile物件,就是request.FILES.

f = ContactFormWithMugshot(request.POST, request.FILES)

問題來了,那如果是一個modelform呢?怎麼例項化這樣一個帶有檔案物件的modelform?答:同普通form一樣,多帶入一個request.FILES。
modelform 是哪個欄位對映到FileField欄位呢?
答:也是form的FileField對應
那例項化提供了SimpleuploadFile物件後,有怎麼通過save()儲存到對應的資料庫表中記錄呢?就算不是modelform,普通的form,是怎麼將上傳的檔案儲存在哪裡呢?
猜測,這些可能就是UploadFile物件封裝了這些繁瑣的事情了吧?後續驗證。

API

特別提醒:注意將form instance api 與 bound field api 對比檢視。
form instance api

  1. form.has_change() 返回True或False。和初始化對比變化。
  2. form.fields是一個字典型別,存放這form定義時的field物件,注意不是boundfield物件(也就不是迭代form物件的產物)。
  3. 列印form 輸出一個HTML
  4. form.label_suffix 設定每個label值後面跟什麼字串,預設是冒號‘:’
  5. form['欄位名'] 得到對應的BoundField物件。
  6. form.fields['欄位名'] 得到對應的Field物件。
  7. form.is_valid() 返回bool值 True校驗通過(包括自定義鉤子);Flase校驗失敗。
  8. form.add_error(field, error) 給引數指定的欄位新增錯誤。如果欄位是校驗過的,新增錯誤會將欄位資料從form.cleaned_data中刪除。
  9. form.errors.as_json() 返回json字串格式的錯誤資訊
  10. form.has_error(field, code=None) 判定欄位是否有指定code的錯誤。
  11. form.non_field_errors() 返回 全域性鉤子校驗失敗的錯誤資訊列表或者通過form.has_error(None,'...) 新增的。記住這個方法,對於容易忘記全域性鉤子錯誤的KEY是什麼的人,是福利。
  12. form.cleaned_data 得到校驗乾淨的資料,資料會格式化為對應的python物件型別。
  13. form.initial 是初始化資料字典。注意初始化是是不會將form變為繫結資料的form的。
  14. form.error_css_class 屬性定義是在form中的類屬性,主要是給每行的<\tr>標籤新增錯誤資訊時的class。
  15. form.required_css_class 屬性定義是在form中的類屬性,主要就是給<\label>標籤新增css class
  16. form.auto_id 有三種值,True,Flase,String。這個主要控制label標籤for屬性 和 input等標籤中的id屬性的。如果是True,值就會是欄位名。如果是False就不會有id屬性。如果是'id_for_%s' 這一類的格式化字串,那麼%s會被欄位名替換,構成一個id值給標籤屬性中用。
  17. form.use_required_attribute 設定為True這預設全部都有required屬性,如果是False預設全部都沒有required屬性。但是對單個欄位定義時的required是沒有影響的。
  18. form.field_order 設定一個列表,加入欄位來定義渲染是欄位的順序。
  19. form.order_fields(field_order) 通過這個api可以隨時改變form中欄位的順序。
  20. form.is_multipart() 返回True是 說明form中有檔案類欄位,False這是普通form.

bound field api

  1. boundfield.has_change()
  2. boundfield.field 而遍歷form時得到的foundfield物件也可以得到form定義時的field物件,就是boundfield.field。
  3. 列印boundfield 輸出一個HTML
  4. boundfield.label_suffix 設定當前label值後面跟什麼字串,預設是冒號‘:’
  5. boundfield.value() 返回當前欄位的值,綁定了資料的,就返回綁定了的值;不然返回初始化時提供的值,再不然就返回一個None。
  6. boundfield.auto_id 設定field自己的id形式。
  7. boundfield.data 類似boundfield.value() 方法,都是返回對應的資料。區別是,只有繫結資料才會有值,其它任何情況都是None.小結就是:value()拿到渲染後能看到的值。data拿到綁定了的值。
  8. boundfield.errors 是一個類列表物件。這就到了__str__ 和__repr__ 的區別了.列印的話會call str方法,輸出html字串。只是值的化就是走repr,打印出來就是一個字典字面值。這個同form.errors是一樣的。
  9. boundfield.form 就是boundfield所在的form物件
  10. boundfield.help_text 就是field定義是的help_text.如果有定義,可以在渲染是放在input後面作為提示幫助資訊。
  11. boundfield.html_name
  12. boundfield.id_for_label 對應欄位的id屬性值。用於自定義<\label>標籤
  13. boundfield.is_hidden 判定是否是隱藏欄位
  14. boundfield.label 欄位的label自,預設是欄位名
  15. boundfield.name field欄位的名字。
  16. boundfield.label_tag(contents=None, attrs=None, label_suffix=None)
  17. boundfield.as_hidden()
  18. boundfield.as_widget()
  19. boundfield.css_classes()

小結:對比後發現,無論是has_changed, as_table ,initail引數等。這些都是form 與 boundfield之間的關係。如form level 的呼叫會去呼叫boundfield的對應的呼叫,has_changed();還是form level的initial初始化字典,影響field的初始化。就連form.as_table / form.as_ul 等,對應都是boundfield的 as_...方式的呼叫。還有,如果整個form容器裡面的boundfield物件都要改變的屬性或者特點,那麼通過form instance api 進行容器空間全域性控制,如果是單個boundfield的改變,就通過物件自己,同時會覆蓋form全域性的設定。

widgets class

每種型別的Field class 都有一個預設對應的Widget class。
為什麼這樣設計?為什麼不Field class 把這個幹完呢?
因為一個Field class可以有多種Widget表現,即Field 可以和Widget任意匹配,這樣能適應的情況就更多了,不然就有超多的細分型別的Field class。同時也可以把一些功能解耦出去。
後面用的多了再總結這一part

rendering form error messages

django官方也一直沒定下怎麼渲染表單驗證錯誤資訊。

關於設定錯誤校驗錯誤資訊為中文:
pass 就是通過error_messages

定義自己form 例項 包括自定義 區域性和全域性 鉤子

from django import forms
from django.forms import widgets
from django.core.exceptions import ValidationError


wid_01 = widgets.TextInput(attrs={"class":"form-control"})
wid_02 = widgets.PasswordInput(attrs={"class":"form-control"})
wid_03 = widgets.EmailInput(attrs={"class":"form-control"})

class RegForms(forms.Form):
    username = forms.CharField(min_length=4, widget=wid_01)
    password = forms.CharField(min_length=4, widget=wid_01)
    r_password = forms.CharField(min_length=4, widget=wid_01)
    email = forms.EmailField(widget=wid_03)
    telephone = forms.CharField(required=False,widget=wid_01)
    school = forms.ChoiceField(choices=[('male', '男'), ('female', '女')])
    age = forms.BooleanField()
    area = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple(attrs={'class': 'checkbox-inline'}), choices=(('china', '中國'), ('america', '美國'), ('english', '英國')))

    def clean_telephone(self):
        val = self.cleaned_data.get('telephone')
        if len(val) == 11 :
            return val
        else:
            raise ValidationError('手機號格式錯誤!')

    def clean(self):
        pwd = self.cleaned_data.get('password')
        r_pwd = self.cleaned_data.get('r_password')

        if pwd and r_pwd:
            if pwd == r_pwd :
                return self.cleaned_data
            else:
                raise ValidationError('兩次密碼不相同!')
        return self.cleaned_data

注意:這裡丟擲錯誤是不規範,也是官方不推薦的,官方推薦拋入ValidationError方式,參考:https://docs.djangoproject.com/en/2.1/ref/forms/validation/#raising-validationerror

reusable form templates

使用include和 include 。。。 with form = comment_form

遺留問題

  1. 檢視form類的元類 DeclarativeFieldsMetaclass
  2. 怎麼給label標籤新增class屬性,或者只有取label值。以解決,第一form物件的required_css_class屬性設定。或者定義form類是新增required_css_class類屬性。

碰到的錯誤

  1. form.is_valid()是一個方法不是一個屬性,千萬不要忘記後加上()呀。因為is_valid()呼叫後才會有clean_data資料(其實獲取form.errors也會產生clean_data資料)。
  2. 有關前端的遺漏只是點,就是form標籤有一個屬性叫 novalidate .加上它之後,前端就不會對required表單進行提示不能為空。新增它的作用,主要是用於方便測試後端form物件對錶單的校驗空值的功能,而不是前端就提示了。

總結

  • 發現form就是容器,存放field物件。form和field看成兩個Level。很多時候form有的方法,field物件也有。如都有has_changed();都可以設定initial引數等。
  • form的校驗資料,不僅僅是校驗,還有清洗資料的作用,比如將提交的字串,轉換成對應field型別的資料物件。如日期字串,通過cleaned_data後,得到的是一個datetime.date的物件。
  • 我覺得,在定義form類時,欄位賦值的是一個如CharField的物件。這個物件有包含了Widget物件。也就是CharField物件主要用於校對和渲染的功能。而form例項化後,form迭代出來的是一個叫bounfield的物件,這個物件應該是繫結資料的一個物件,這個boundfield物件是包裹了CharField物件資料的。通過boundfield物件是不能改變其繫結的資料的,但是渲染是可以改變的,就可以通過改變CharField物件的屬性,來改變最後利用boundfield物件渲染的輸出。如:
You can access the fields of Form instance from its fields attribute:

>>> for row in f.fields.values(): print(row)
...
<django.forms.fields.CharField object at 0x7ffaac632510>
<django.forms.fields.URLField object at 0x7ffaac632f90>
<django.forms.fields.CharField object at 0x7ffaac3aa050>
>>> f.fields['name']
<django.forms.fields.CharField object at 0x7ffaac6324d0>
You can alter the field of Form instance to change the way it is presented in the form:

>>> f.as_table().split('\n')[0]
'<tr><th>Name:</th><td><input name="name" type="text" value="instance" required></td></tr>'
>>> f.fields['name'].label = "Username"  ## 這裡修改了CharField物件,最後使用影響了渲染。雖然影響不了繫結的資料。
>>> f.as_table().split('\n')[0]
'<tr><th>Username:</th><td><input name="name" type="text" value="instance" required></td></tr>'

官方文件也說明了boundfield就是wraps包裹form定義的field的。作用就是展示出

  • 所以說要特別注意在form中,說道的field是class定義時的field,還是例項化後form物件迭代出的boundfield物件。form和boundfield兩個不同level都是可以得到class定義時的field物件的;通過改變class定義時的field物件的屬性,可以影響最後form渲染的效果。
  • 有個心得:Model class 的屬性是Field物件(特別是關聯Field物件)。Model instance 的屬性是值或者Manager管理器;Form class 的屬性是Field物件。Form instance 迭代出來的是BoundField物件。