目前為止你所有的Django應(yīng)用——除了weblog的評論系統(tǒng)——全部集中在站點(diǎn)的可信任用戶通過Django的管理界面輸入內(nèi)容,而沒有允許普通用戶提交內(nèi)容的機(jī)制。因此,在這個應(yīng)用中你將需要一個方式允許用戶提交他們自己的代碼片段。你還需要確保他們提交的內(nèi)容是符合你在模型中設(shè)定的數(shù)據(jù)格式的。
幸運(yùn)的是,Django通過使用一種簡單而強(qiáng)大的顯示和處理基于web表單的系統(tǒng)使這項(xiàng)任務(wù)變得異常簡單。在本章中,你將徹底的了解Django表單處理系統(tǒng)并使用它來創(chuàng)建一個可使用戶提交和編輯它們代碼的樣例。
Django表單處理代碼,位于django.forms模塊中,提供了三個關(guān)鍵的組件,涵蓋了構(gòu)建,顯示,處理表單的各個方面:
為了來感受下它是如何工作的,我們來看一個簡單但是常用的需求:用戶注冊。
#Admonition#這些代碼到哪去了?
這段特定的代碼邏輯上是不屬于你正在開發(fā)的cab應(yīng)用的,如果你已經(jīng)開發(fā)了處理用戶注冊的代碼,那么最好是將這些代碼放置在獨(dú)立的應(yīng)用中。不過目前不用擔(dān)心如何在python文件中保存這些代碼的問題。這只是一個用于盡可能多的展示django表單處理系統(tǒng)的有用的例子。如果你確實(shí)有需要實(shí)現(xiàn)一個用戶注冊系統(tǒng),當(dāng)然,你可以自由的引用這段代碼并調(diào)整到合適于你需要的樣子。
基本的注冊表單由三部分?jǐn)?shù)據(jù)組成:
另外,你還要做一點(diǎn)自定義的校驗(yàn)工作:
從邏輯上說,這會產(chǎn)生一個有四個表單域的<element>元素:用戶名和e-mail各一個,另外兩個處理重復(fù)密碼。下面就是構(gòu)建這個表單開始時的樣子:
1 from django import forms2 3 class SignupForm(forms.Form):4 username = forms.CharField(max_length = 30)5 email = forms.EmailField()6 password1 =forms.CharField(max_length = 30)7 password2 = forms.CharField(max_length = 30)
除了使用django.froms代替django.db.models,以上代碼看起來和定義數(shù)據(jù)模型時十分類似:簡單的子類化適當(dāng)?shù)幕惒⒓尤牒线m的字段。
但是這些代碼并不完善。HTML提供了一個特殊的表單輸入類型來處理密碼——<input type="password">——這是生成密碼域的適當(dāng)?shù)姆绞健D憧梢暂p微改寫這兩個字段來實(shí)現(xiàn)這個輸入類型:
password1 = forms.CharField(max_length=30,widget=forms.PasswordInput()) password2 = forms.CharField(max_lengt=30,widget = forms.PasswordInput())
PasswordInput組件將會產(chǎn)生<input type="password">。這同樣也顯示出Django表單系統(tǒng)有分離的數(shù)據(jù)校驗(yàn)機(jī)制,一個是在字段中處理的,一個是在組件中處理的。最常見的情況是你有一個單獨(dú)的基本校驗(yàn)規(guī)則需要使用與多個不同的字段,并且是來源于不同類型的HTML輸入。這種分離校驗(yàn)機(jī)制就使事情變簡單了:你可以重用一個字段類型而只需要修改包含的組件。
現(xiàn)在,再做一些修改:
password1 = forms.CharField(max_length=30,widget =forms.PasswordInput(render_value=False))password2 = forms.CharField(max_lengt=30,widget = forms.PasswordInput(render_value=False))
參數(shù)render_value告訴PasswordInput盡管有一些數(shù)據(jù),但是不會顯示出來。當(dāng)輸入密碼時產(chǎn)生了錯誤原先的數(shù)據(jù)會被完全清除使下次用戶能正確輸入。
到目前為止你所設(shè)置的字段都包含有隱式的校驗(yàn)規(guī)則。username字段及兩個密碼字段都有最大長度設(shè)置,Email字段會確認(rèn)輸入的是正常的e-mail格式的地址(通過正則表達(dá)式)。但還是需要保證用戶名不是已經(jīng)存在的,因此需要自定義校驗(yàn)規(guī)則。
你可以在form中定義一個名為你call_username()的方法。在校驗(yàn)過程中,Django表單系統(tǒng)會自動查找任何以call_開始,以字段名結(jié)束的方法,然后在字段內(nèi)建的校驗(yàn)規(guī)則完成后調(diào)用。
下面是clean_username()方法看起來的樣子(假設(shè)Django User模型已經(jīng)被導(dǎo)入了):
User.objects.get(username=self.cleaned_date['username'])except User.DoesNotExist:return self.cleaned_data['username']rasise forms.ValidationError("Thisusernameis already in use.Please choose another.")
這段代碼短小精悍。首先,這個方法只會在username字段少于內(nèi)建規(guī)則的30個字符時才會被調(diào)用。在這種情況下,username字段提交的值在self.cleaned_data['username']中。這個cleaned_data屬性是一個所有被提交數(shù)據(jù)中那些已經(jīng)通過校驗(yàn)后的數(shù)據(jù)值組成的字典。
查詢是否存在一個用戶與username字段提交的值精確匹配。如果沒有這樣一個用戶則Django會拋出一個UserNotExist異常。這個異常告訴你這個用戶不存在,因此這個username字段的值即通過了校驗(yàn)。在這種情況下,只簡單返回這個值。
校驗(yàn)密碼是一個需要點(diǎn)技巧的,因?yàn)樾枰瑫r查看兩個字段并確保兩項(xiàng)匹配。你可以為一個字段定義一個方法,如下:
def clean_password2(self): if self.cleaned_data['password1'] != self.cleaned_data['password2']: raise forms.ValidationError("You must type the same password each time") return self.cleaned_data['password2']
但是還有一個更好的方式來實(shí)現(xiàn)這個校驗(yàn)。Django允許你定義一個校驗(yàn)方法——簡單的名為clean()——可以應(yīng)用于整個表單。下面是寫法:
def clean(self): if 'password1' in self.clean_data and 'password2' in self.cleaned_data: if self.clean_data['password1'] != self.cleaned_data['password2']: raise forms.ValidationError("You must type the same password each time") return self.cleaned_data
注意在這種情況下,你手動的檢查了兩個password字段的值是否存在于clean_data中。如果有任何錯誤在單獨(dú)的字段校驗(yàn)中被拋出,cleand_data就會變成空的。因此你需要在引用任何你需要的值之前檢查它。
#Admonition# 默認(rèn)的表單字段都是必須的
所有內(nèi)建于Django表單系統(tǒng)的字段類型都是必須的,因此不能被留空。如果密碼字段中的一個被留空,Django會在調(diào)用clean()方法前產(chǎn)生一個ValidationError的異常,因此你不需要為必需的值生成額外的錯誤。為了將一個字段標(biāo)記為可選,可以向其傳遞一個關(guān)鍵字參數(shù)required=False。
現(xiàn)在,停下寫form代碼的手轉(zhuǎn)到處理表單的視圖上來。你可以創(chuàng)建視圖使得可以創(chuàng)建和保存新的用戶對象。但是如果一旦你需要在別的視圖中重用這個form,就需要一遍遍的重寫這樣的代碼。所以一個更好的辦法是在form中編寫一個方法如何處理校驗(yàn)后的數(shù)據(jù)。因?yàn)檫@個方法是保存一個新的User對象到數(shù)據(jù)庫中,所以我們稱其為save().
#Admonition# save()方法不只是針對數(shù)據(jù)庫的
大多數(shù)時候,表單用于創(chuàng)建和更新模型對象,在這些情況下save()方法是最自然的選擇。但是表單可能被用于其他目的(例如,一個聯(lián)系表單可能會發(fā)送一封電子郵件而不是保存一個對象)。在Django社區(qū)中一般的約定是:任何時候表單類都有一個“知道”該如何處理處理校驗(yàn)后數(shù)據(jù)的方法,這個方法應(yīng)該被叫做save(),即便它有時候可能不保存任何數(shù)據(jù)到你的數(shù)據(jù)庫中去。給這類方法一個固定和易識別的名字的好處遠(yuǎn)大于可能導(dǎo)致的初始化混淆。
在save()中,你需要從form提交的用戶名,e-mail和密碼中生成一個User對象。假設(shè)你已經(jīng)導(dǎo)入了User模型,下面可以這樣編寫:
def save(self): new_user = User.objects.create_user(username=slef.cleaned_data['username'], email=self.cleaned_data['email'],password=self.cleaned_data['password1']) return new_user
#Admoniton# 用戶名及密碼
儲存在數(shù)據(jù)庫中的用戶名和密碼有一個很大的問題,即任何能訪問數(shù)據(jù)庫的人都可能看到全部密碼。由于很多人都喜歡在不同的站點(diǎn)上使用相同的密碼,這就會導(dǎo)致恨嚴(yán)重的安全風(fēng)險。為了幫助保護(hù)你的用戶,Django阻止在數(shù)據(jù)庫中儲存用戶實(shí)際用于登錄的純文本的密碼。Django使用一套稱為哈希函數(shù)(hash function)的數(shù)學(xué)技巧,將密碼轉(zhuǎn)換為一組隨機(jī)的(實(shí)際上不是正真隨機(jī)的)字符串和數(shù)字的組合。儲存在數(shù)據(jù)庫中的其實(shí)是這組隨機(jī)結(jié)果。這樣做的好處是,哈希函數(shù)只能單向工作:如果知道了密碼可以使用哈希函數(shù)并總是得到同樣的結(jié)果,但是如果只是知道結(jié)果,就不能反向的得到實(shí)際密碼。這就提供了一種合理的儲存密碼的安全方法。當(dāng)你嘗試登錄的時候,Django的認(rèn)證系統(tǒng)對輸入的密碼運(yùn)用哈希函數(shù)并比較其結(jié)果與數(shù)據(jù)庫中儲存的結(jié)果。這就表示純文本的密碼不必永久的保存在任何地方。但是由于這個系統(tǒng)的工作需要一定的技巧,Django的User模塊有一個自定的管理器定義了你在這里使用的create_user()方法。這個方法負(fù)責(zé)處理對密碼使用哈希函數(shù)并儲存正確的結(jié)果。
下面是最終完成后的form:
1 from django.contrib.auth.models import User 2 from django import forms 3 4 class SignupForm(forms.Form): 5 username = forms.CharField(max_length=30) 6 email = forms.EmailField() 7 password1 = forms.CharField(max_length=30,widget = forms.PasswordInput(render_value=False)) 8 password2 = forms.CharField(max_length=30,widget = forms.PasswordInput(render_value=False)) 9 10 def clean_username(self):11 try:12 User.objects.get(username=self.cleaned_data['username'])13 except User.DoesNotExist:14 return self.cleaned__data['username']15 raise forms.ValidationError("This username is already in use. Please choose another.")16 17 def clean(self):18 if 'password1' in self.clean_data and 'password2' in self.cleaned_data:19 if self.clean_data['password1'] != self.cleaned_data['password2']:20 raise forms.ValidationError("You must type the same password each time")21 return self.cleaned_data22 23 def save(self):24 new_user = User.objects.create_user(username =slef.cleaned_data['username'],25 email=self.cleaned_data['email'],26 password=self.cleaned_data['password1'])27 return new_user
在視圖中用來確定是否有數(shù)據(jù)通過校驗(yàn)的方法名為is_valid(),被定義與Form基類,所有Django表單都源于該基類。在Form類中,is_valid( ) touches off表單的驗(yàn)證路徑,以一個特定順序,通過一個叫做full_clean( )的方法(django.form中的Form基類中定義的其他方法見圖9-1)。
校驗(yàn) 的順序如下:
理解這個過程是充分發(fā)揮Django表單處理系統(tǒng)的作用的關(guān)鍵。可能初看起來有些復(fù)雜,但是在不同地方為表單附加校驗(yàn)規(guī)則的能力產(chǎn)生了巨大的靈活性而且能更容易的寫出可重用的代碼。舉個例子,如果你需要不斷的使用一個特定類型的校驗(yàn)多次,你會注意到給每一個表單都寫自定義方法十分繁瑣。最好是寫一個自定義字段類,定義一個clean()方法,然后重用這個字段。
類似的,區(qū)分開特定字段校驗(yàn)方法和“表單級”clean( )方法顯示出大量驗(yàn)證多個字段組合的技巧。當(dāng)使用單獨(dú)一個字段時不一定需要這些技巧。
現(xiàn)在,來看看用于顯示和處理表單的視圖:
from django.http import HttpResponseRedirect from django.shortcuts import render_to_response def signup(request): if request.method =='POST': form = SignupForm(data = request.POST) if form.is_valid(): new_user = form.save() return HttpResponseRedirect("/account/login/") else: form = SignupForm() return render_to_response('signup.html',{'form':form})
我們一步步的分解來看:
最后,來看看使用這個視圖如何在signup.html模板中顯示這個表單:
1 <html> 2 <head> 3 <title>Sign up for an account</title> 4 </head> 5 <body> 6 <h1>Sign up for an account</h1> 7 <p>Use the form below to register for your new account; all 8 fields are required.</p> 9 <form method="post" action="">10 {% if form.non_field_errors %}11 <p><span class="error">12 {{ form.non_field_errors|join:", " }}13 </span></p>14 {% endif %}15 <p>{% if form.username.errors %}16 <span class="error">{{ form.username.errors|join:", " }}</span>17 {% endif %}</p>18 <p><label for="id_username">Username:</label>19 {{ form.username }}</p>20 <p>{% if form.email.errors %}21 <span class="error">22 {{ form.email.errors|join:", " }}23 </span>24 {% endif %}</p>25 <p><label for="id_name">Your e-mail address:</label>26 {{ form.email }}</p>27 <p>{% if form.password1.errors %}28 <span class="error">29 {{ form.passsword1.errors|join:", " }}30 </span>31 {% endif %}</p>32 <p><label for="id_password1">Password:</label>33 {{ form.password1 }}</p>34 <p>{% if form.password2.errors %}35 <span class="error">36 {{ form.passsword2.errors|join:", " }}37 </span>38 {% endif %}</p>39 <p><label for="id_password2">Password (again, to catch40 typos): </label>41 {{ form.password2 }}</p>42 <p><input type="submit" value="Submit"></p>43 </form>44 </body>45 </html>
這里的HTML語句大多都很簡單:一個標(biāo)準(zhǔn)的<form>標(biāo)簽,每個字段帶有一個<label>標(biāo)簽以及一個提交按鈕。但是注意最終是如何實(shí)際顯示字段的。每一個都作為{{ form }}變量的屬性被訪問。你可以檢查每一個是否有錯誤,并且顯示錯誤信息(將會是一種列表形式,即使只有一條信息,因此使用join過濾器,用一個特定字符將列表中的項(xiàng)組合起來。)
注意,雖然在表單的開始使用了{(lán){ form.non_field_errors }}。這是因?yàn)閏lean()方法拋出的異常不屬于任何一個字段(因?yàn)槭莵碜杂诒容^兩個字段)。無論什么時候有一個潛在的校驗(yàn)錯誤來自clean( )方法,你需要檢查non_field_errors并在存在的時候顯示它。
現(xiàn)在,用戶注冊的例子已經(jīng)給你關(guān)于如何編寫一個接受提交數(shù)據(jù)信息的表單的很好的啟發(fā),你可以編寫一個來添加Snippte模型實(shí)例。你可以簡單的設(shè)置想要用戶填寫的字段信息,然后給它一個save()方法,用于創(chuàng)建和保存新的代碼段。
但是現(xiàn)在這里有一個新的需要處理的問題。Snippet模型中的author字段必須填寫,并且是被正確的填寫,但是你不希望將其顯示給用戶并讓他們來選擇這個值。如果你這樣做了,那么任何人都可以通過填寫一個其他人的名字來假裝成是別的用戶。所以你需要某種方法來填寫這個字段而又不使它成為表單中公開的部分。
幸運(yùn)的是,這很容易辦到:一個表單就是一個Python類。因此你可以添加你自己的__init__()方法并信任處理這個表單的視圖函數(shù)將會傳入確定的正確的、經(jīng)過認(rèn)證的用戶,你可以儲存并在需要保存snippet的時候引用。那么,我們來開始編寫AddSnippetForm。
在cab目錄下創(chuàng)建一個forms.py文件。在其中開始編寫如下代碼:
from django import formsfrom cab.models import Snippet class AddSnippetForm(forms.Form): def __init__(self,author,*args,**kwargs): super(AddSnippteForm,self).__init__(*args,**kwargs): self.author = author
除了接受一個額外的參數(shù)——author以便之后使用——在這里還做了兩件重要的事:
使用這一技術(shù)——接受*args和**kwargs并傳遞給父類方法——是使復(fù)寫的方法接收大量參數(shù),特別是大量的可選參數(shù)時的一種有用的快捷方法。Form基類的__init__()方法實(shí)際上接收了七個全部是可選的參數(shù),這是一個方便的小技巧。
現(xiàn)在可以添加所需要的字段:
title = forms.CharField(max_length=255) description = forms.CharField(widget=forms.Textarea) code = forms.CharField(widget=forms.Textarea) tags = forms.CharField(max_length=255)
再次提醒,你可以借助修改字段所使用的組建來改變它的表現(xiàn)形式。Django的模型系統(tǒng)使用了兩個不同的字段——CharField和TextField——來表現(xiàn)不同大小的基于文本的字段(必須如此,因?yàn)樗鼈冊诘讓訑?shù)據(jù)庫列上是工作于不同的數(shù)據(jù)類型的),表單系統(tǒng)則只有一個CharField。為了在最終的HTML文件中將其轉(zhuǎn)換為<textarea>,你需要簡單的將其組建修改為Textarea,以同樣的方式,你在用戶注冊表單的例子中使用了PasswordInput組建。
這就解決了除了語言之外的其他字段,這個需要突然看起來需要那么一點(diǎn)點(diǎn)技巧。你像要做的是顯示一個下拉列表(一個HTML元素<select>)其中含有可用語言及校驗(yàn)規(guī)則,使用戶可以從中選擇。但是到目前為止你見過的字段類型沒有適合處理這個問題的,所以需要求助于一些新的東西。
一個可以處理這個問題的字段類型叫做ChoiceField。它需要一個選項(xiàng)列表(和模型字段中接受的選項(xiàng)是同樣的格式——例如,你在weblog的Entry模型中的status字段已經(jīng)見過)并確保提交的值是其中之一。但是將其設(shè)置妥當(dāng)以使每次使用時表單查詢語言集合(以防管理員像系統(tǒng)中添加了新的語言)這就需要在__init__()方法中做些hacking。像這樣表現(xiàn)一個模型中的關(guān)聯(lián)是一項(xiàng)極為常見的情況,因此你會希望有一種簡單的方法來處理它。
最終證明,Django還是提供了一個簡單的解決:一個叫做ModelChoiceField的特殊字段。一個普通的ChoiceField字段只需要一個簡單的選項(xiàng)列表,一個ModelChoiceField字段需要一個Django QuerySet并從查詢的結(jié)果(每次都執(zhí)行最新的)中動態(tài)的生成它的選項(xiàng)。為了使用它,需要在文件頭部導(dǎo)入模型的語句中加上Language 模型:
from cab.models import Snippet,Laabguage
然后簡單的編寫:
language = ModelChoiceField(queryset = Language.objetcs.all())
對這個表單來說,你不需要在字段本身提供的校驗(yàn)之外的任何特殊校驗(yàn),所以你可以直接編寫save()方法:
def save(self): snippet = Snippet(title = self.cleaned_data['title'], description = self.cleaned_data['description'], code = self.cleand_data['code'], tags = self.cleand_data['tags'], author = self.author, language = self.cleand_data['language']) snippet.save() return snippet
因?yàn)樵贒jango中在一個步驟中創(chuàng)建并保存一個對象是一種常規(guī)做法,實(shí)際上你可以用這種更為簡便的做法。Django提供的默認(rèn)管理器類包含了一個叫做create()的方法,可以創(chuàng)建,保存并返回一個新的對象。使用它,你的save()方法就簡化到了只需兩行:
1 def save(self):2 return Snippets.objects.create(title=self.cleaned_data['title'],description=self.cleaned_data['description'],code=self.cleaned_data['code'],tags=self.cleaned_data['tags'],author=self.author,language=self.cleaned_data['language']
現(xiàn)在,你的AddSnippetForm就完成了:
1 from django import forms 2 from cab.models import Snippet, Language 3 class AddSnippetForm(forms.Form): 4 def __init__(self, author, *args, **kwargs): 5 super(AddSnippetForm, self).__init__(*args, **kwargs): 6 self.author = author 7 title = forms.CharField(max_length=255) 8 description = forms.CharField(widget=forms.Textarea()) 9 code = forms.CharField(widget=forms.Textarea())10 tags = forms.CharField(max_length=255)11 language = forms.ModelChoiceField(queryset=Language.objects.all())12 def save(self):13 return Snippet.objects.create(title=self.cleaned_data['title'],14 description=self.cleaned_data['description'],15 code=self.cleaned_data['code'],16 tags=self.cleaned_data['tags'],17 author=self.author,18 language=self.cleaned_data['language'])
現(xiàn)在你可以編寫一個簡短的名為add_snippet的視圖來處理后續(xù)任務(wù)。在cab/views目錄下,創(chuàng)建一個snippets.py文件,然后加入如下代碼:
from django.http import HttpResponseRedirectfrom django.shortcuts import render_to_responsefrom cab.forms import AddSnippetFormdef add_snippet(request): if request.method== 'POST' form = AddSnippetForm(author=request.user,data=request.POST) if form.is_valid(): new_snippet = form.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = AddSnippetForm(author = request.user) return render_to_response('cab/add_snippet.html',{'form':form})
這段代碼將實(shí)例化表單,校驗(yàn)數(shù)據(jù),保存新的Snippet,然后返回一個重定向到該段snippet的詳細(xì)視圖。(再次強(qiáng)調(diào),總是在成功的POST之后重定向。)
開始這個視圖函數(shù)看起來還不錯,但是這里有一個潛在的問題。你引用了request.user,即當(dāng)前登錄的用戶(Django在當(dāng)認(rèn)證系統(tǒng)被正確的激活后會自動的設(shè)置這個值)。但是如果填寫這個表單的人沒有登錄會發(fā)生什么呢?
答案是,你的數(shù)據(jù)不會被真正的校驗(yàn)。當(dāng)當(dāng)前用戶沒有登錄,request.user是一個表示匿名用戶的“假”對象,它無法被用做一個snippet的author字段的值。因此你需要以某種方式來確認(rèn)只有登錄的用戶才能填寫這個表單。
幸運(yùn)的是,Django提供了一個簡單的方式處理這個問題,使用一個認(rèn)證系統(tǒng)中的稱為login_required的裝飾器。
你可以簡單的導(dǎo)入并應(yīng)用于你的視圖函數(shù),沒有登錄的用戶將會被重定向到登錄頁面:
from django.http import HttpResponseRedirectfrom django.shortcuts import render_to_responsefrom django.contrib.auth.decorators import login_requiredfrom cab.forms import AddSnippetFromdef add_snippet(request): if request.method == 'POST': form = AddSnippetForm(author=request.user,data=request.POST) if form.is_valid(): new_snippet = form.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = AddSnippetForm(author = request.user) return render_to_response('cab/add_snippet.html', {'form':form})add_snippet = login_required(add_snippet)
#admonition:設(shè)置登錄/登出視圖
Django的認(rèn)證系統(tǒng),內(nèi)置于django.contrib.auth,包括了認(rèn)證并登入用戶的視圖函數(shù)及表單。只要是在你自己的機(jī)器上對一個應(yīng)用進(jìn)行測試,那么你可以通過django管理界面登錄,然后訪問任何你以login_required標(biāo)記的視圖。但是在公開部署時,你會需要為一般用戶設(shè)置公開界面的登入/登出視圖。
了解如何使用內(nèi)置的認(rèn)證視圖,參考Django認(rèn)證系統(tǒng)在線文檔http://docs.djangoproject.com/en/dev/topics/auth/.
現(xiàn)在,你可以編寫cab/add_snippet.html模版如下:
<html> <head> <title>Add a snippet</title> </head> <body> <h1>Add a snippet</h1> <p> Use the form below to submit your snippet;all fields are required.</p> <form method ="post" action=""> <p>{% if form.title.errors %} <span class="error"> {{form.title.errors|join:", "}} </span> {% endif %}</p>
<p><label for="id_title">Title:</label>
{{ form.title }}</p>
<p>{% if form.language.errors %}
<span class="error">
{{ form.language.errors|join:", " }}
</span>
{% endif %}</p>
盡管Django的表單系統(tǒng)使你能較為簡潔的方式編寫和使用這個表單,但是這還不是最理想的解決方案。設(shè)置一個表單來添加和編輯一個模型的實(shí)例是極為常見的事情,一遍又一遍的編寫這些程式化的表單會是件及其惱火的事情(特別是你已經(jīng)在定義模型類的時候?qū)⒍鄶?shù)甚至全部相關(guān)信息都設(shè)置了一遍的情況下)。
幸運(yùn)的是,有一個方法可以戲劇性的減少你編寫的代碼的數(shù)量。為提供不需要太多自定義行文的表單,Django提供了一個稱為ModelForm的類,可以從模型定義自動的產(chǎn)生一個適度自定義的表單,包含了所有相關(guān)字段及必要的save()方法。它的最基礎(chǔ)的工作方式如下:
from django.forms import ModelFormfrom cab.models import Snippetclass SnippetForm(ModelForm): class Meta: model = Snippet
子類化ModelForm并提供一個內(nèi)部Meta類指定一個模型將自動的從制定模型中提取字段來設(shè)置這個新的SnippetForm類。并且,ModelForm會自動的忽略在模型中定義為editable=False的字段,因此類似HTML版本的描述字段將不會在表單中顯示。這里唯一欠缺的東西其實(shí)是author這個字段將會顯示出來。幸運(yùn)的是,ModelForm支持一些自定義,包括一個要排除在表單之外的字段列表,因此你可以簡單的修改SnippetFrom的定義如下:
class SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author']
這會將author字段排除在外?,F(xiàn)在,你可以簡單的刪除cab/forms.py并像這樣重寫cab/views/snippets.py:
from django.http import HttpResponseRedirectfrom django.forms import ModelFormfrom django.shortcuts import render_to_responsefrom django.contrib.auth.decoartors import login_requiredfrom cab.models import Snippetclass SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author']def add_snippet(request): if request.method == 'POST': form = SnippetForm(data=request.POST) if form.is_valid(): new_snippet = form.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = SnippetForm() return render_to_response('cab/add_snippet.html', {'form':form})add_snippet = login_required(add_snippet)
當(dāng)然,這不是十分正確。Snippets需要填充一個作者(author),但是你已經(jīng)將這個字段排除出表單之外了。你可以回過頭去定義一個自定義的__init__()方法,并傳遞一個request.user,但是ModelForm有一個更富技巧的方式。你可以用ModelForm來創(chuàng)建一個Snippet對象并返回它,但并不保存;你通過向save()方法傳遞一個額外的參數(shù)——commit=False——來做到這一點(diǎn)。當(dāng)這樣做的時候,save()還是會返回一個新的Snippet對象,但是它不會被保存到數(shù)據(jù)庫中去。這就給了你自由的添加user,并手動的將這個新的Snippet對象插入到數(shù)據(jù)庫的機(jī)會:
from django.http import HttpResponseRedirectfrom django.forms import ModelFormfrom django.shortcuts import render_to_responsefrom django.contrib.auth.decorators import login_requiredfrom cab.models import Snippetclass SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author']def add_snippet(request): if request.method == 'POST': form = SnippetForm(data = request.POST) if form.is_valid(): new_snippet = form.save(commit=False) new_snippet.author = request.user new_snippet.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form = SnippetForm() return render_to_response('cab/add_snippet.html',{'form':form})add_snippet = login_required(add_snippet)
#admonition:commit=False 和多對多關(guān)系(many to many relationships)
如果使用的模型中有多對多字段(在表單中會以一個名為ModelMultipleChoiceField的字段來表示),你需要在在ModelForm中的save()方法里使用commit=False的時候再多一個步驟。多對多關(guān)系在主對象被保存之前是不能被設(shè)置的(因?yàn)樾枰浪跀?shù)據(jù)庫中的id)。因此在有多對多關(guān)系的表單中使用commit=False的時候,表單將會有一個名為save_m2m()的方法,為最終的多對多關(guān)系保存數(shù)據(jù)。你需要在保存了主對象之后手動的調(diào)用這個方法(不帶參數(shù))。
現(xiàn)在,你可以打開cab/urls/snippets.py,然后添加一個新的導(dǎo)入語句:
from cab.views.snippets import add_snippet
以及一個新的URL模式:
url(r'^add/$',add_snippet,name='cab_snippet_add'),
之前列出的模版還是會繼續(xù)正常的工作,因?yàn)楸韱蔚淖侄螞]有改變。但是,如果django提供一種在模版中顯示表單的簡單方式可以使你免除編寫全部重復(fù)的HTML和字段錯誤檢查的話就太好了。你已經(jīng)避免了自行定義表單類的冗繁工作,為什么不排除在模版上的冗余工作呢?
為此,每個Django表單都有一些附加的方法來獲知如何將表單渲染為不同類型的HTML:
as_url:將表單渲染為一系列HTML列表項(xiàng)(<li>標(biāo)簽),一個字段一項(xiàng)。
as_p:將表單渲染為一系列的段落(<p>標(biāo)簽),一個段落一項(xiàng)。
as_table:將表單渲染為一個HTML表格,一個字段一個<tr>標(biāo)簽。
那么,舉例來說,你可以將目前你所做的模版(以HTML段落元素分隔)替換為如下一句:
{{ form.as_p }}
但是在使用這些方法的時候需要注意:
現(xiàn)在你有了一個可以讓用戶提交他們的代碼段的系統(tǒng),但是如果有人想要返回去編輯他們提交的代碼段該怎么做呢?不可避免的總是會有某人意外的提交了含有錯誤的代碼段,或者找到了解決實(shí)際問題的更好方法。最好是允許用戶在這些情況下能夠編輯他們自己的代碼段,因此我們通過一個名為edite_snippet的視圖來設(shè)置代碼段的編輯。
幸運(yùn)的是,這十分之簡單。ModelForm同樣知道改如何編輯一個已經(jīng)存在的對象,這就解決了大部分的苦活累活。你所要做的,就是處理兩件事:
處理第一個問題十分簡單:你可以在URL中設(shè)置你的edite_snippet視圖函數(shù)接收Snippet的id,然后在數(shù)據(jù)庫中查詢它。然后你可以比較Snippet的author字段同當(dāng)前登錄用戶的一致性來保證它們的匹配。所以,我么開始再添加兩個導(dǎo)入語句在cab/views/snippets.py中:
from django.shortcuts import get_object_or_404from django.http import HttpResponseForbidden
HttpResponseForbidden 類表示一個狀態(tài)碼為403的HTTP響應(yīng),它表示用戶沒有他們想要做的事情的權(quán)限。你需要在用戶嘗試編輯不是由他們提交的代碼段的時候使用它。
下面是edite_snippet視圖:
def edit_snippet(request,snippet_id): snippet = get_object_or_404(Snippet,pk=snippet_id) if request.user.id != snippet.author.id: return HttpResponseForbidden() if request.method == 'POST': form = SnippetForm(instance=snippet,data=request.POST) if form.is_valid(): snippet = form.save() return HttpResponseRedirect(snippet.get_absolute_url()) else: form = SnippetForm(instance=snippet) return render_to_response('cab/edit_snippet.html'.{'form':form})edit_snippet = login_required(edit_snippet)
為了告訴ModelForm子類你要編輯一個已經(jīng)存在的對象,你簡單的將這個對象作為instance參數(shù)傳遞;表單會處理剩下的事情。注意因?yàn)镾nippet已經(jīng)有了一個作者,且這個值是不要改變的,你不需要使用commit=False然后手動的保存Snippet。表單不會修改這個值,所以你可以簡單的讓它來保存即可。
現(xiàn)在你可以為視圖函數(shù)添加一個URL模式。首先修改cab/urls/snippets.py中的導(dǎo)入語句來導(dǎo)入這個視圖函數(shù):
from cab.views.snippet import add_snippet,edit_snippet
然后添加URL模式:
url(r'^edit/(?P<snippet_id>\d+)/$',edit_snippet,name='cab_snippet_edit'),
因?yàn)閑dit_snippet視圖和add_snippet視圖的表單具有同樣的字段,你可以將模版化簡成一個,然后通過傳遞一個指示添加還是編輯的的變量(這樣諸如頁面標(biāo)題之類的元素可以根據(jù)它來改變)。因此我們來修改add_snippet視圖的最后一行來傳遞一個叫做add的額外變量,將其值設(shè)置為True,然后將模版名改為cab/snippet_form.html:
return render_to_response('cab/snippet_form.html',{'form':form,'add':True})
然后修改edit_snippet視圖中同樣的一行來使用cab/snippet_form.html,并將add變量設(shè)置為False:
return render_to_response('cab/snippet_form.html'.{'form':form,'add':False})
現(xiàn)在可以簡化為一個模版——cab/snippet_form.html——內(nèi)容如下:
<html> <head> <title>{%if add%}Add a {% else %}Edit your{% endif %} snippet</title> </head> <body> <h1>{% if add %}Add a {% else %}Edit your{% endif %} snippet</h1> <p>Use the form below to {% if add %}add{% else %}edit {% endif %} your snippet;all fields are required</p> <form action="" method="post"> {{ form.as_p }} <p><input type="submit" value="Send" ></p> </form> </body></html>
現(xiàn)在,使用戶能夠添加和編輯他們的代碼段的表單、視圖、模版都一應(yīng)俱全了。下面就是完成后的cab/views/snippets.py 文件:
from django.http import HttpResponseForbidden,HttpResponseRedirectfrom django.forms import ModelFormfrom django.shortcuts import get_object_or_404,render_to_responsefrom django.contrib.auth.decorators import login_requiredfrom cab.models import Snippetclass SnippetForm(ModelForm): class Meta: model = Snippet exclude = ['author']def add_snippet(request): if request.method == 'POST': form = SnippetForm(data=request.POST) if form.is_valid(): new_snippet = form.save(commit=False) new_snipet.author = request.user new_snippet.save() return HttpResponseRedirect(new_snippet.get_absolute_url()) else: form=SnippetForm() return render_to_response('cab/snippet_form.html',{'form':form,'add':True})add_snippet=login_required(add_snippet)def edit_snippet(request,snippet_id): snippet = get_object_or_404(Snipet,pk=snippet_id) if request.user.id != snippet.author.id: return HttpResponseForbidden() if request.method == 'POST': form = SnippetForm(instance=snippet,data=request.POST) if form.is_valid(): snippet = form.save() return HttpResponseReidrect(snippet.get_absolute_url()) else: form = SnippetFrom(instance=snippet) return render_to_response('cab/snippet_form.html',{'form':form,'add':False})edit_snippet = login_required(edit_snippet)
在繼續(xù)之前,我建議花點(diǎn)時間來熟悉一下Django的表單系統(tǒng)。盡管目前為止你對基本內(nèi)容已經(jīng)有了很好掌握,你可能還想多花點(diǎn)時間來看看django.forms包的完整文檔(在線地址 http://docs/djangoproject.com/en/dev/topics/forms/)對全部特性有一個全面了解(包括全部字段類型和組件,及自定義表單外觀的高級技巧)。
當(dāng)你準(zhǔn)備好回來之后,下一章將會對這個應(yīng)用進(jìn)行最后的掃尾工作,包括添加書簽和評分系統(tǒng),包括最受歡迎的代碼段列表,以及為確定用戶是否標(biāo)記或評分而做的必要的模版擴(kuò)展。
聯(lián)系客服