6. 表單
從簡朴的單個搜索框,到常見的Blog評論提交表單,再到復雜的自定義數據輸入接口,HTML表單一直是交互性網站的重要交互手段。本章介紹如何用Django如何對用戶通過表單提交的數據進行訪問、有效性檢查以及其它處理等。
首先,我們先簡要介紹一下HttpRequest對象和Form對象。
6.1. 提交的數據信息
除了基本的元數據,HttpRequest對象有兩個屬性包含了用戶所提交的信息: request.GET 和 request.POST。二者都是類字典對象,我們可以通過它們來訪問GET和POST數據。
POST數據是來自HTML中的〈form〉標簽提交的,而GET數據可能來自〈form〉提交也可能是URL中的查詢字符串(the query string),
如:http://127.0.0.1/search/?q=python。
6.2. 一個簡單的表單示例
繼續本文一直進行的關於物料、庫存、入庫單的例子,我們現在來創建一個簡單的view函數以便讓用戶可以通過物料名稱從數據庫中查找物料信息。
通常,如前面模板章節闡述的表單開發分為兩個部分: 前端HTML頁面用戶接口和后台view函數對所提交數據的處理過程。
我們先建立一個view來顯示一個搜索表單:
from django.shortcuts import render_to_response def search_form(request): return render_to_response('search_form.html')
根據前面章節創建的應用,我們把這個view函數放到 inventory/views.py 里,同時在應用inventory的目錄增加一個子目錄“Forms”來放置模板文件,inventory目錄結構及文件如下:
inventory / __init__.py models.py tests.py views.py Forms/ search_form.html
這個 search_form.html 模板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>
現在我們修改mysite/urls.py 中的 URLpattern列表,修改結果如下:
from django.conf.urls import patterns, include, url from mysite.views import helloworld,current_datetime from inventory import views urlpatterns = patterns('', url(r'^helloworld/$', helloworld), url(r'^mytime/$', current_datetime), url(r'^search_form/$', views.search_form), )
然后我們添加第二個視圖函數“Search”並設置對應的URL:
# urls.py urlpatterns = patterns('', # ... (r'^search-form/$', views.search_form), (r'^search/$', views.search), # ... ) # views.py from django.shortcuts import render_to_response from django.http import HttpResponse 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)
Search函數暫時先只顯示用戶搜索的字詞,以確定搜索數據被正確地提交到Django服務端,同時,我們來看看搜索數據是如何在這個系統中傳遞的。
本例中,我們修改了模板文件的存放位置,我們需要修改Django的模板加載目錄配置信息:
import os.path TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. os.path.join(os.path.dirname(__file__), '../inventory/forms').replace('\\','/'), )
現在我們瀏覽這個例子的結果:

點擊提交按鈕后如下:

我們來分析一下上面函數的整個運行機制,在HTML里定義了一個變量q。當提交表單時,變量q的值將通過GET(method=”get”)的方式附加在URL /search/上,提交到Django服務端處理。search_form.html中定義了后台的處理響應URL為/search/(search())的視圖,通過request.GET來獲取q的值,最后把獲取的值通過HttpResponse反饋回來。
需要注意的是在這里必須明確地判斷q是否包含在request.GET中,在這里若沒有進行檢測判斷,那么用戶提交一個空的表單將引發KeyError異常。因為使用GET方法的數據是通過查詢字符串的方式傳遞的(例如/search/?q=python),因此我們可以使用requet.GET來獲取這些數據。
獲取使用POST方法的數據與GET的相似,只是使用request.POST代替了request.GET。POST與GET之間有什么不同請查閱相關資料。比較簡單的區別就是當提交的表單僅僅需要讀取數據就用GET,需要寫服務器數據時、或者其它操作時,就使用POST(寫操作可以理解成對服務器持久化數據、狀態等的修改)。
現在我們的表單已經可以正常的提交數據到Django服務端,接下來我們就可以通過model層從數據庫中查詢提交過來的數據(views.py修改如下):
def search(request): if 'q' in request.GET and request.GET['q']: q = request.GET['q'] items = Item.objects.filter(ItemName__icontains=q) return render_to_response('search_results.html', {'items': items, 'query': q}) else: return HttpResponse('You submitted an empty form.')
同時,我們增加一個反饋結果的模板search_results.html,來顯示查詢到物料列表。 查詢結果的顯示模板如下所示:
<p>You searched for: <strong>{{ query }}</strong></p> {% if items %} <p>Found {{ items|length }} item{{ items|pluralize }}.</p> <ul> {% for item in items %} <li>{{ item.ItemName }}</li> {% endfor %} </ul> {% else %} <p>No Items matched your search criteria.</p> {% endif %}

6.3.表單改進
本節我們闡述如何通過不斷優化代碼來改進表單的用戶體驗,簡化代碼的復雜度。首先,search()視圖對於空字符串的處理相當簡單——僅顯示一條”Please submit a search term.”的提示信息。若用戶要重新填寫表單必須自行點擊“后退”按鈕,在檢測到空字符串時更好的解決方法是重新顯示表單,並在表單上面給出錯誤提示以便用戶立刻重新填寫。
簡單的實現方法既是添加else分句重新顯示表單,代碼如下:
from django.http import HttpResponse from django.shortcuts import render_to_response from inventory.models import Item 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'] items= Item.objects.filter(ItemName__icontains=q) return render_to_response('search_results.html', {' items ': items, 'query': q}) else: return render_to_response('search_form.html', {'error': True})
我們改進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/(未包含GET的數據)后將會顯示一個空的表單,提交數據后再處理數據。
search()視圖修改成這樣:當用戶訪問/search/並未提交任何數據時就隱藏錯誤信息,這樣就可以用一個視圖函數實現上面的全部功能了。在改進后的視圖中,若用戶訪問/search/並且沒有帶有GET數據,那么他將看到一個沒有錯誤信息的表單; 如果用戶提交了一個空表單,那么它將看到錯誤提示信息和表單; 最后,若用戶提交了一個非空的值,那么他將看到搜索結果。
def search(request): error=False if 'q' in request.GET: q = request.GET['q'] if not q: error = True else: items = Item.objects.filter(ItemName__icontains=q) return render_to_response('search_results.html', {'items': items, 'query': q}) return render_to_response('search_form.html',{'error':error})
6.4. 簡單的驗證
實際的項目中我們還可以利用JavaScript在客戶端進行必要的數據驗證,即使這樣,在服務器端仍必須再驗證一次,來避免任何可能的非法數據提交。
我們進一步調整search()視圖,讓它能夠驗證搜索關鍵詞是否小於或等於10個字符:
def search(request): error=False if 'q' in request.GET: q = request.GET['q'] if not q: error = True elif len(q) > 10: error = True else: items = Item.objects.filter(ItemName__icontains=q) return render_to_response('search_results.html', {'items': items, 'query': q}) return render_to_response('search_form.html',{'error':error})
現在,如果嘗試着提交一個超過20個字符的搜索關鍵詞,系統不會執行搜索操作,而是顯示一條錯誤提示信息。
關於這個表單提示信息更詳細的優化方案,請參考<the django book>。
6.5. 編寫入庫單表單
現在我們延續前面庫存的例子來處理另一個稍微復雜一點的表單,新增一個入庫單並把數據提交到后台數據庫。
首先,在inventory/forms增加InStockAdd.html模板文件,結構如下:
<html> <head> <title>Add In Stock Bill</title> </head> <body> <h1>Add In Stock Bill</h1> {% if errors %} <ul> {% for error in errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <form action="/AddInStockBill/" method="post" > <p>入庫單編號: <input type="text" name="InStockBillCode"></p> <p>入庫時間: <input type="text" name="InStockDate"></p> <p>操作員: <input type="text" name="Operator"></p> <p>物料Id: <input type="text" name="ItemId"></p> <p>數量: <input type="text" name="Amount"></p> <input type="submit" value="Submit"> </form> </body> </html>
注意:這里模板文本文件保存的,編碼格式選擇為UTF-8,否則后面會出現中文解碼錯誤的提示。

模板文件我們已經入庫單模型定義了五個字段: 物料編碼、物料id、數量、操作員和入庫時間。注意,這里我們使用method=”post”而非method=”get”,因為這個表單會修改(寫)服務器端數據:在入庫單表中增加一條入庫單據記錄。
如果我們順着上一節編寫search()視圖的思路,那么一個AddInStockBill ()視圖代碼應該像這樣:
def AddInStockBill(request): errors = [] if request.method == 'POST': if not request.POST.get('InStockBillCode', ''): errors.append('Enter a In Stock Bill Code.') if not request.POST.get('InStockDate', ''): errors.append('Enter a In Stock Date.') if not request.POST.get('Amount',''): errors.append('Enter a Amount.') if not request.POST.get('Operator',''): errors.append('Enter a Operator.') if not errors: return HttpResponseRedirect('/success/') return render_to_response('InStockAdd.html',{'errors': errors}) def success(request): return HttpResponse('success')
上面的代碼如果表單提交的數據校驗正常,我們將直接重定向到一個成功提示頁面,否則返回錯誤提示讓用戶重新填寫數據。對於Post表單提交成功后重新向到另一個頁面主要是為了避免用戶通過點擊刷新一個包含POST表單的頁面,請求將會重新發送造成數據重復提交后台服務端,出現重復業務數據等。比如:在我們的例子中,將導致數據庫中有兩條相同的業務入庫單據。如果用戶在POST表單之后被重定向至另外的頁面,就不會造成重復的請求了。
把每次都給成功的POST請求做重定向,這就是web開發的最佳實踐之一。
urls.py文件增加url如下:
urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite.views.home', name='home'), # url(r'^mysite/', include('mysite.foo.urls')), # Uncomment the admin/doc line below to enable admin documentation: # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: url(r'^admin/', include(admin.site.urls)), url(r'^helloworld/$', helloworld), url(r'^mytime/$', current_datetime), url(r'^search/$', views.search), url(r'^AddInStockBill/$', views.AddInStockBill), url(r'^success/$', views.success), )
下圖是入庫單運行的效果如下:

這里,當我們點擊提交按鈕提交數據時,會出現如下錯誤:
Forbidden (403)
CSRF verification failed. Request aborted.
解決辦法如下:
1) 第一步在模板文件的表單里增加 {% csrf_token %} 標識
<html> <head> <title>Add In Stock Bill</title> </head> <body> <h1>Add In Stock Bill</h1> {% if errors %} <ul> {% for error in errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <form action="/AddInStockBill/" method="post" > {% csrf_token %} <p>入庫單編號: <input type="text" name="InStockBillCode"></p> <p>入庫時間: <input type="text" name="InStockDate"></p> <p>操作員: <input type="text" name="Operator"></p> <p>物料Id: <input type="text" name="ItemId"></p> <p>數量: <input type="text" name="Amount"></p> <input type="submit" value="Submit"> </form> </body> </html>
2) 第二步,在視圖里引入RequestContext,並在AddInStockBill()函數的render_to_response里使用RequestContext。
def AddInStockBill(request): errors = [] if request.method == 'POST': if not request.POST.get('InStockBillCode', ''): errors.append('Enter a In Stock Bill Code.') if not request.POST.get('InStockDate', ''): errors.append('Enter a In Stock Date.') if not request.POST.get('Amount',''): errors.append('Enter a Amount.') if not request.POST.get('Operator',''): errors.append('Enter a Operator.') if not request.POST.get('ItemId',''): errors.append('Enter a Item.') if not errors: return HttpResponseRedirect('/success/') return render_to_response('InStockAdd.html',{'errors': errors}, ,context_instance = RequestContext(request))
由於AddInStockBill視圖函數里,獲取表單數據校驗后沒有錯誤后,我們就直接返回了一個success提示,接下來我們修改視圖AddInStockBill函數把入庫單業務數據保存到數據庫中。
def AddInStockBill(request): errors = [] if request.method == 'POST': if not request.POST.get('InStockBillCode', ''): errors.append('Enter a In Stock Bill Code.') if not request.POST.get('InStockDate', ''): errors.append('Enter a In Stock Date.') if not request.POST.get('InStockDate', ''): errors.append('Enter a In Stock Date.') if not request.POST.get('Amount',''): errors.append('Enter a Amount.') if not request.POST.get('Operator',''): errors.append('Enter a Operator.') if not request.POST.get('ItemId',''): errors.append('Enter a Item.') if not errors: inStockBill = InStockBill() inStockBill.InStockBillCode = request.POST.get('InStockBillCode', '') inStockBill.InStockDate = request.POST.get('InStockDate', '') inStockBill.Amount = request.POST.get('Amount','') inStockBill.Operator = request.POST.get('Operator','') itemId = request.POST.get('ItemId','') inStockBill.Item = Item.objects.get(ItemId= itemId) #注意物料需要保持Model對象 inStockBill.save() return HttpResponseRedirect('/success/') return render_to_response('InStockAdd.html',{'errors': errors} ,context_instance = RequestContext(request))
重新填寫入庫單數據點擊提交,數據就正常保存到數據庫里了。

6.5.1. 優化表單,添加下拉列表
目前為止我們的表單中的物料id是人工直接錄入的,實際項目中應該只能錄入數據庫中已經維護的物料數據,這里優化一下模板文件修改為下拉列表,能選擇數據庫中已經有的物料數據。
<html> <head> <title>Add In Stock Bill</title> </head> <body> <h1>Add In Stock Bill</h1> {% if errors %} <ul> {% for error in errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <form action="/AddInStockBill/" method="post" > {% csrf_token %} <p>入庫單編號: <input type="text" name="InStockBillCode"></p> <p>入庫時間: <input type="text" name="InStockDate"></p> <p>操作員: <input type="text" name="Operator"></p> <p>物料: <select name="ItemId"> {% if items %} {% for item in items %} <option value={{ item.ItemId }}>{{ item.ItemName }}</option> {% endfor %} {% endif %} </select></p> <p>數量: <input type="text" name="Amount"></p> <input type="submit" value="Submit"> </form> </body> </html>
view.py的AddInStockBill函數修改如下:
def AddInStockBill(request): errors = [] items = Item.objects.all() if request.method == 'POST': if not request.POST.get('InStockBillCode', ''): errors.append('Enter a In Stock Bill Code.') if not request.POST.get('InStockDate', ''): errors.append('Enter a In Stock Date.') if not request.POST.get('Amount',''): errors.append('Enter a Amount.') if not request.POST.get('Operator',''): errors.append('Enter a Operator.') if not request.POST.get('ItemId',''): errors.append('Enter a Item.') if not errors: inStockBill = InStockBill() inStockBill.InStockBillCode = request.POST.get('InStockBillCode', '') inStockBill.InStockDate = request.POST.get('InStockDate', '') inStockBill.Amount = request.POST.get('Amount','') inStockBill.Operator = request.POST.get('Operator','') itemId = request.POST.get('ItemId','') inStockBill.Item = Item.objects.get(ItemId= itemId) #注意物料需要保持Model對象 inStockBill.save() return HttpResponseRedirect('/success/') return render_to_response('InStockAdd.html',{'errors': errors,'items':items} ,context_instance = RequestContext(request))
6.6. 錯誤提示及表單數據回填
表單的重新顯示。數據驗證失敗后,返回客戶端的表單中各字段應該有原來用戶填好的數據,便於用戶查看錯誤提示的同時,用戶不需再次填寫已經填寫正確的字段值,否則如果是數據量比較大的表單,要用戶重新錄入幾乎是不實際的。下面我們把代碼改成下面這樣來實現這一功能(不需要再次選擇已經選擇過的下拉列表物料,筆者在做的時候也遇到了點問題,這個例子里模板的ifequal對數據類型比較敏感。
InStockAdd.html模板文件:
<html> <head> <title>Add In Stock Bill</title> </head> <body> <h1>Add In Stock Bill</h1> {% if errors %} <ul> {% for error in errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <form action="/AddInStockBill/" method="post" > {% csrf_token %} <p>入庫單編號: <input type="text" name="InStockBillCode" value="{{ InStockBillCode }}" ></p> <p>入庫時間: <input type="text" name="InStockDate" value="{{ InStockDate }}" ></p> <p>操作員: <input type="text" name="Operator" value="{{ Operator }}" ></p> <p>物料: <select name="ItemId"> {% if items %} {% for item in items %} {% ifequal item.ItemId ItemId %} <option value={{ item.ItemId }} selected>{{ item.ItemName }}</option> {% else %} <option value={{ item.ItemId }} >{{ item.ItemName }}</option> {% endifequal %} {% endfor %} {% endif %} </select></p> <p>數量: <input type="text" name="Amount" value="{{ Amount }}"></p> <input type="submit" value="Submit"> </form> </body> </html>
view.py AddInStockBill()代碼調整如下:
def AddInStockBill(request): errors = [] items = Item.objects.all() ItemId = '' if request.method == 'POST': if not request.POST.get('InStockBillCode', ''): errors.append('Enter a In Stock Bill Code.') if not request.POST.get('InStockDate', ''): errors.append('Enter a In Stock Date.') if not request.POST.get('Amount',''): errors.append('Enter a Amount.') if not request.POST.get('Operator',''): errors.append('Enter a Operator.') if not request.POST.get('ItemId',''): errors.append('Enter a Item.') else: ItemId = int( request.POST.get('ItemId','')) if not errors: inStockBill = InStockBill() inStockBill.InStockBillCode = request.POST.get('InStockBillCode', '') inStockBill.InStockDate = request.POST.get('InStockDate', '') inStockBill.Amount = request.POST.get('Amount','') inStockBill.Operator = request.POST.get('Operator','') itemId = request.POST.get('ItemId','') inStockBill.Item = Item.objects.get(ItemId= itemId) #注意物料需要保持Model對象 inStockBill.save() return HttpResponseRedirect('/success/') return render_to_response('InStockAdd.html',{'errors': errors,'items':items, 'InStockBillCode':request.POST.get('InStockBillCode', ''), 'InStockDate':request.POST.get('InStockDate', ''), 'Amount': request.POST.get('Amount', ''), 'ItemId': ItemId, 'Operator': request.POST.get('Operator', ''),}
,context_instance = RequestContext(request))
6.7. 小結
本章我們實現了一個簡單的表單的例子來說明數據是如何通過前段頁面代碼與Django模型一起組合,演示了如何把頁面業務數據如何通過服務端持久化到數據庫中。
下一章我們將再簡要介紹Django的form類。

