欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

翻译www.djangobook.com之第七章:表单处理 博客分类: Python DjangoPython框架HTML出版 

程序员文章站 2024-02-22 19:14:40
...
The Django Book:第7章 表单处理

翻译:xin_wang

嘉宾作者: Simon Willison

经过上一章,你应该已经有一个全功能的简单网站了。在这一章里,我们将会处理下一个难题:建立能处理用户输入的视图。

我们会从手工打造一个简单的搜索页面开始,看看怎样处理浏览器提交而来的数据。然后我们开始使用Django的forms框架。

搜索

Web的关键就是搜索。网络的两个最大的传奇Yahoo和Google,它们数以亿计的商业收入就是建立在搜索的基础之上。几乎所有的站点都可以观察到来自搜索页面的巨大流量。通常一个站点的成败就系于搜索质量的好坏。所以我们最好给我们羽翼未丰的网站加上搜索功能,不是吗?

我们将会在URLConf(mysite.urls)里面加上搜索的视图。也就是在URL表达式的集合里加上类似于(r'^search/$', 'mysite.books.views.search')的表达式。

下一步,我们会在视图模块里加入这个搜索的视图(方法):

Toggle line numbers

   1 from django.db.models import Q
   2 from django.shortcuts import render_to_response
   3 from models import Book
   4 def search(request):
   5     query = request.GET.get('q', '')
   6     if query:
   7         qset = (
   8             Q(title__icontains=query) |
   9             Q(authors__first_name__icontains=query) |
  10             Q(authors__last_name__icontains=query)
  11         )
  12         results = Book.objects.filter(qset).distinct()
  13     else:
  14         results = []
  15     return render_to_response("books/search.html", {
  16         "results": results,
  17         "query": query
  18     })

这里有几处用法我们没有见过。首先是request.GET。这是在Django中我们访问GET数据的方法;POST数据可以通过相似的方法 request.POST来访问。这些对象的行为非常类似标准的Python字典对象,但是它们还是有一些额外的特性,请参看附录H。

什么是GET/POST数据?

GET和POST是两种浏览器发送数据到服务器的方法。大多数情况下,我们会在html的form标签里看到它们:

<form action="/books/search/" method="get"/>

它指示浏览器向URL /books/search/以GET方法提交数据。 GET和POST的语义有着很大的不同,我们不会马上跳进这样的细节,但是如果你想知道更多,请参考http: //www.w3.org/2001/tag/doc/whenToUseGet.html。

所以这一行:

Toggle line numbers

   1 query = request.GET.get('q', '')

查找一个名叫q的GET参数,如果这个参数不存在,则返回一个空字符串。

请注意我们在使用get()方法,这可能引起迷惑。这里的get()方法是每一个Python字典都有的。我们在这里使用它是出于谨慎: 假设request.GET之中含有这个'q'的键值是不安全的,所以我们用了get('q', )提供一个“退而求其次”的办法,也就是空字符串。如果我们只通过request.GET['q']来访问值变量的话,如果键值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>

这段代码应该不算晦涩。但还是说明一下:

    * form的action属性是.,意味着form会被提交到当前的URL。这个算是标准的最佳实践了:对于form页和结果页,不要使用两个不同的视图;使用一个就可以了。
    *

      我们把查询回来的值又插入了<input>标签。这样可以使读者具体化他们的查询,而不必一次又一次的输入同样的查询条件。
    * 在每一个查询和书籍出现的地方,我们都是用了escape过滤器,这样可以怀有恶意的查询文本不会被填入页面。对于任何用户提交的数据,这都是非常重要的!否则你的网站将会彻底暴露在跨域脚本攻击(cross-site scripting,XSS)之下。第十九章详细讨论了XSS以及安全模型的话题。
    * 但是,我们倒是可以不同担心在数据库查询的时候发生同样的事情 - 我们直接把query原样传入就好了。因为Django的数据库层已经为我们做好足够的防护了。 

现在我们终于有了一个能工作的查询了。更进一步的工作大概就是把查询form放在每一个页面上了(也就是在基模板中);这个工作请读者自行完成了。

下面,我们看一个更复杂的例子。不过在此之前,来讨论一个更抽象的话题:“完美的form”。

完美的form

form这个东西,常常是打败你网站用户的罪魁祸首。我们来想象一下一个完美的form吧:

    *

      它要问用户一些信息,显然可访问性和可用性(Accessibility and usability)是非常重要的。所以聪明地使用<label>标签和有用的上下文相关的帮助是很重要的。
    * 提交的数据应该接受验证。web应用程序的一条铁律:“永远不要相信输入”,所以验证是基本要求。
    * 如果用户犯了错,form应该能够显示详细的,有帮助的出错信息。原来的数据应该可以被填充起来,节省重新填写的时间。
    * form应该持续的重新显示出错的form,直到所有的字段都正确地得以填充。 

构建一个完美的form看来需要很多工作。谢天谢地,Django的forms框架为我们做了大部分工作。我们只需要描述form的字段,验证规则,以及一个简单的模板,Django会帮我们做起他的。我们能用非常少的工作来构建一个完美的form。

一个反馈表单

构建受欢迎站点的最佳模式就是倾听用户的声音。很多网站看来忽略了这一点:他们把联系方式深深藏在层层的FAQ中,联系他们简直难如登天。

当你的站点拥有上百万的用户,这当然是一个合理的策略。但是当我们培养用户的时候,在任何可能的时候鼓励反馈才是我们应该做的事情。让我们建立一个反馈的form来实战一下Django的forms框架吧。

我们在URLConf里面增加一个URL规则:(r'^contact/$', 'mysite.books.views.contact'),然后定义我们的form。在Djaong里创建form与model类似:用申明式的方式,定义一个Python的class。以下是我们简单的form定义。依循惯例,我们在应用程序的目录里面添加一个新的forms.py文件:

Toggle line numbers

   1 from django import newforms as forms
   2 TOPIC_CHOICES = (
   3     ('general', 'General enquiry'), ('bug', 'Bug report'), ('suggestion', 'Suggestion'),
   4 )
   5 class ContactForm(forms.Form):
   6    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列表。

我们的ContackForm包含三个字段:一个topic,它是一个三选一的选择框;一个message,他是一个文本域;还有一个sender,它是一个可选的(匿名的反馈也是有用的)email域。还有很多字段类型可供选择,如果它们都不满足要求,你可以考虑自己写一个。

form对象自己知道如何做一些有用的事情。它能校验数据集合,生成HTML“部件”,生成一集有用的错误信息,当然,如果你确实很懒,它也能绘出整个form。现在让我们把它嵌入一个视图,看看怎么样使用它。在views.py里面:

Toggle line numbers

   1 from django.db.models import Q
   2 from django.shortcuts import render_to_response
   3 from models import Book
   4 from forms import ContactForm
   5 def search(request):
   6     query = request.GET.get('q', '')
   7     if query:
   8         qset = (
   9             Q(title__icontains=query) |
  10             Q(authors__first_name__icontains=query) |
  11             Q(authors__last_name__icontains=query)
  12         )
  13         results = Book.objects.filter(qset).distinct()
  14     else:
  15         results = []
  16     return render_to_response("books/search.html", {
  17         "results": results,
  18         "query": query
  19     })
  20 def contact(request):
  21     form = ContactForm()
  22     return render_to_response('contact.html', {'form': form})

and in contact.html:

在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>部件来替代:

Toggle line numbers

   1 class ContactForm(forms.Form):
   2     topic = forms.ChoiceField(choices=TOPIC_CHOICES)
   3     message = forms.CharField(widget=forms.Textarea())
   4     sender = forms.EmailField(required=False)

forms框架把每一个字段的显示逻辑分离到一组部件(widget)中。每一个字段类型都拥有一个默认的部件,我们也可以容易地替换掉默认的部件,或者提供一个自定义的部件。

现在,提交这个form没有在后台做任何事情。让我们把我们的校验规则加进去:

Toggle line numbers

   1 def contact(request):
   2     if request.method == 'POST':
   3         form = ContactForm(request.POST)
   4     else:
   5         form = ContactForm()
   6     return render_to_response('contact.html', {'form': form})

一个form实例可能处在两种状态:绑定或者未绑定。一个绑定的实例是由字典(或者类似于字典的对象)构造而来的,它同样也知道如何验证和重新显示它的数据。一个未绑定的form是没有与之联系的数据,仅仅知道如何显示其自身。

现在可以试着提交一下这个空白的form了。页面将会被重新显示出来,显示一个验证错误,提示我们message字段是必须的。

现在输入一个不合法的email地址,EmailField知道如何验证email地址,大多数情况下这种验证是合理的。

设置初始数据

Passing data directly to the form constructor binds that data and indicates that validation should be performed. Often, though, we need to display an initial form with some of the fields prefilled — for example, an “edit” form. We can do this with the initial keyword argument:

向form的构造器函数直接传递数据会把这些数据绑定到form,指示form进行验证。我们有时也需要在初始化的时候预先填充一些字段——比方说一个编辑form。我们可以传入一些初始的关键字参数:

Toggle line numbers

   1 form = CommentForm(initial={'sender': ' user@example.com '})

如果我们的form总是会使用相同的默认值,我们可以在form自身的定义中设置它们:

Toggle line numbers

   1 message = forms.CharField(widget=forms.Textarea(),initial="Replace with your feedback")

处理提交

当用户填完form,完成了校验,我们需要做一些有用的事情了。在这种情况下,我们需要构造并发送一个包含了用户反馈的email,我们将会使用Django的email包来完成。

首先,我们需要知道用户数据是不是真的合法,如果是这样,我们就要访问已经验证过的数据。forms框架甚至做的更多,它会把它们转换成对应的Python类型。我们的联系方式form仅仅处理字符串,但是如果我们使用IntegerField或者DataTimeField,forms框架会保证我们从中取得类型正确的值。

测试一个form是否已经绑定到合法的数据,使用is_valid()方法:

Toggle line numbers

   1 form = ContactForm(request.POST) if form.is_valid():

处理form数据

现在我们要访问数据了。我们可以从request.POST里面直接把它们取出来,但是这样做我们就丧失了由framework为我们自动做类型转换的好处了。所以我们要使用form.clean_date:

Toggle line numbers

   1 if form.is_valid():
   2     topic = form.clean_data['topic'] message = form.clean_data['message']
   3     sender = form.clean_data.get('sender', ' noreply@example.com ')
   4     # ...

请注意因为sender不是必需的,我们为它提供了一个默认值。终于,我们要记录下用户的反馈了,最简单的方法就是把它发送给站点管理员,我们可以使用send_mail方法:

Toggle line numbers

   1 from django.core.mail import send_mail
   2 # ...
   3 send_mail('Feedback from your site, topic: %s' % topic, message, sender,['administrator@example.com'])

send_mail方法有四个必须的参数:主题,邮件正文,from和一个接受者列表。send_mail是Django的EmailMessage类的一个方便的包装,EmailMessage类提供了更高级的方法,比如附件,多部分邮件,以及对于邮件头部的完整控制。 发送完邮件之后,我们会把用户重定向到确认的页面。完成之后的视图方法如下:

Toggle line numbers

   1 from django.http import HttpResponseRedirect
   2 from django.shortcuts import render_to_response
   3 from django.core.mail import send_mail
   4 from forms import ContactForm
   5 def contact(request):
   6    if request.method == 'POST':
   7       form = ContactForm(request.POST) if form.is_valid():
   8       topic = form.clean_data['topic'] message = form.clean_data['message']
   9       sender = form.clean_data.get('sender', ' noreply@example.com ') send_mail(
  10         'Feedback from your site, topic: %s' % topic, message, sender,
  11         [' administrator@example.com '])
  12        return HttpResponseRedirect('/contact/thanks/')
  13    else:
  14        form = ContactForm()
  15        return render_to_response('contact.html', {'form': form})

Redirect After POST

在POST之后立即重定向

在一个POST请求过后,如果用户选择刷新页面,这个请求就重复提交了。这常常会导致我们不希望的行为,比如重复的数据库记录。在POST之后重定向页面是一个有用的模式,可以避免这样的情况出现:在一个POST请求成功的处理之后,把用户导引到另外一个页面上去,而不是直接返回HTML页面。

Custom Validation Rules

自定义校验规则

假设我们已经发布了反馈页面了,email已经开始源源不断地涌入了。只有一个问题:一些email只有寥寥数语,很难从中得到什么详细有用的信息。所以我们决定增加一条新的校验:来点专业精神,最起码写四个字,拜托。

我们有很多的方法把我们的自定义校验挂在Django的form上。如果我们的规则会被一次又一次的使用,我们可以创建一个自定义的字段类型。大多数的自定义校验都是一次性的,可以直接绑定到form类.

我们希望message字段有一个额外的校验,我们增加一个clean_message方法:

Toggle line numbers

   1 class ContactForm(forms.Form):
   2     topic = forms.ChoiceField(choices=TOPIC_CHOICES)
   3     message = forms.CharField(widget=forms.Textarea())
   4     sender = forms.EmailField(required=False)
   5     def clean_message(self):
   6         message = self.clean_data.get('message', '')
   7         num_words = len(message.split())
   8         if num_words < 4:
   9             raise forms.ValidationError("Not enough words!") ''
  10         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",在一个有序列表中显示错误信息。

从模型中创造forms

我们弄个有趣的东西吧:一个新的form,提交一个新出版商的信息到我们第五章的book应用。

一个非常重要的Django的开发理念就是不要重复你自己(DRY)。Any Hunt和Dave Thomas在《实用主义程序员》里定义了这个原则:在系统内部,每一条(领域相关的)知识的片断都必须有一个单独的,无歧义的,正式的表述。我们的出版商模型拥有一个名字,地址,城市,州(省),国家和网站。在form中重复这个信息无疑违反了DRY原则。我们可以使用一个捷径: form_for_model():

Toggle line numbers

   1 from models import Publisher
   2 from django.newforms import form_for_model
   3 PublisherForm = form_for_model(Publisher)

PublisherForm是一个Form子类,像刚刚手工创建的ContactForm类一样。我们可以像刚才一样使用它:

Toggle line numbers

   1 from forms import PublisherForm
   2 def add_publisher(request):
   3     if request.method == 'POST':
   4        form = PublisherForm(request.POST) if form.is_valid():
   5        form.save()
   6        return HttpResponseRedirect('/add_publisher/thanks/')
   7     else:
   8        form = PublisherForm()
   9        return render_to_response('books/add_publisher.html', {'form': form})

add_publisher.html文件几乎跟我们的contact.html模板一样,所以不赘述了。记得在URLConf里面加上:(r'^add_publisher/$', 'mysite.books.views.add_publisher').

Toggle line numbers

   1    if request.method == 'POST':
   2        form = PublisherForm(request.POST) if form.is_valid():
   3        form.save()
   4        return HttpResponseRedirect('/add_publisher/thanks/')
   5    else:
   6        form = PublisherForm()
   7        return render_to_response('books/add_publisher.html', {'form': form})

还有一个快捷的方法。因为从模型而来的表单经常被用来把新的模型的实例保存到数据库,从form_for_model而来的表单对象包含一个save方法。一般情况下够用了;你要是要做更多的事情,无视它就好了。

form_for_instance()是另外一个方法,用于从一个模型对象中产生一个初始化过的表单对象,这个当然给“编辑”表单提供了方便。

下一步?

这一章已经完成了这本书的介绍性的材料。下面的十三章讨论了一些高级的话题,包括声称非html内容(ch11),安全(ch19)和部署(ch20)。

在本书最初的七章后,我们(终于)对于使用Django构建自己的网站已经知道的够多了,接下来的内容可以在需要的时候阅读。 第八章里我们会更进一步地介绍视图和URLConfs(介绍见第三章)。