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类。