翻译:The Django Book (第7章:表单处理)
第7章:表单处理客座作者:Simon Willison
跟随上一章走到现在,你现在应该有一个拥有完整功能的简单网站了。在本章,我们将处理这个谜题的下一个部分:构建视图来获取读者的输入。
我们将从“手动”制作一个简单的搜索表单开始来看看如何处理从浏览器提交的数据。从那里,我们将转到使用Django的表单框架。
搜索
Web中到处存在搜索。网络中两个最大的成功故事,Google和Yahoo,围绕着搜索构建了他们几十亿美元的业务。几乎每个网站的很大比例的进出流量都来自于它们的搜索页面。通常在一个站点的成功与失败之间的区别是它们的搜索的质量。所以看起来我们最好给我们的初级书籍站点添加搜索,不是么?
我们将从给我们的URLconf(mysite.urls)添加搜索视图开始。记住这意味着要在URL模式集合中添加某些类似(r'^search/$', 'mysite.books.views.search')的东西。
接下来,我们将把这个搜索视图写入到我们的模块中(mysite.books.views):
from django.db.models import Q
from django.shortcuts import render_to_response
from models import Book
def search(request):
query = request.GET.get('q', '')
if query:
qset = (
Q(title__icontains=query) |
Q(authors__first_name__icontains=query) |
Q(authors__last_name__icontains=query)
)
results = Book.objects.filter(qset).distinct()
else:
results = []
return render_to_response("books/search.html", {
"results": results,
"query": query
})
这里有几件事情是你还没有见到过的。首先,有request.GET。这是你从Django中访问GET数据的方法;POST数据是通过一个类似的request.POST对象来访问的。这些对象的行为和标准的Python字典及其相似,一些额外的特性将在附录H介绍。
什么是GET和POST数据?
GET和POST是浏览器用来发送数据到服务器的两个方法。多数时间,你将在HTML表单标签中见到它们:
<form action="/books/search/" method="get">
这告诉浏览器使用GET方法将表单数据提交给URL /books/search/。
GET和POST方法在语义上有重要的区别,我们现在不会讨论,不过如果你想学习更多的内容请参看[url]http://www.w3.org/2001/tag/doc/whenToUseGet.html[/url]。
所以这行代码:
query = request.GET.get('q', '')
查找GET方法的名为'q'的参数,如果这个参数没有被提交就返回一个空字符串。
注意我们正在对request.GET对象使用get()方法,有点隐晦。这里的get()方法是每一个Python字典都有的。我们在这里很小心地使用它:它是不安全的,它假定request.GET对象包含有一个'q'键,所以我们使用get('q','')来提供''(空字符串)作为默认返回值。如果我们只是使用request.GET['q']来访问这个变量,如果q在GET数据中不可用,这些代码将会引发一个KeyError异常。
第二,Q是什么?Q对象被用来构建复杂的查询——在这个例子中,我们搜索任何标题或者任何一位作者的名字匹配查询字符串的书籍。技术上讲,这些Q对象包含一个查询集合,你可以在附录C中获取更多信息。
在这些查询中,icontains是一种大小写不敏感的查询,在底层数据库中使用SQL 的LIKE操作符。
因为我们正在搜索一个多对多的域,很可能同一本书会在一次查询中被返回多次(例如,一本书的两个作者都匹配查询字符串)。在过滤器末尾添加.distinct()用来过滤重复的结果。
然而,现在还没有对应此搜索视图的模板。下面的代码将做这件事:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<title>Search{% if query %} Results{% endif %}</title>
</head>
<body>
<h1>Search</h1>
<form action="." method="GET">
<label for="q">Search: </label>
<input type="text" name="q" value="{{ query|escape }}">
<input type="submit" value="Search">
</form>
{% if query %}
<h2>Results for "{{ query|escape }}":</h2>
{% if results %}
<ul>
{% for book in results %}
<li>{{ book|escape }}</l1>
{% endfor %}
</ul>
{% else %}
<p>No books found</p>
{% endif %}
{% endif %}
</body>
</html>
希望到现在为止一切都是显而易见的。不过,还是有一些细微之处值得指出:
* 表单的动作是.,意思是"当前URL"。这是一个标准的最佳实践:不要对表单页面和结果页面使用分开的单独的视图;使用同一个来处理表单和搜索结果。
* 我们将查询字符串的值重新插入<input>。这会帮助读者很容易地调整他们的搜索而不用重新输入他们的搜索。
* 在每个使用query和book的地方,我们都将它们传递通过escape过滤器来确保任何潜在的恶意搜索文本在插入页面之前都被过滤掉了。
对于任何用户提交的内容都使用它(escape过滤器)是极为必要的。否则你就让你的站点对跨站脚本攻击敞开了大门。第19章会详细讨论跨站脚本攻击(XSS)和安全。
* 不过,我们不需要为你的数据库查询中的有害内容担忧——我们只需简单地将查询字符串原样传递给查询过程。这是因为Django的数据库层为你处理了这方面的安全。
现在我们有了一个可以工作的搜索了。更进一步的改进是把搜索表单添加到每一个页面(例如,在基本模板中);我们会留给你自己来处理。
接下来,我们会看一个更复杂的例子。但是在开市之前,让我们讨论一个更抽象的话题:“最好的表单。”
“最好的表单”
表单常常是让你的网站用户受挫的主要原因。让我们考虑一个理想中最好的表单的行为:
* 显然,它应该询问用户一些信息。易用性和可用性在这里是很重要的,所以灵活使用HTML的<label>元素和条理清晰的帮助信息是很重要的。
* 提交的数据应该被施以充分的验证。Web应用安全的金科玉律是“永远不要相信输入数据”,所以验证是必不可少的。
* 如果用户翻了任何错误,表单应该给出详细的提示性的错误信息并重新显示。原来的数据应该预先填好,以避免用户逐一重新输入所有东西。
* 表单应该一直重新显示,直到表单中所有的域都被正确地填充了。
构建最好的表单看起来好像是大量的工作!谢天谢地,Django的表单框架被设计为替你处理大部分的工作。你只需提供表单域的描述,验证规则,和一个简单的模板,其余的Django会处理。结果是只需很少的努力便得到“最好的表单”。
创建一个反馈表单
构建一个人们喜爱的站点的最好途径是聆听他们的反馈。许多站点好像忘记了这一点;他们把他们的详细联系方式隐藏到FAQ之后,并且看起来他们是让与真人接触尽可能地难。
当你的站点有数以百万计的用户,这样的战略或许是合理的。可是,当你试图建立起一个用户群的时候,你应该尽可能地抓住每一个机会鼓励反馈信息。让我们建立一个简单的反馈表单并用它来实际地说明Django的表单框架。
我们将从添加(r'^contact/$', 'mysite.books.views.contact')到我们的URLconf开始,然后定义我们的表单。表单在Django中的创建方式类似于模型:声明,使用一个Python类。这里是我们的简单的表单的类。按照惯例,我们将把它插入到我们的应用目录中的一个新文件forms.py:
from django import newforms as forms
TOPIC_CHOICES = (
('general', 'General enquiry'),
('bug', 'Bug report'),
('suggestion', 'Suggestion'),
)
class ContactForm(forms.Form):
topic = forms.ChoiceField(choices=TOPIC_CHOICES)
message = forms.CharField()
sender = forms.EmailField(required=False)
“新”表单?什么?
当Django第一次公开发布时,它有一个复杂的,难以理解的表单系统。它使得产生表单非常困难,所以它被完全地重写了,现在被叫做"newforms"。不过,仍然有大量代码是基于“旧”表单系统的,所以到现在为止Django在两种表单包之间过渡。
到本书写作时为止,Django的旧表单系统仍然作为django.forms可用,新表单包是django.newforms。某些地方改变了并且 django.forms将会指向新的表单包。不过,为了确保本书中的例子能够尽可能广泛地工作,所有的例子会引用django.newforms。
Django的表单是django.newforms.Form的子类,就像Django模型是django.db.models.Model的子类。django.newforms模块也包含一些Field类;完整的列表在Django的文章网站[url]http://www.djangoproject.com/documentation/0.96/newforms/[/url]可用。
我们的ContactForm由3个域组成:话题,从三个选项中选择;消息,是一个字符域;发送者,是一个电子邮件域并且是可选的(因为即使是匿名的用户也是有用的)。还有其他的一些域类型可以使用,并且如果它们不满足你的需要你可以编写你自己的类型。
表单对象本身知道怎样去做一些有用的事情。它可以验证一个数据的收集,它可以生成自己的HTML"小部件",它可以构建一个有用的出错信息的集合,并且,如果我们比较懒,它还可以为我们绘制整个表单。让我们把它挂接到一个视图来实际地看看它。在views.py中:
from django.db.models import Q
from django.shortcuts import render_to_response
from models import Book
from forms import ContactForm
def search(request):
query = request.GET.get('q', '')
if query:
qset = (
Q(title__icontains=query) |
Q(authors__first_name__icontains=query) |
Q(authors__last_name__icontains=query)
)
results = Book.objects.filter(qset).distinct()
else:
results = []
return render_to_response("books/search.html", {
"results": results,
"query": query
})
def contact(request):
form = ContactForm()
return render_to_response('contact.html', {'form': form})
并且在contact.html中:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<title>Contact us</title>
</head>
<body>
<h1>Contact us</h1>
<form action="." method="POST">
<table>
{{ form.as_table }}
</table>
<p><input type="submit" value="Submit"></p>
</form>
</body>
</html>
这里最有趣的一行是{{ form.as_table }}。form是我们的ContactForm实例,传递到render_to_response。as_table是一个将表单渲染为表格的行序列的方法(as_ul和as_p也可以被使用)。生成的HTML看起来是这样的:
<tr>
<th><label for="id_topic">Topic:</label></th>
<td>
<select name="topic" id="id_topic">
<option value="general">General enquiry</option>
<option value="bug">Bug report</option>
<option value="suggestion">Suggestion</option>
</select>
</td>
</tr>
<tr>
<th><label for="id_message">Message:</label></th>
<td><input type="text" name="message" id="id_message" /></td>
</tr>
<tr>
<th><label for="id_sender">Sender:</label></th>
<td><input type="text" name="sender" id="id_sender" /></td>
</tr>
注意<table>和<form>标签并没有被包含;你需要在模板中自己定义它们,给你了自由来控制当表单被提交的时候的行为。<label>元素被包含,使得表单在盒子之外也可以被访问。
我们的表单当前为消息域使用<input type="text">小部件。我们不想限制我们的用户只能输入一行文本,所以我们将使用<textarea>小部件来代替:
class ContactForm(forms.Form):
topic = forms.ChoiceField(choices=TOPIC_CHOICES)
message = forms.CharField(widget=forms.Textarea())
sender = forms.EmailField(required=False)
表单框架把每个域的表现逻辑分为一个小部件的集合。每个域类型都有一个默认的小部件,但是你可以很容易地覆盖默认值,或者提供一个你自己定制的小部件。
现在,提交这个表单实际上不会做任何事情。让我们添加自己的验证规则:
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
else:
form = ContactForm()
return render_to_response('contact.html', {'form': form})
一个表单实例可以处在两种状态之一:绑定的和非绑定的。一个绑定的实例是使用字典(或者类似字典的对象)构造并且知道怎样验证和从它们重新显示数据。一个非绑定的表单没有数据和它绑定,它仅仅知道如何显示它自己。
尝试输入一个无效的电子邮件地址。EmailField知道如何验证电子邮件地址,至少在一个合理的可接受的水平上。
设置初始数据
直接把数据传递给表单构造器会绑定这个数据并指示应该进行验证。不过,通常我们需要显示一个一些域被预填的初始表单——例如,一个“编辑”表单。我们可以通过initial关键字参数来做这个:
form = CommentForm(initial={'sender': 'user@example.com'})
如果我们的表单将总是使用相同的默认值,我们可以在表单的定义中配置它们:
message = forms.CharField(widget=forms.Textarea(),
initial="Replace with your feedback")
处理提交
一旦用户填写了表单并且它们通过了我们的验证规则,我们需要用这些数据来做一些有用的事。在这个例子里,我们想要构建并发送一封包含用户反馈的电子邮件。我们将使用Django的email包来做这些。
首先,我们需要知道数据是否确实被验证了,如果是,我们需要访问经过验证的数据。表单框架做了比验证数据更多的事,它也把数据转换为Python的类型。我们的联系人表单仅处理字符串,但是如果我们使用了IntegerField 或者 DateTimeField,表单框架会确保我们得到一个Python的整数或者datetime对象。
为知道表单是否被绑定验证数据,调用is_vaild()方法:
form = ContactForm(request.POST)
if form.is_valid():
# Process form data
现在我们需要访问数据。我们可以直接从request.POST取出它,但是如果我们这样做,我们会错过由表单框架实施的类型转换。取而代之,我们使用form.clean_fata:
if form.is_valid():
topic = form.clean_data['topic']
message = form.clean_data['message']
sender = form.clean_data.get('sender', 'noreply@example.com')
# ...
注意因为发送人域并不是必需的,当它缺失时我们提供了一个默认值。最后,我们需要记录用户的反馈。做到这一点最简单的途径是用电子邮件把它发送给站点管理员。我们可以使用send_mail函数来做这件事:
from django.core.mail import send_mail
# ...
send_mail(
'Feedback from your site, topic: %s' % topic,
message, sender,
['administrator@example.com']
)
send_mail函数有四个必需的参数:邮件标题,邮件体,"from"地址,和一个接收者列表。send_mail是Django的EmailMessage类的一个便利的包装形式,EmailMessage提供高级的特性如附件、多块电子邮件、和电子邮件头部的完全控制。
发送了电子邮件之后,我们应该将我们的用户重定向到一个静态的确认页面。完成后的视图函数看起来是这样的:
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.core.mail import send_mail
from forms import ContactForm
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
topic = form.clean_data['topic']
message = form.clean_data['message']
sender = form.clean_data.get('sender', 'noreply@example.com')
send_mail(
'Feedback from your site, topic: %s' % topic,
message, sender,
['administrator@example.com']
)
return HttpResponseRedirect('/contact/thanks/')
else:
form = ContactForm()
return render_to_response('contact.html', {'form': form})
POST之后重定向
如果用户在页面被POST请求显示过之后选择刷新页面,那么那个请求会被重复。这通常会导致不期望的行为,如一个重复的记录被添加到数据库中。在POST 之后重定向是避免此种情况的有用的模式:在成功执行一个POST请求之后,将用户重定向到另一个页面而不是直接返回HTML。
自定义验证规则
想想我们已经建立了反馈表单,并且电子邮件也已经嵌入了。只有一个问题:某些邮件只有一或者两个词语,并不能形成包含详细信息的邮件。我们准备实施一个新的验证规则:4个词语或者更多,谢谢。
有很多种方法在Django表单中嵌入自定义验证规则。如果我们的规则是要一次又一次被重复使用的,我们可以创建一个自定义的域类型。大多数的自定义验证规则是一次性的,因此,可以直接绑定在表单类中。
我们想要在message域添加验证规则,因此我们需要给我们的表单添加一个clean_message方法:
class ContactForm(forms.Form):
topic = forms.ChoiceField(choices=TOPIC_CHOICES)
message = forms.CharField(widget=forms.Textarea())
sender = forms.EmailField(required=False)
def clean_message(self):
message = self.clean_data.get('message', '')
num_words = len(message.split())
if num_words < 4:
raise forms.ValidationError("Not enough words!")
return message
这个新的方法会在默认的域验证方法(在这个例子中是CharField的验证方法)之后被调用。因为域数据已经部分地被处理了,我们需要我们需要把它从表单的clean_data字典中取出来。
我们使用len()和split()组合来计算单词的数目。如果用户输入了太少的单词,我们就会抛出一个ValidationError异常。附加在这个异常的字符串会作为错误列表中的一个条目显示给用户。
在方法的末尾明确地返回出错域的值是十分重要的。这允许我们修改(或者把它转换为一个不同的Python类型)我们的自定义验证方法中的值。如果我们忘记了返回语句,None会被返回,原来的值就会丢失。
自定义外观和风格
自定义表单外观最快速的途径是使用CSS。错误列表尤其可以使用一些视觉增强在定制外观,<ul>标签有一个errorlist类属性专门用来做这件事。接下来的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 }}和其类似的标签是我们开发应用时非常有用的捷径,但是有关表单显示的所有东西都可以被覆盖,大部分针对模板自身。
每一个域部件(<input type="text">, <select>, <textarea>,或类似的东西)都可以通过访问{{ form.fieldname }}独立地渲染。与域绑定的错误通过{{ form.fieldname.errors }}访问。我们可以使用这些表单变量为我们的联系人表单构建一个自定义的模板:
<form action="." method="POST">
<div class="fieldWrapper">
{{ form.topic.errors }}
<label for="id_topic">Kind of feedback:</label>
{{ form.topic }}
</div>
<div class="fieldWrapper">
{{ form.message.errors }}
<label for="id_message">Your message:</label>
{{ form.message }}
</div>
<div class="fieldWrapper">
{{ form.sender.errors }}
<label for="id_sender">Your email (optional):</label>
{{ form.sender }}
</div>
<p><input type="submit" value="Submit"></p>
</form>
{{ form.message.errors }}将被作为<ul class="errorlist">显示,如果错误存在并且一个空白字符串如果域是有效的(或者表单时无界的)。我们也可以把form.message.errors当作一个布尔变量来处理,或者甚至把它作为一个列表来迭代,例如:
<div class="fieldWrapper{% if form.message.errors %} errors{% endif %}">
{% if form.message.errors %}
<ol>
{% for error in form.message.errors %}
<li><strong>{{ error|escape }}</strong></li>
{% endfor %}
</ol>
{% endif %}
{{ form.message }}
</div>
如果发生验证错误,这将会在<div>容器中添加一个"errors"类并且显示一个排序的错误列表。
从模型创建表单
让我们创建一些更有趣的东西:一个提交一个新的出版社到我们第5章中的book应用的新出版社。
Django在这里试图遵循的软件开发中的一个重要的经验规则是不要重复你自己(DRY)。Andy Hunt 和 Dave Thomas 在 Pragmatic Programmer 对此作了如下定义:
每一块知识,都必须有一个单一的、明确的、权威的表现体系。
我们的Publisher模型告诉我们一个出版社有一个名字,地址,省或者州,国家,和网站。在表单定义中重复这些信息会打破DRY规则。取而代之,我们可以使用一个有用的shortcut:form_for_model():
from models import Publisher
from django.newforms import form_for_model
PublisherForm = form_for_model(Publisher)
PublisherForm是一个Form子类,如同我们在前面手动创建的ContactForm类。我们可以以同样的方式使用它:
from forms import PublisherForm
def add_publisher(request):
if request.method == 'POST':
form = PublisherForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect('/add_publisher/thanks/')
else:
form = PublisherForm()
return render_to_response('books/add_publisher.html', {'form': form})
add_publisher.html文件和我们原来的contact.html模板几乎是一模一样的,所以在这里忽略了。还要记得在URLconf中添加一个新的模式:
(r'^add_publisher/$', 'mysite.books.views.add_publisher').
这里展示了另一个shortcut。因为由模型生成的表单通常使用来保存新的模型实例到数据库中的,由form_for_model创建的表单类包含一个方便的save()方法。它处理了通常的情形;如果你想做一些与提交的数据密切相关的事情,你可以忽略它。
form_for_instance()是一个从一个模型实例创建预填写的表单相关的方法。这对创建“编辑”表单很有用。
接下来是什么?
本章是本书的引导部分的终结。接下来的13章处理各类高级主题,包括生成非HTML内容(第11章),安全(第19章),和部署(第20章)。
在前面的7章之后,你应该已经知道了足够多的东西来写你自己的Django项目。本书中剩余的资料会填补你需要的缺失的部分。
我们会从第8章开始,回头更仔细地看看视图和URLconf(在第3章首次介绍)。
页:
[1]