第七章 表单处理
经过上一章,你应该对简单网站有个全面的认识。这一章,来处理 web 开发的下一个难题:建立用户输入的视图。
我们会从手工打造一个简单的搜索页面开始,看看怎样处理浏览器提交而来的数据。然后我们开始使用 Django 的 forms 框架。
搜索
在 web 应用上,有两个关于搜索获得巨大成功的故事:Google 和 Yahoo,通过搜索,他们建立了几 十亿美元的业务。几乎每个网站都有很大的比例访问量来自这两个搜索引擎。甚至,一个网站是否成功取决于其站内搜索的质量。因此,在我们这个网站添加搜索功 能看起来好一些。
开始,在 URLconf (mysite.urls )添加搜索视图。添加类似
(r'^search/$','mysite.books.views.search') 设置 URL 模式。
下一步,在视图模块(mysite.books.views )中写这个 search 视图: 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 and POST 数据?
GET 和 POST 是浏览器使用的两个方法,用于发送数据到服务器端。 一般来说,会在 html表单里面看到:
<form action="/books/search/" method="get">
它指示浏览器向/books/search/以 GET 的方法提交数据
关于GET 和POST 这两个方法之间有很大的不同,不过我们暂时不深入它,如果你想了解更多,可以访问: http://www.w3.org/2001/tag/doc/whenToUseGet.html 。
所以下面这行:
query = request.GET.get('q', '')
寻找名为 q 的 GET 参数,而且如果参数没有提交,返回一个空的字符串。
注意在 request.GET 中使用了 get() 方法,这可能让大家不好理解。这里的 get() 是每个
python 的的字典数据类型都有的方法。使用的时候要小心:假设 request.GET 包含一个 'q' 的 key 是不安全的,所以我们使用 get('q', '') 提供一个缺省的返回值 '' (一个空字符串)。如果只是使用 request.GET['q'] 访问变量,在 Get 数据时 q 不可得,可能引发 KeyError .
其次,关于 Q , Q 对象在这个例子里用于建立复杂的查询,搜索匹配查询的任何书籍.技术上
Q 对象包含 QuerySet,可以在附录 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>
希望你已经很清楚地明白这个实现。不过,有几个细节需要指出:
表单的 action 是 . , 表示当前的 URL。这是一个标准的最佳惯常处理方式:不使用独立 的视图分别来显示表单页面和结果页面;而是使用单个视图页面来处理表单并显示搜索结果。
我们把返回的查询值重新插入到 <input> 中去,以便于读者可以完善他们的搜索内容, 而不必重新输入搜索内容。
在所有使用 query 和 book 的地方,我们通过 escape 过滤器来确保任何 可能的恶意的搜索文字被过滤出去,以保证不被插入到页面里。
这对处理任何用户提交数据来说是 必须 的!否则的话你就开放你的网站允许跨站点脚本
(XSS)攻击。在第十九章中将详细讨论了 XSS 和安全。
不过,我们不必担心数据库对可能有危害内容的查询的处理。 Django 的数据库层在这方面已经做过安全处理。 【译注:数据库层对查询数据自动 Escape,所以不用担心】
现在我们已经作了搜索。进一步要把搜索表单加到所有的页面(例如,在 base 模板);这个可以由你自己完成。
下面,我们看一下更复杂的例子。事先我们讨论一个抽象的话题:完美表单。完美表单
表单经常引起站点用户的反感。我们考虑一下一个假设的完美的表单的行为:
它应该问用户一些信息,显然,由于可用性的问题, 使用 HTML <label> 元素和有用的 上下文帮助是很重要的。
所提交的数据应该多方面的验证。Web 应用安全的金科玉律是从不要相信进来的数据,所以验证是必需的。
如果用户有一些错误,表单应该重新显示详情,错误信息。原来的数据应该已经填好,避免用户重新录入,
表单应该在所有域验证正确前一直重新显示。
建立这样的表单好像需要做很多工作!幸好,Django 的表单框架已经设计的可以为你做绝大部分的工作。你只需要提供表单域的描述,验证规则和简单的模板即可。这样就只需要一点的工作就可以做成一个完美的表单。
创建一个回馈表单
做好一个网站需要注意用户的反馈,很多站点好像忘记这个。他们把联系信息放在 FAQ 后面,而且好像很难联系到实际的人。
一个百万用户级的网站,可能有些合理的策略。如果建立一个面向用户的站点,需要鼓励回馈。我们建立一个简单的回馈表单,用来展示 Django 的表单框架。
开始,在 URLconf 里添加 (r'^contact/$', 'mysite.books.views.contact') ,然后定义表单。 在 Django 中表单的创建类似 MODEL:使用 Python 类来声明。这里是我们简单表单的类。为了方便,把它写到新的 forms.py 文件中,这个文件在 app 目录下。
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)
New Forms 是什么?
当 Django 最初推出的时候,有一个复杂而难用的 form 系统。用它来构建表单简直就是噩梦,所以它在新版本里面被一个叫做 newforms 的系统取代了。但是鉴于还有很多代码依赖于老的那个 form 系统,暂时 Django 还是同时保有两个 forms 包。
在本书写作期间,Django 的老 form 系统还是在 django.forms 中,新的 form 系统位于
django.newforms 中。这种状况迟早会改变, django.forms 会指向新的 form 包。 但是为了让本书中的例子尽可能广泛地工作,所有的代码中仍然会使用 django.newforms 。
一个 Django 表单是 django.newforms.Form 的子类,就像 Django 模型是
django.db.models.Model 的子类一样。在 django.newforms 模块中还包含很多 Field 类;
Django 的文档( http://www.djangoproject.com/documentation/0.96/newforms/ )中包含了一个可用的 Field 列表。
我们的 ContactForm 包含三个字段:一个 topic,它是一个三选一的选择框;一个 message,它是一个文本域;还有一个 sender,它是一个可选的 email 域(因为即使是匿名反馈也是有用的)。还有很多字段类型可供选择,如果它们都不满足要求,你可以考虑自己写一个。
form 对象自己知道如何做一些有用的事情。它能校验数据集合,生成 HTML“部件”,生成一集有用的错误信息,当然,如果你确实很懒,它也能绘出整个 form。现在让我们把它嵌入一个视图,看看怎么样使用它。在 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 是 form 的一 个方法,它把 form 渲染成一系列的表格行(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>标签并没有包含在内;我们需要在模板里定义它们,这给予我们更大的控制权去决定 form 提交时的行为。Label 元素是包含在内的,令访问性更佳(因为 label的值会显示在页面上)。
我们的 form 现在使用了一个<input type=”text”>部件来显示 message 字段。但我们不想限制我们的用户只能输入一行文本,所以我们用一个<textarea>部件来替代:
class ContactForm(forms.Form):
topic = forms.ChoiceField(choices=TOPIC_CHOICES) message = forms.CharField(**widget=forms.Textarea()** ) sender = forms.EmailField(required=False)
forms 框架把每一个字段的显示逻辑分离到一组部件(widget)中。每一个字段类型都拥有一个默认的部件,我们也可以容易地替换掉默认的部件,或者提供一个自定义的部件。
现在,提交这个 form 没有在后台做任何事情。让我们把我们的校验规则加进去: def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST) else:
form = ContactForm()
return render_to_response('contact.html', {'form': form})
一个 form 实例可能处在两种状态:绑定或者未绑定。一个绑定的实例是由字典(或者类似于字典的对象)构造而来的,它同样也知道如何验证和重新显示它的数据。一个未绑定的 form是没有与之联系的数据,仅仅知道如何显示其自身。
现在可以试着提交一下这个空白的 form 了。页面将会被重新显示出来,显示一个验证错误,提示我们 message 字段是必须的。
现在输入一个不合法的 email 地址,EmailField 知道如何验证 email 地址,大多数情况下这种验证是合理的。
设置初始数据
向 form 的构造器函数直接传递数据会把这些数据绑定到 form,指示 form 进行验证。我们有时也需要在初始化的时候预先填充一些字段——比方说一个编辑 form。我们可以传入一些初始的关键字参数:
form = CommentForm(initial={'sender': 'user@example.com'})
如果我们的 form 总是会使用相同的默认值,我们可以在 form 自身的定义中设置它们 message = forms.CharField(widget=forms.Textarea(),
**initial="Replace with your feedback"** )
处理提交
当用户填完 form,完成了校验,我们需要做一些有用的事情了。在本例中,我们需要构造并发送一个包含了用户反馈的 email,我们将会使用 Django 的 email 包来完成
首先,我们需要知道用户数据是不是真的合法,如果是这样,我们就要访问已经验证过的数据。forms 框 架甚至做的更多,它会把它们转换成对应的 Python 类型。我们的联系方式 form仅仅处理字符串,但是如果我们使用 IntegerField 或者 DataTimeField,forms 框架会保证我们从中取得类型正确的值。
测试一个 form 是否已经绑定到合法的数据,使用 is_valid()方法: form = ContactForm(request.POST)
if form.is_valid():
# Process form data
现在我们要访问数据了。我们可以从 request.POST 里面直接把它们取出来,但是这样做我们就丧失了由 framework 为我们自动做类型转换的好处了。所以我们要使用 form.clean_data:
if form.is_valid():
topic = form.clean_data['topic'] message = form.clean_data['message']
sender = form.clean_data.get('sender', 'noreply@example.com')
# ...
请注意因为 sender 不是必需的,我们为它提供了一个默认值。终于,我们要记录下用户的反馈了,最简单的方法就是把它发送给站点管理员,我们可以使用 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']
else:
)
return HttpResponseRedirect('/contact/thanks/')
form = ContactForm()
return render_to_response('contact.html', {'form': form})在 POST 之后立即重定向
在一个 POST 请求过后,如果用户选择刷新页面,这个请求就重复提交了。这常常会导致我们不希望的行 为,比如重复的数据库记录。在 POST 之后重定向页面是一个有用的模式,可以避免这样的情况出现:在一个 POST 请求成功的处理之后,把用户导引到另外一 个页面上去,而不是直接返回 HTML 页面。
自定义校验规则
假设我们已经发布了反馈页面了,email 已经开始源源不断地涌入了。只有一个问题:一些
email 只有寥寥数语,很难从中得到什么详细有用的信息。所以我们决定增加一条新的校验:来点专业精神,最起码写四个字,拜托。
我们有很多的方法把我们的自定义校验挂在 Django 的 form 上。如果我们的规则会被一次又一次的使用,我们可以创建一个自定义的字段类型。大多数的自定义校验都是一次性的,可以直接绑定到 form 类.
我们希望 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 的校验器)。因为字段数据已经被部分地处理掉了,我们需要从 form 的 clean_data 字典中把它弄出来。
我们简单地使用了 len()和 split()的组合来计算单词的数量。如果用户输入了过少的词,我们扔出一个 ValidationError。这个 exception 的错误信息会被显示在错误列表里。
在函数的末尾显式地返回字段的值非常重要。我们可以在我们自定义的校验方法中修改它的值(或者把它转换成另一种 Python 类型)。如果我们忘记了这一步,None 值就会返回,原始的数据就丢失掉了。
自定义视感
修改 form 的显示的最快捷的方式是使用 CSS。错误的列表可以做一些视觉上的增强,<ul>标签的 class 属性为了这个目的。下面的 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>
虽然我们可以方便地使用 form 来生成 HTML,可是默认的渲染在多数情况下满足不了我们的应用。{{form.as_table}}和其它的方法在开发的时候是一个快捷的方式,form 的显示方式也可以在 form 中被方便地重写。
每一个字段部件(<input type=”text”>, <select>, <textarea>, 或者类似)都可以通过访问{{form.字段名}}进行单独的渲染。任何跟字段相关的错误都可以通过
{{form.fieldname.errors}} 访问。我们可以同这些 form 的变量来为我们的表单构造一个自定义的模板:
<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 没有被绑定,就显示一个空字符串。我们还可以把 form.message.errors 当作一个布尔值或者当它是 list 在上面做迭代:
<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 的 class 属性中增加一个”errors”,在一个有序列表中显示错误信息。
从模型创建表单
我们弄个有趣的东西吧:一个新的 form,提交一个新出版商的信息到我们第五章的 book 应用。
一个非常重要的 Django 的开发理念就是不要重复你自己(DRY)。Any Hunt 和 Dave Thomas 在
《实用主义程序员》里定义了这个原则:
在系统内部,每一条(领域相关的)知识的片断都必须有一个单独的,无歧义的,正式的表述。
我们的出版商模型拥有一个名字,地址,城市,州(省),国家和网站。在 form 中重复这个信息无疑违反了 DRY 原则。我们可以使用一个捷径: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') .
还有一个快捷的方法。因为从模型而来的表单经常被用来把新的模型的实例保存到数据库,从 form_for_model 而来的表单对象包含一个 save() 方法。一般情况下够用了;你想对提交的数据作进一步的处理的话,无视它就好了。
form_for_instance() 是另外一个方法,用于从一个模型对象中产生一个初始化过的表单对象,这个当然给“编辑”表单提供了方便。
下一步?
这一章已经完成了这本书的介绍性的材料。接下来的十三个章节讨论了一些高级的话题,包括生成非 html 内容(第 11 章),安全(第 19 章)和部署(第 20 章)。
在本书最初的七章后,我们(终于)对于使用 Django 构建自己的网站已经知道的够多了,接下来的内容可以在需要的时候阅读。
第八章里我们会更进一步地介绍视图和 URLConfs(介绍见第三章)。