第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方法在语义上有重要的区别,我们现在不会讨论,不过如果你想学习更多的内容请参看http://www.w3.org/2001/tag/doc/whenToUseGet.html。
所以这行代码:
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>Searchh1> <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 foundp> {% endif %} {% endif %} body> html>
希望到现在为止一切都是显而易见的。不过,还是有一些细微之处值得指出:
- 表单的动作是.,意思是"当前URL"。这是一个标准的最佳实践:不要对表单页面和结果页面使用分开的单独的视图;使用同一个来处理表单和搜索结果。
- 我们将查询字符串的值重新插入。这会帮助读者很容易地调整他们的搜索而不用重新输入他们的搜索。
- 在每个使用query和book的地方,我们都将它们传递通过escape过滤器来确保任何潜在的恶意搜索文本在插入页面之前都被过滤掉了。
对于任何用户提交的内容都使用它(escape过滤器)是极为必要的。否则你就让你的站点对跨站脚本攻击敞开了大门。第19章会详细讨论跨站脚本攻击(XSS)和安全。
- 不过,我们不需要为你的数据库查询中的有害内容担忧——我们只需简单地将查询字符串原样传递给查询过程。这是因为Django的数据库层为你处理了这方面的安全。
现在我们有了一个可以工作的搜索了。更进一步的改进是把搜索表单添加到每一个页面(例如,在基本模板中);我们会留给你自己来处理。
接下来,我们会看一个更复杂的例子。但是在开市之前,让我们讨论一个更抽象的话题:“最好的表单。”
“最好的表单”
表单常常是让你的网站用户受挫的主要原因。让我们考虑一个理想中最好的表单的行为:
- 显然,它应该询问用户一些信息。易用性和可用性在这里是很重要的,所以灵活使用HTML的
- 提交的数据应该被施以充分的验证。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的文章网站http://www.djangoproject.com/documentation/0.96/newforms/可用。
我们的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 ustitle> head> <body> <h1>Contact ush1> <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 enquiryoption> <option value="bug">Bug reportoption> <option value="suggestion">Suggestionoption> 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>
注意
我们的表单当前为消息域使用小部件。我们不想限制我们的用户只能输入一行文本,所以我们将使用