本章作者是Simon Willison
经过上一章,你应该对简单网站有个全面的认识。这一章,来处理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'] ) return HttpResponseRedirect('/contact/thanks/') else: form = ContactForm() return render_to_response('contact.html', {'form': form})
在POST之后立即重定向
在一个POST请求过后,如果用户选择刷新页面,这个请求就重复提交了。这常常会导致我们不希望的行为,比如重复的数据库记录。在POST之后重定向页面是一个有用的模式,可以避免这样的情况出现:在一个POST请求成功的处理之后,把用户导引到另外一个页面上去,而不是直接返回HTML页面。
1d0p2u <a href=”http://azetemwaeinp.com/“>azetemwaeinp</a>, [url=http://lcaytpsbsxkh.com/]lcaytpsbsxkh[/url], [link=http://hgjnhjgnqlch.com/]hgjnhjgnqlch[/link], http://curhltadaokb.com/
假设我们已经发布了反馈页面了,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(介绍见第三章)。
关于本评注系统
本站使用上下文关联的评注系统来收集反馈信息。不同于一般对整章做评注的做法, 我们允许你对每一个独立的“文本块”做评注。一个“文本块”看起来是这样的:
一个“文本块”是一个段落,一个列表项,一段代码,或者其他一小段内容。 你选中它会高亮度显示:
要对文本块做评注,你只需要点击它旁边的标识块:
我们会仔细阅读每个评论,如果可能的话我们也会把评注考虑到未来的版本中去:
如果你愿意你的评注被采用,请确保留下你的全名 (注意不是昵称或简称)
Many, many thanks to Jack Slocum; the inspiration and much of the code for the comment system comes from Jack's blog, and this site couldn't have been built without his wonderful
YAHOO.ext
library. Thanks also to Yahoo for YUI itself.