Python开发入门与实战6-表单
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类。
Python开发入门与实战6-表单的更多相关文章
- Python开发入门与实战12-业务逻辑层
12. Biz业务层 前面的章节我们把大量的业务函数都放在了views.py里,按照目前这一的写法,当我们编写的系统复杂较高时,我们的views.py将会越来越复杂,大量的业务函数包含其中使其成为一个 ...
- Python开发入门与实战7-Django Form
7. Django Form 7.1. Form表单 Django带有一个form库,称为django.forms,这个库可以处理上一章提到的包括HTML表单的自动生成以及数据验证. 我们在inven ...
- Python开发入门与实战5-django模型
5.Django模型 在当今的Web 应用中,主观逻辑经常牵涉到与数据库的交互,数据库驱动网站.在后台连接数据库服务器,从中取出一些数据,然后在 Web 页面用各种各样的格式展示这些数据.这个网站也可 ...
- Python开发入门与实战11-单元测试
11. 单元测试 本章节我们来讲讲django工程中如何实现单元测试,单元测试如何编写以及在可持续项目中单元测试的重要性. 下面是单元测试的定义: 单元测试是开发者编写的一小段代码,用于检验被测代码的 ...
- Python开发入门与实战8-基于Java的集成开发环境
8. 基于Java的Python的集成开发环境 目前为止我们所有的代码和例子都是通过Notepad文本编辑器来实现的,实际项目开发中这种编码模式效率较低(大虾除外),使用IDE集成开发环境常常大幅度的 ...
- Python开发入门与实战1-开发环境
1.搭建Python Django开发环境 1.1.Python运行环境安装 Python官网:http://www.python.org/ Python最新源码,二进制文档,新闻资讯等可以在Pyth ...
- Python开发入门与实战22-简单消息回复
22. 简单消息回复 本章节我们来实现一个微信库存查询功能,使用我们前面的BIZ业务逻辑层示例如何利用微信入口来实现文本消息类的库存查询服务. 22.1. 在responseMsg函数里增加处理微信文 ...
- Python开发入门与实战10-事务
1. 事务 本章我们将通过一个例子来简要的说明“事务”,这个开发实战里经常遇到的名词.事务是如何体现在一个具体的业务和系统的实现里. 事务是通过将一组相关操作组合为一个,要么全部成功要么全部失败的单元 ...
- Python开发入门与实战17-新浪云部署
17. 新浪云部署 上一章节我们介绍了如何在本地windows服务器部署python django的网站,本章我们简要说明一下如何把python django工程部署到云服务上. 本章章节我们描述如何 ...
随机推荐
- 161214、oracle查询表信息
1.查找表的所有索引(包括索引名,类型,构成列): select t.*,i.index_type from user_ind_columns t,user_indexes i where t.ind ...
- ifconfig
虚拟机启动后发现ifconfig命令下,没有打印ip .用ifconfig eth0 up命令开启下网卡. #ifconfig eth0 up .更改eth0配置: #vi/etc/sysconfig ...
- html里面的相对路径和绝对路径
HTML有两种路径的写法:相对路径和绝对路径. 相对路径是在同一个目录的文件引用,如果源文件和引用文件在同一个目录里,直接写引用文件名即可. 如: a.html: c:\test\menu\a ...
- UA模拟
安卓QQ内置浏览器UA: Mozilla/5.0 (Linux; Android 5.0; SM-N9100 Build/LRX21V) AppleWebKit/537.36 (KHTML, like ...
- libevent源码分析:bufferevent
struct bufferevent定义在文件bufferevent_struct.h中. /** Shared implementation of a bufferevent. This type ...
- Linux Shell 批量更换文件名或后缀名
把下列所有.c的文件名修改为.cc rename .c .cc *.c
- Hibernate <查询缓存>
查询缓存: 定义:查询缓存它是基于二级缓存的,可以保存普通属性查询的结果,查询对象实体时,他会保存id作为键,查询结果作为值,下个对象访问时,可以直接查到 查询缓存查询实体对象时,显著的特点是,会执行 ...
- 【HDU5955】Guessing the Dice Roll/马尔科夫
先从阿里机器学习算法岗网络笔试题说起:甲乙两人进行一个猜硬币的游戏.每个人有一个目标序列,由裁判来抛硬币.谁先得到裁判抛出的一串连续结果,谁赢. 甲的目标序列是正正正,乙的目标序列是反正正.那么如果裁 ...
- java高薪之路__004_泛型
参考地址: 1. http://www.cnblogs.com/lwbqqyumidi/p/3837629.html2. http://www.cnblogs.com/abcwt112/p/47350 ...
- ios 模拟器不显示系统版本了,后边都是 uuid 了,怎么弄回来?系统升级xcode6.4,模拟器找不到选择了?
当我用El Capitan Beta 下 Xcode6.4版本时候出现了问题 常用的Scheme 选择版本不见了 而在Xcode 7.0 beta 6中显示有 简直就是坑,经过查资料其实是一个bug ...
