從Google的簡樸的單個搜索框,到常見的Blog評論提交表單,再到復雜的自定義數據輸入接口,HTML表單一直是交互性網站的支柱。 本章介紹如何用Django對用戶通過表單提交的數據進行訪問、有效性檢查以及其它處理。 與此同時,我們將介紹HttpRequest對象和Form對象。
## 從Request對象中獲取數據
我們在第三章講述View的函數時已經介紹過HttpRequest對象了,但當時并沒有講太多。 讓我們回憶下:每個view函數的第一個參數是一個HttpRequest對象,就像下面這個hello()函數:
~~~
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello world")
~~~
HttpRequest對象,比如上面代碼里的request變量,會有一些有趣的、你必須讓自己熟悉的屬性和方法,以便知道能拿它們來做些什么。 在view函數的執行過程中,你可以用這些屬性來獲取當前request的一些信息(比如,你正在加載這個頁面的用戶是誰,或者用的是什么瀏覽器)。
### URL相關信息
HttpRequest對象包含當前請求URL的一些信息:
| 屬性/方法 | 說明 | 舉例 |
| --- | --- | --- |
| request.path | 除域名以外的請求路徑,以正斜杠開頭 | "/hello/" |
| request.get_host() | 主機名(比如,通常所說的域名) | "127.0.0.1:8000"?or"www.example.com" |
| request.get_full_path() | 請求路徑,可能包含查詢字符串 | "/hello/?print=true" |
| request.is_secure() | 如果通過HTTPS訪問,則此方法返回True, 否則返回False | True?或者?False |
在view函數里,要始終用這個屬性或方法來得到URL,而不要手動輸入。 這會使得代碼更加靈活,以便在其它地方重用。 下面是一個簡單的例子:
~~~
# BAD!
def current_url_view_bad(request):
return HttpResponse("Welcome to the page at /current/")
# GOOD
def current_url_view_good(request):
return HttpResponse("Welcome to the page at %s" % request.path)
~~~
### 有關request的其它信息
request.META 是一個Python字典,包含了所有本次HTTP請求的Header信息,比如用戶IP地址和用戶Agent(通常是瀏覽器的名稱和版本號)。 注意,Header信息的完整列表取決于用戶所發送的Header信息和服務器端設置的Header信息。 這個字典中幾個常見的鍵值有:
* HTTP_REFERER,進站前鏈接網頁,如果有的話。 (請注意,它是REFERRER的筆誤。)
* HTTP_USER_AGENT,用戶瀏覽器的user-agent字符串,如果有的話。 例如:"Mozilla/5.0?(X11;?U;?Linux?i686;?fr-FR;?rv:1.8.1.17)?Gecko/20080829?Firefox/2.0.0.17"?.
* REMOTE_ADDR?客戶端IP,如:"12.345.67.89"?。(如果申請是經過代理服務器的話,那么它可能是以逗號分割的多個IP地址,如:"12.345.67.89,23.456.78.90"?。)
注意,因為 request.META 是一個普通的Python字典,因此當你試圖訪問一個不存在的鍵時,會觸發一個KeyError異常。 (HTTP header信息是由用戶的瀏覽器所提交的、不應該給予信任的“額外”數據,因此你總是應該好好設計你的應用以便當一個特定的Header數據不存在時,給出一個優雅的回應。)你應該用 try/except 語句,或者用Python字典的 get() 方法來處理這些“可能不存在的鍵”:
~~~
# BAD!
def ua_display_bad(request):
ua = request.META['HTTP_USER_AGENT'] # Might raise KeyError!
return HttpResponse("Your browser is %s" % ua)
# GOOD (VERSION 1)
def ua_display_good1(request):
try:
ua = request.META['HTTP_USER_AGENT']
except KeyError:
ua = 'unknown'
return HttpResponse("Your browser is %s" % ua)
# GOOD (VERSION 2)
def ua_display_good2(request):
ua = request.META.get('HTTP_USER_AGENT', 'unknown')
return HttpResponse("Your browser is %s" % ua)
~~~
我們鼓勵你動手寫一個簡單的view函數來顯示 request.META 的所有數據,這樣你就知道里面有什么了。 這個view函數可能是這樣的:
~~~
def display_meta(request):
values = request.META.items()
values.sort()
html = []
for k, v in values:
html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, v))
return HttpResponse('<table>%s</table>' % '\n'.join(html))
~~~
做為一個練習,看你自己能不能把上面這個view函數改用Django模板系統來實現,而不是上面這樣來手動輸入HTML代碼。 也可以試著把前面提到的 request.path 方法或 HttpRequest 對象的其它方法加進去。
### 提交的數據信息
除了基本的元數據,HttpRequest對象還有兩個屬性包含了用戶所提交的信息: request.GET 和 request.POST。二者都是類字典對象,你可以通過它們來訪問GET和POST數據。
類字典對象
我們說“request.GET和request.POST是類字典對象”,意思是他們的行為像Python里標準的字典對象,但在技術底層上他們不是標準字典對象。 比如說,request.GET和request.POST都有get()、keys()和values()方法,你可以用用 for key in request.GET 獲取所有的鍵。
那到底有什么區別呢? 因為request.GET和request.POST擁有一些普通的字典對象所沒有的方法。 我們會稍后講到。
你可能以前遇到過相似的名字:類文件對象,這些Python對象有一些基本的方法,如read(),用來做真正的Python文件對象的代用品。
POST數據是來自HTML中的〈form〉標簽提交的,而GET數據可能來自〈form〉提交也可能是URL中的查詢字符串(the query string)。
## 一個簡單的表單處理示例
繼續本書一直進行的關于書籍、作者、出版社的例子,我們現在來創建一個簡單的view函數以便讓用戶可以通過書名從數據庫中查找書籍。
通常,表單開發分為兩個部分: 前端HTML頁面用戶接口和后臺view函數對所提交數據的處理過程。 第一部分很簡單;現在我們來建立個view來顯示一個搜索表單:
~~~
from django.shortcuts import render_to_response
def search_form(request):
return render_to_response('search_form.html')
~~~
在第三章已經學過,這個view函數可以放到Python的搜索路徑的任何位置。 為了便于討論,咱們將它放在 books/views.py 里。
這個 search_form.html 模板,可能看起來是這樣的:
~~~
<html>
<head>
<title>Search</title>
</head>
<body>
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>
</body>
</html>
~~~
而 urls.py 中的 URLpattern 可能是這樣的:
~~~
from mysite.books import views
urlpatterns = patterns('',
# ...
(r'^search-form/$', views.search_form),
# ...
)
~~~
(注意,我們直接將views模塊import進來了,而不是用類似 from mysite.views import search_form 這樣的語句,因為前者看起來更簡潔。 我們將在第8章講述更多的關于import的用法。)
現在,如果你運行?runserver?命令,然后訪問http://127.0.0.1:8000/search-form/,你會看到搜索界面。 非常簡單。
不過,當你通過這個form提交數據時,你會得到一個Django 404錯誤。 這個Form指向的URL?/search/?還沒有被實現。 讓我們添加第二個視圖函數并設置URL:
~~~
# urls.py
urlpatterns = patterns('',
# ...
(r'^search-form/$', views.search_form),
(r'^search/$', views.search),
# ...
)
# views.py
def search(request):
if 'q' in request.GET:
message = 'You searched for: %r' % request.GET['q']
else:
message = 'You submitted an empty form.'
return HttpResponse(message)
~~~
暫時先只顯示用戶搜索的字詞,以確定搜索數據被正確地提交給了Django,這樣你就會知道搜索數據是如何在這個系統中傳遞的。 簡而言之:
1. 在HTML里我們定義了一個變量q。當提交表單時,變量q的值通過GET(method=”get”)附加在URL /search/上。
1. 處理/search/(search())的視圖通過request.GET來獲取q的值。
需要注意的是在這里明確地判斷q是否包含在request.GET中。就像上面request.META小節里面提到,對于用戶提交過來的數據,甚至是正確的數據,都需要進行過濾。 在這里若沒有進行檢測,那么用戶提交一個空的表單將引發KeyError異常:
~~~
# BAD!
def bad_search(request):
# The following line will raise KeyError if 'q' hasn't
# been submitted!
message = 'You searched for: %r' % request.GET['q']
return HttpResponse(message)
~~~
查詢字符串參數
因為使用GET方法的數據是通過查詢字符串的方式傳遞的(例如/search/?q=django),所以我們可以使用requet.GET來獲取這些數據。 第三章介紹Django的URLconf系統時我們比較了Django的簡潔的URL與PHP/Java傳統的URL,我們提到將在第七章講述如何使用傳統的URL。通過剛才的介紹,我們知道在視圖里可以使用request.GET來獲取傳統URL里的查詢字符串(例如hours=3)。
獲取使用POST方法的數據與GET的相似,只是使用request.POST代替了request.GET。那么,POST與GET之間有什么不同?當我們提交表單僅僅需要獲取數據時就可以用GET; 而當我們提交表單時需要更改服務器數據的狀態,或者說發送e-mail,或者其他不僅僅是獲取并顯示數據的時候就使用POST。 在這個搜索書籍的例子里,我們使用GET,因為這個查詢不會更改服務器數據的狀態。 (如果你有興趣了解更多關于GET和POST的知識,可以參見http://www.w3.org/2001/tag/doc/whenToUseGet.html。)
既然已經確認用戶所提交的數據是有效的,那么接下來就可以從數據庫中查詢這個有效的數據(同樣,在views.py里操作):
~~~
from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book
def search(request):
if 'q' in request.GET and request.GET['q']:
q = request.GET['q']
books = Book.objects.filter(title__icontains=q)
return render_to_response('search_results.html',
{'books': books, 'query': q})
else:
return HttpResponse('Please submit a search term.')
~~~
讓我們來分析一下上面的代碼:
> 除了檢查q是否存在于request.GET之外,我們還檢查來reuqest.GET[‘q’]的值是否為空。
>
> 我們使用Book.objects.filter(title__icontains=q)獲取數據庫中標題包含q的書籍。 icontains是一個查詢關鍵字(參看第五章和附錄B)。這個語句可以理解為獲取標題里包含q的書籍,不區分大小寫。
>
> 這是實現書籍查詢的一個很簡單的方法。 我們不推薦在一個包含大量產品的數據庫中使用icontains查詢,因為那會很慢。 (在真實的案例中,我們可以使用以某種分類的自定義查詢系統。 在網上搜索“開源 全文搜索”看看是否有好的方法)
>
> 最后,我們給模板傳遞來books,一個包含Book對象的列表。 查詢結果的顯示模板search_results.html如下所示:
~~~
<p>You searched for: <strong>{{ query }}</strong></p>
{% if books %}
<p>Found {{ books|length }} book{{ books|pluralize }}.</p>
<ul>
{% for book in books %}
<li>{{ book.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>No books matched your search criteria.</p>
{% endif %}
~~~
> 注意這里pluralize的使用,這個過濾器在適當的時候會輸出s(例如找到多本書籍)。
## 改進表單
同上一章一樣,我們先從最為簡單、有效的例子開始。 現在我們再來找出這個簡單的例子中的不足,然后改進他們。
首先,search()視圖對于空字符串的處理相當薄弱——僅顯示一條”Please submit a search term.”的提示信息。 若用戶要重新填寫表單必須自行點擊“后退”按鈕, 這種做法既糟糕又不專業。如果在現實的案例中,我們這樣子編寫,那么Django的優勢將蕩然無存。
在檢測到空字符串時更好的解決方法是重新顯示表單,并在表單上面給出錯誤提示以便用戶立刻重新填寫。 最簡單的實現方法既是添加else分句重新顯示表單,代碼如下:
~~~
from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book
def search_form(request):
return render_to_response('search_form.html')
def search(request):
if 'q' in request.GET and request.GET['q']:
q = request.GET['q']
books = Book.objects.filter(title__icontains=q)
return render_to_response('search_results.html',
{'books': books, 'query': q})
else:
return render_to_response('search_form.html', {'error': True})
~~~
(注意,將search_form()視圖也包含進來以便查看)
這段代碼里,我們改進來search()視圖:在字符串為空時重新顯示search_form.html。 并且給這個模板傳遞了一個變量error,記錄著錯誤提示信息。 現在我們編輯一下search_form.html,檢測變量error:
~~~
<html>
<head>
<title>Search</title>
</head>
<body>
{% if error %}
<p style="color: red;">Please submit a search term.</p>
{% endif %}
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>
</body>
</html>
~~~
我們修改了search_form()視圖所使用的模板,因為search_form()視圖沒有傳遞error變量,所以在條用search_form視圖時不會顯示錯誤信息。
通過上面的一些修改,現在程序變的好多了,但是現在出現一個問題: 是否有必要專門編寫search_form()來顯示表單? 按實際情況來說,當一個請求發送至/search/(未包含GET的數據)后將會顯示一個空的表單(帶有錯誤信息)。 所以,只要我們改變search()視圖:當用戶訪問/search/并未提交任何數據時就隱藏錯誤信息,這樣就移去search_form()視圖以及對應的URLpattern。
~~~
def search(request):
error = False
if 'q' in request.GET:
q = request.GET['q']
if not q:
error = True
else:
books = Book.objects.filter(title__icontains=q)
return render_to_response('search_results.html',
{'books': books, 'query': q})
return render_to_response('search_form.html',
{'error': error})
~~~
在改進后的視圖中,若用戶訪問/search/并且沒有帶有GET數據,那么他將看到一個沒有錯誤信息的表單; 如果用戶提交了一個空表單,那么它將看到錯誤提示信息,還有表單; 最后,若用戶提交了一個非空的值,那么他將看到搜索結果。
最后,我們再稍微改進一下這個表單,去掉冗余的部分。 既然已經將兩個視圖與URLs合并起來,/search/視圖管理著表單的顯示以及結果的顯示,那么在search_form.html里表單的action值就沒有必要硬編碼的指定URL。 原先的代碼是這樣:
~~~
<form action="/search/" method="get">
~~~
現在改成這樣:
~~~
<form action="" method="get">
~~~
action=”“意味著表單將提交給與當前頁面相同的URL。 這樣修改之后,如果search()視圖不指向其它頁面的話,你將不必再修改action。
## 簡單的驗證
我們的搜索示例仍然相當地簡單,特別從數據驗證方面來講;我們僅僅只驗證搜索關鍵值是否為空。 然后許多HTML表單包含著比檢測值是否為空更為復雜的驗證。 我們都有在網站上見過類似以下的錯誤提示信息:
* 請輸入一個有效的email地址, foo’ 并不是一個有效的e-mail地址。
* 請輸入5位數的U.S 郵政編碼, 123并非是一個有效的郵政編碼。
* 請輸入YYYY-MM-DD格式的日期。
* 請輸入8位數以上并至少包含一個數字的密碼。
關于JavaScript驗證
可以使用Javascript在客戶端瀏覽器里對數據進行驗證,這些知識已超出本書范圍。 要注意: 即使在客戶端已經做了驗證,但是服務器端仍必須再驗證一次。 因為有些用戶會將JavaScript關閉掉,并且還有一些懷有惡意的用戶會嘗試提交非法的數據來探測是否有可以攻擊的機會。
除了在服務器端對用戶提交的數據進行驗證(例如在視圖里驗證),我們沒有其他辦法。 JavaScript驗證可以看作是額外的功能,但不能作為唯一的驗證功能。
我們來調整一下search()視圖,讓她能夠驗證搜索關鍵詞是否小于或等于20個字符。 (為來讓例子更為顯著,我們假設如果關鍵詞超過20個字符將導致查詢十分緩慢)。那么該如何實現呢? 最簡單的方式就是將邏輯處理直接嵌入到視圖里,就像這樣:
~~~
def search(request):
error = False
if 'q' in request.GET:
q = request.GET['q']
if not q:
error = True
elif len(q) > 20:
error = True
else:
books = Book.objects.filter(title__icontains=q)
return render_to_response('search_results.html',
{'books': books, 'query': q})
return render_to_response('search_form.html',
{'error': error})
~~~
現在,如果嘗試著提交一個超過20個字符的搜索關鍵詞,系統不會執行搜索操作,而是顯示一條錯誤提示信息。 但是,search_form.html里的這條提示信息是:”Please submit a search term.”,這顯然是錯誤的, 所以我們需要更精確的提示信息:
~~~
<html>
<head>
<title>Search</title>
</head>
<body>
{% if error %}
<p style="color: red;">Please submit a search term 20 characters or shorter.</p>
{% endif %}
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>
</body>
</html>
~~~
但像這樣修改之后仍有一些問題。 我們包含萬象的提示信息很容易使人產生困惑: 提交一個空表單怎么會出現一個關于20個字符限制的提示? 所以,提示信息必須是詳細的,明確的,不會產生疑議。
問題的實質在于我們只使用來一個布爾類型的變量來檢測是否出錯,而不是使用一個列表來記錄相應的錯誤信息。 我們需要做如下的調整:
~~~
def search(request):
errors = []
if 'q' in request.GET:
q = request.GET['q']
if not q:
errors.append('Enter a search term.')
elif len(q) > 20:
errors.append('Please enter at most 20 characters.')
else:
books = Book.objects.filter(title__icontains=q)
return render_to_response('search_results.html',
{'books': books, 'query': q})
return render_to_response('search_form.html',
{'errors': errors })
~~~
接著,我們要修改一下search_form.html模板,現在需要顯示一個errors列表而不是一個布爾判斷。
~~~
<html>
<head>
<title>Search</title>
</head>
<body>
{% if errors %}
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form action="/search/" method="get">
<input type="text" name="q">
<input type="submit" value="Search">
</form>
</body>
</html>
~~~
## 編寫Contact表單
雖然我們一直使用書籍搜索的示例表單,并將起改進的很完美,但是這還是相當的簡陋: 只包含一個字段,q。這簡單的例子,我們不需要使用Django表單庫來處理。 但是復雜一點的表單就需要多方面的處理,我們現在來一下一個較為復雜的例子: 站點聯系表單。
這個表單包括用戶提交的反饋信息,一個可選的e-mail回信地址。 當這個表單提交并且數據通過驗證后,系統將自動發送一封包含題用戶提交的信息的e-mail給站點工作人員。
我們從contact_form.html模板入手:
~~~
<html>
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
{% if errors %}
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form action="/contact/" method="post">
<p>Subject: <input type="text" name="subject"></p>
<p>Your e-mail (optional): <input type="text" name="email"></p>
<p>Message: <textarea name="message" rows="10" cols="50"></textarea></p>
<input type="submit" value="Submit">
</form>
</body>
</html>
~~~
我們定義了三個字段: 主題,e-mail和反饋信息。 除了e-mail字段為可選,其他兩個字段都是必填項。 注意,這里我們使用method=”post”而非method=”get”,因為這個表單會有一個服務器端的操作:發送一封e-mail。 并且,我們復制了前一個模板search_form.html中錯誤信息顯示的代碼。
如果我們順著上一節編寫search()視圖的思路,那么一個contact()視圖代碼應該像這樣:
~~~
from django.core.mail import send_mail
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
def contact(request):
errors = []
if request.method == 'POST':
if not request.POST.get('subject', ''):
errors.append('Enter a subject.')
if not request.POST.get('message', ''):
errors.append('Enter a message.')
if request.POST.get('email') and '@' not in request.POST['email']:
errors.append('Enter a valid e-mail address.')
if not errors:
send_mail(
request.POST['subject'],
request.POST['message'],
request.POST.get('email', 'noreply@example.com'),
['siteowner@example.com'],
)
return HttpResponseRedirect('/contact/thanks/')
return render_to_response('contact_form.html',
{'errors': errors})
~~~
(如果按照書中的示例做下來,這這里可能乎產生一個疑問:contact()視圖是否要放在books/views.py這個文件里。 但是contact()視圖與books應用沒有任何關聯,那么這個視圖應該可以放在別的地方? 這毫無緊要,只要在URLconf里正確設置URL與視圖之間的映射,Django會正確處理的。 筆者個人喜歡創建一個contact的文件夾,與books文件夾同級。這個文件夾中包括空的__init__.py和views.py兩個文件。
現在來分析一下以上的代碼:
> 確認request.method的值是’POST’。用戶瀏覽表單時這個值并不存在,當且僅當表單被提交時這個值才出現。 (在后面的例子中,request.method將會設置為’GET’,因為在普通的網頁瀏覽中,瀏覽器都使用GET,而非POST)。判斷request.method的值很好地幫助我們將表單顯示與表單處理隔離開來。
>
> 我們使用request.POST代替request.GET來獲取提交過來的數據。 這是必須的,因為contact_form.html里表單使用的是method=”post”。如果在視圖里通過POST獲取數據,那么request.GET將為空。
>
> 這里,有兩個必填項,subject 和 message,所以需要對這兩個進行驗證。 注意,我們使用request.POST.get()方法,并提供一個空的字符串作為默認值;這個方法很好的解決了鍵丟失與空數據問題。
>
> 雖然email非必填項,但如果有提交她的值則我們也需進行驗證。 我們的驗證算法相當的薄弱,僅驗證值是否包含@字符。 在實際應用中,需要更為健壯的驗證機制(Django提供這些驗證機制,稍候我們就會看到)。
>
> 我們使用了django.core.mail.send_mail函數來發送e-mail。 這個函數有四個必選參數: 主題,正文,寄信人和收件人列表。 send_mail是Django的EmailMessage類的一個方便的包裝,EmailMessage類提供了更高級的方法,比如附件,多部分郵件,以及對于郵件頭部的完整控制。
>
> 注意,若要使用send_mail()函數來發送郵件,那么服務器需要配置成能夠對外發送郵件,并且在Django中設置出站服務器地址。 參見規范:http://docs.djangoproject.com/en/dev/topics/email/
>
> 當郵件發送成功之后,我們使用HttpResponseRedirect對象將網頁重定向至一個包含成功信息的頁面。 包含成功信息的頁面這里留給讀者去編寫(很簡單 一個視圖/URL映射/一份模板即可),但是我們要解釋一下為何重定向至新的頁面,而不是在模板中直接調用render_to_response()來輸出。
>
> 原因就是: 若用戶刷新一個包含POST表單的頁面,那么請求將會重新發送造成重復。 這通常會造成非期望的結果,比如說重復的數據庫記錄;在我們的例子中,將導致發送兩封同樣的郵件。 如果用戶在POST表單之后被重定向至另外的頁面,就不會造成重復的請求了。
>
> 我們應每次都給成功的POST請求做重定向。 這就是web開發的最佳實踐。
contact()視圖可以正常工作,但是她的驗證功能有些復雜。 想象一下假如一個表單包含一打字段,我們真的將必須去編寫每個域對應的if判斷語句?
另外一個問題是表單的重新顯示。若數據驗證失敗后,返回客戶端的表單中各字段最好是填有原來提交的數據,以便用戶查看哪里出現錯誤(用戶也不需再次填寫正確的字段值)。 我們可以手動地將原來的提交數據返回給模板,并且必須編輯HTML里的各字段來填充原來的值。
~~~
# views.py
def contact(request):
errors = []
if request.method == 'POST':
if not request.POST.get('subject', ''):
errors.append('Enter a subject.')
if not request.POST.get('message', ''):
errors.append('Enter a message.')
if request.POST.get('email') and '@' not in request.POST['email']:
errors.append('Enter a valid e-mail address.')
if not errors:
send_mail(
request.POST['subject'],
request.POST['message'],
request.POST.get('email', `'noreply@example.com`_'),
[`'siteowner@example.com`_'],
)
return HttpResponseRedirect('/contact/thanks/')
return render_to_response('contact_form.html', {
'errors': errors,
'subject': request.POST.get('subject', ''),
'message': request.POST.get('message', ''),
'email': request.POST.get('email', ''),
})
# contact_form.html
<html>
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
{% if errors %}
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form action="/contact/" method="post">
<p>Subject: <input type="text" name="subject" value="{{ subject }}" ></p>
<p>Your e-mail (optional): <input type="text" name="email" value="{{ email }}" ></p>
<p>Message: <textarea name="message" rows="10" cols="50">{{ message }}</textarea></p>
<input type="submit" value="Submit">
</form>
</body>
</html>
~~~
這看起來雜亂,且寫的時候容易出錯。 希望你開始明白使用高級庫的用意——負責處理表單及相關校驗任務。
## 第一個Form類
Django帶有一個form庫,稱為django.forms,這個庫可以處理我們本章所提到的包括HTML表單顯示以及驗證。 接下來我們來深入了解一下form庫,并使用她來重寫contact表單應用。
Django的newforms庫
在Django社區上會經常看到django.newforms這個詞語。當人們討論django.newforms,其實就是我們本章里面介紹的django.forms。
改名其實有歷史原因的。 當Django一次向公眾發行時,它有一個復雜難懂的表單系統:django.forms。后來它被完全重寫了,新的版本改叫作:django.newforms,這樣人們還可以通過名稱,使用舊版本。 當Django 1.0發布時,舊版本django.forms就不再使用了,而django.newforms也終于可以名正言順的叫做:django.forms。
表單框架最主要的用法是,為每一個將要處理的HTML的`` `` 定義一個Form類。 在這個例子中,我們只有一個`` `` ,因此我們只需定義一個Form類。 這個類可以存在于任何地方,甚至直接寫在`` views.py`` 文件里也行,但是社區的慣例是把Form類都放到一個文件中:forms.py。在存放`` views.py`` 的目錄中,創建這個文件,然后輸入:
~~~
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField()
email = forms.EmailField(required=False)
message = forms.CharField()
~~~
這看上去簡單易懂,并且很像在模塊中使用的語法。 表單中的每一個字段(域)作為Form類的屬性,被展現成Field類。這里只用到CharField和EmailField類型。 每一個字段都默認是必填。要使email成為可選項,我們需要指定required=False。
讓我們鉆研到Python解釋器里面看看這個類做了些什么。 它做的第一件事是將自己顯示成HTML:
~~~
>>> from contact.forms import ContactForm
>>> f = ContactForm()
>>> print f
<tr><th><label for="id_subject">Subject:</label></th><td><input type="text" name="subject" id="id_subject" /></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="text" name="email" id="id_email" /></td></tr>
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message
~~~
為了便于訪問,Django用`` `` 標志,為每一個字段添加了標簽。 這個做法使默認行為盡可能合適。
默認輸出按照HTML的格式,另外有一些其它格式的輸出:
~~~
>>> print f.as_ul()
<li><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></li>
<li><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></li>
<li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li>
>>> print f.as_p()
<p><label for="id_subject">Subject:</label> <input type="text" name="subject" id="id_subject" /></p>
<p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p>
<p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p>
~~~
請注意,標簽、、的開閉合標記沒有包含于輸出當中,這樣你就可以添加額外的行或者自定義格式。
這些類方法只是一般情況下用于快捷顯示完整表單的方法。 你同樣可以用HTML顯示個別字段:
~~~
>>> print f['subject']
<input type="text" name="subject" id="id_subject" />
>>> print f['message']
<input type="text" name="message" id="id_message" />
~~~
Form對象做的第二件事是校驗數據。 為了校驗數據,我們創建一個新的對Form象,并且傳入一個與定義匹配的字典類型數據:
~~~
>>> f = ContactForm({'subject': 'Hello', 'email': 'adrian@example.com', 'message': 'Nice site!'})
~~~
一旦你對一個Form實體賦值,你就得到了一個綁定form:
~~~
>>> f.is_bound
True
~~~
調用任何綁定form的is_valid()方法,就可以知道它的數據是否合法。 我們已經為每個字段傳入了值,因此整個Form是合法的:
~~~
>>> f.is_valid()
True
~~~
如果我們不傳入email值,它依然是合法的。因為我們指定這個字段的屬性required=False:
~~~
>>> f = ContactForm({'subject': 'Hello', 'message': 'Nice site!'})
>>> f.is_valid()
True
~~~
但是,如果留空subject或message,整個Form就不再合法了:
~~~
>>> f = ContactForm({'subject': 'Hello'})
>>> f.is_valid()
False
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.is_valid()
False
~~~
你可以逐一查看每個字段的出錯消息:
~~~
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f['message'].errors
[u'This field is required.']
>>> f['subject'].errors
[]
>>> f['email'].errors
[]
~~~
每一個邦定Form實體都有一個errors屬性,它為你提供了一個字段與錯誤消息相映射的字典表。
~~~
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.errors
{'message': [u'This field is required.']}
~~~
最終,如果一個Form實體的數據是合法的,它就會有一個可用的cleaned_data屬性。 這是一個包含干凈的提交數據的字典。 Django的form框架不但校驗數據,它還會把它們轉換成相應的Python類型數據,這叫做清理數據。
~~~
>>> f = ContactForm({subject': Hello, email: adrian@example.com, message: Nice site!})
>>> f.is_valid()
True
>>> f.cleaned_data
{message': uNice site!, email: uadrian@example.com, subject: uHello}
~~~
我們的contact form只涉及字符串類型,它們會被清理成Unicode對象。如果我們使用整數型或日期型,form框架會確保方法使用合適的Python整數型或datetime.date型對象。
## 在視圖中使用Form對象
在學習了關于Form類的基本知識后,你會看到我們如何把它用到視圖中,取代contact()代碼中不整齊的部分。 一下示例說明了我們如何用forms框架重寫contact():
~~~
# views.py
from django.shortcuts import render_to_response
from mysite.contact.forms import ContactForm
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
send_mail(
cd['subject'],
cd['message'],
cd.get('email', 'noreply@example.com'),
['siteowner@example.com'],
)
return HttpResponseRedirect('/contact/thanks/')
else:
form = ContactForm()
return render_to_response('contact_form.html', {'form': form})
# contact_form.html
<html>
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<form action="" method="post">
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
</body>
</html>
~~~
看看,我們能移除這么多不整齊的代碼! Django的forms框架處理HTML顯示、數據校驗、數據清理和表單錯誤重現。
嘗試在本地運行。 裝載表單,先留空所有字段提交空表單;繼而填寫一個錯誤的郵箱地址再嘗試提交表單;最后再用正確數據提交表單。 (根據服務器的設置,當send_mail()被調用時,你將得到一個錯誤提示。而這是另一個問題。)
## 改變字段顯示
你可能首先注意到:當你在本地顯示這個表單的時,message字段被顯示成`` input type=”text”`` ,而它應該被顯示成。我們可以通過設置* widget* 來修改它:
~~~
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField()
email = forms.EmailField(required=False)
message = forms.CharField(widget=forms.Textarea )
~~~
forms框架把每一個字段的顯示邏輯分離到一組部件(widget)中。 每一個字段類型都擁有一個默認的部件,我們也可以容易地替換掉默認的部件,或者提供一個自定義的部件。
考慮一下Field類表現* 校驗邏輯* ,而部件表現* 顯示邏輯* 。
## 設置最大長度
一個最經常使用的校驗要求是檢查字段長度。 另外,我們應該改進ContactForm,使subject限制在100個字符以內。 為此,僅需為CharField提供max_length參數,像這樣:
~~~
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100 )
email = forms.EmailField(required=False)
message = forms.CharField(widget=forms.Textarea)
~~~
選項min_length參數同樣可用。
## 設置初始值
讓我們再改進一下這個表單:為字subject段添加* 初始值* :?"I?love?your?site!"?(一點建議,但沒壞處。)為此,我們可以在創建Form實體時,使用initial參數:
~~~
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
send_mail(
cd['subject'],
cd['message'],
cd.get('email', `'noreply@example.com`_'),
[`'siteowner@example.com`_'],
)
return HttpResponseRedirect('/contact/thanks/')
else:
form = ContactForm(
initial={'subject': 'I love your site!'}
)
return render_to_response('contact_form.html', {'form': form})
~~~
現在,subject字段將被那個句子填充。
請注意,傳入* 初始值* 數據和傳入數據以* 綁定* 表單是有區別的。 最大的區別是,如果僅傳入* 初始值* 數據,表單是_unbound_的,那意味著它沒有錯誤消息。
## 自定義校驗規則
假設我們已經發布了反饋頁面了,email已經開始源源不斷地涌入了。 這里有一個問題: 一些提交的消息只有一兩個字,我們無法得知詳細的信息。 所以我們決定增加一條新的校驗: 來點專業精神,最起碼寫四個字,拜托。
我們有很多的方法把我們的自定義校驗掛在Django的form上。 如果我們的規則會被一次又一次的使用,我們可以創建一個自定義的字段類型。 大多數的自定義校驗都是一次性的,可以直接綁定到form類.
我們希望`` message`` 字段有一個額外的校驗,我們增加一個`` clean_message()`` 方法到`` Form`` 類:
~~~
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
email = forms.EmailField(required=False)
message = forms.CharField(widget=forms.Textarea)
def clean_message(self):
message = self.cleaned_data['message']
num_words = len(message.split())
if num_words < 4:
raise forms.ValidationError("Not enough words!")
return message
~~~
Django的form系統自動尋找匹配的函數方法,該方法名稱以clean_開頭,并以字段名稱結束。 如果有這樣的方法,它將在校驗時被調用。
特別地,clean_message()方法將在指定字段的默認校驗邏輯執行* 之后* 被調用。(本例中,在必填CharField這個校驗邏輯之后。)因為字段數據已經被部分處理,所以它被從self.cleaned_data中提取出來了。同樣,我們不必擔心數據是否為空,因為它已經被校驗過了。
我們簡單地使用了len()和split()的組合來計算單詞的數量。 如果用戶輸入字數不足,我們拋出一個forms.ValidationError型異常。這個異常的描述會被作為錯誤列表中的一項顯示給用戶。
在函數的末尾顯式地返回字段的值非常重要。 我們可以在我們自定義的校驗方法中修改它的值(或者把它轉換成另一種Python類型)。 如果我們忘記了這一步,None值就會返回,原始的數據就丟失掉了。
## 指定標簽
HTML表單中自動生成的標簽默認是按照規則生成的:用空格代替下劃線,首字母大寫。如email的標簽是"Email"?。(好像在哪聽到過? 是的,同樣的邏輯被用于模塊(model)中字段的verbose_name值。 我們在第五章談到過。)
像在模塊中做過的那樣,我們同樣可以自定義字段的標簽。 僅需使用label,像這樣:
~~~
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
email = forms.EmailField(required=False, label='Your e-mail address' )
message = forms.CharField(widget=forms.Textarea)
~~~
## 定制Form設計
在上面的`` contact_form.html`` 模板中我們使用`` {{form.as_table}}`` 顯示表單,不過我們可以使用其他更精確控制表單顯示的方法。
修改form的顯示的最快捷的方式是使用CSS。 尤其是錯誤列表,可以增強視覺效果。自動生成的錯誤列表精確的使用`<ul class=”errorlist”>`,這樣,我們就可以針對它們使用CSS。 下面的CSS讓錯誤更加醒目了:
~~~
<style type="text/css">
ul.errorlist {
margin: 0;
padding: 0;
}
.errorlist li {
background-color: red;
color: white;
display: block;
font-size: 10px;
margin: 0 0 3px;
padding: 4px 5px;
}
</style>
~~~
雖然,自動生成HTML是很方便的,但是在某些時候,你會想覆蓋默認的顯示。 {{form.as_table}}和其它的方法在開發的時候是一個快捷的方式,form的顯示方式也可以在form中被方便地重寫。
每一個字段部件(, , , 或者類似)都可以通過訪問{{form.字段名}}進行單獨的渲染。
~~~
<html>
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<form action="" method="post">
<div class="field">
{{ form.subject.errors }}
<label for="id_subject">Subject:</label>
{{ form.subject }}
</div>
<div class="field">
{{ form.email.errors }}
<label for="id_email">Your e-mail address:</label>
{{ form.email }}
</div>
<div class="field">
{{ form.message.errors }}
<label for="id_message">Message:</label>
{{ form.message }}
</div>
<input type="submit" value="Submit">
</form>
</body>
</html>
~~~
{{?form.message.errors?}}?會在??class="errorlist">?里面顯示,如果字段是合法的,或者form沒有被綁定,就顯示一個空字符串。 我們還可以把?form.message.errors?當作一個布爾值或者當它是list在上面做迭代, 例如:
~~~
<div class="field{% if form.message.errors %} errors{% endif %}">
{% if form.message.errors %}
<ul>
{% for error in form.message.errors %}
<li><strong>{{ error }}</strong></li>
{% endfor %}
</ul>
{% endif %}
<label for="id_message">Message:</label>
{{ form.message }}
</div>
~~~
在校驗失敗的情況下, 這段代碼會在包含錯誤字段的div的class屬性中增加一個”errors”,在一個有序列表中顯示錯誤信息。
## 下一章
這一章總結了本書的介紹材料,即所謂“核心教程”。 后面部分,從第八章到第十二章,將詳細講述高級(進階)使用,包括如何配置一個Django應用程序(第十二章)。
在學習本書的前面七章后,我們終于對于使用Django構建自己的網站已經知道的夠多了, 本書中剩余的材料將在你需要的時候幫助你補遺。
第八章我們將回頭、并深入地講解 視圖和URLconfs(第三章已簡單介紹)。