本节课,我们从做一个表单开始,我们使用传统的方法做出表单,然后,我们来分析用传统方式做表单的缺陷在哪儿。
继续本教程一直进行的关于书籍、作者、出版社的例子,我们现在来创建一个简单的 view 函数以便让用户可以通过书名从数据库中查找书籍。
通常,表单开发分为两个部分:前端HTML页面用户接口和后台 view 函数对所提交数据的处理过程。第一部分很简单;现在我们来建立个 view 来显示一个搜索表单:
from django.shortcuts import render_to_response def search_form(request): return render_to_response('search_form.html')
在第三章已经学过,这个 view 函数可以放到 Python 的搜索路径的任何位置。为了便于讨论,咱们将它放在 books/views.py 里。
这个 search_form.html 模板,可能看起来是这样的:
<html> <head> <title>Search</title> </head> <body> <form action="/search/" method="get"> <input type="text" name="q"> <input type="submit" value="Search"> </form> </body> </html>
而 urls.py 中的 URLpattern 可能是这样的:
from mysite.books import views urlpatterns = patterns('', # ... (r'^search-form/$', views.search_form), # ... )
(注意,我们直接将 views 模块 import 进来了,而不是用类似 from mysite.views import search_form 这样的语句,因为前者看起来更简洁。我们将在第8章讲述更多的关于 import 的用法。)
现在,如果你运行 runserver
命令,然后访问 http://127.0.0.1:8000/search-form/
,你会看到搜索界面。
非常简单。
不过,当你通过这个 form 提交数据时,你会得到一个 Django 404 错误。这个 Form 指向的
URL /search/
还没有被实现。让我们添加第二个视图函数并设置URL:
# urls.py urlpatterns = patterns('', # ... (r'^search-form/$', views.search_form), (r'^search/$', views.search), # ... ) # views.py def search(request): if 'q' in request.GET: message = 'You searched for: %r' % request.GET['q'] else: message = 'You submitted an empty form.' return HttpResponse(message)
暂时先只显示用户搜索的字词,以确定搜索数据被正确地提交给了 Django,这样你就会知道搜索数据是如何在这个系统中传递的。 简而言之:
在HTML里我们定义了一个变量 q。当提交表单时,变量 q 的值通过 GET(method=”get”)附加在 URL /search/ 上。
处理 /search/(search())的视图通过 request.GET 来获取 q 的值。
需要注意的是在这里明确地判断 q 是否包含在 request.GET 中。就像上面 request.META 小节里面提到,对于用户提交过来的数据,甚至是正确的数据,都需要进行过滤。 在这里若没有进行检测,那么用户提交一个空的表单将引发KeyError异常:
# BAD! def bad_search(request): # The following line will raise KeyError if 'q' hasn't # been submitted! message = 'You searched for: %r' % request.GET['q'] return HttpResponse(message)
查询字符串参数
因为使用 GET 方法的数据是通过查询字符串的方式传递的(例如 /search/?q=django),所以我们可以使用 requet.GET 来获取这些数据。第三章介绍 Django 的 URLconf 系统时我们比较了 Django 的简洁的 URL 与 PHP/Java 传统的 URL,我们提到将在第七章讲述如何使用传统的 URL。通过刚才的介绍,我们知道在视图里可以使用 request.GET 来获取传统URL里的查询字符串(例如 hours=3)。
获取使用 POST 方法的数据与 GET 的相似,只是使用 request.POST 代替了 request.GET。那么,POST
与 GET 之间有什么不同?当我们提交表单仅仅需要获取数据时就可以用 GET;
而当我们提交表单时需要更改服务器数据的状态,或者说发送
e-mail,或者其他不仅仅是获取并显示数据的时候就使用 POST。 在这个搜索书籍的例子里,我们使用
GET,因为这个查询不会更改服务器数据的状态。(如果你有兴趣了解更多关于
GET
和 POST
的知识,可以参见http://www.w3.org/2001/tag/doc/whenToUseGet.html。)
既然已经确认用户所提交的数据是有效的,那么接下来就可以从数据库中查询这个有效的数据(同样,在 views.py 里操作):
from django.http import HttpResponse from django.shortcuts import render_to_response from mysite.books.models import Book def search(request): if 'q' in request.GET and request.GET['q']: q = request.GET['q'] books = Book.objects.filter(title__icontains=q) return render_to_response('search_results.html', {'books': books, 'query': q}) else: return HttpResponse('Please submit a search term.')
让我们来分析一下上面的代码:
除了检查 q 是否存在于 request.GET 之外,我们还检查来 reuqest.GET[‘q’] 的值是否为空。我们使用 Book.objects.filter(title__icontains=q) 获取数据库中标题包含q的书籍。 icontains 是一个查询关键字(参看第五章和附录B)。这个语句可以理解为获取标题里包含 q 的书籍,不区分大小写。这是实现书籍查询的一个很简单的方法。我们不推荐在一个包含大量产品的数据库中使用 icontains 查询,因为那会很慢。(在真实的案例中,我们可以使用以某种分类的自定义查询系统。在网上搜索“开源全文搜索”看看是否有好的方法)最后,我们给模板传递来 books,一个包含 Book 对象的列表。查询结果的显示模板 search_results.html 如下所示:
<p>You searched for: <strong>{{ query }}</strong></p> {% if books %} <p>Found {{ books|length }} book{{ books|pluralize }}.</p> <ul> {% for book in books %} <li>{{ book.title }}</li> {% endfor %} </ul> {% else %} <p>No books matched your search criteria.</p> {% endif %}
注意这里 pluralize 的使用,这个过滤器在适当的时候会输出(例如找到多本书籍)。
同上一章一样,我们先从最为简单、有效的例子开始。现在我们再来找出这个简单的例子中的不足,然后改进他们。
首先,search()视图对于空字符串的处理相当薄弱——仅显示一条 ”Please submit a search term.” 的提示信息。 若用户要重新填写表单必须自行点击“后退”按钮, 这种做法既糟糕又不专业。如果在现实的案例中,我们这样子编写,那么 Django 的优势将荡然无存。
在检测到空字符串时更好的解决方法是重新显示表单,并在表单上面给出错误提示以便用户立刻重新填写。最简单的实现方法既是添加else分句重新显示表单,代码如下:
from django.http import HttpResponse from django.shortcuts import render_to_response from mysite.books.models import Book def search_form(request): return render_to_response('search_form.html') def search(request): if 'q' in request.GET and request.GET['q']: q = request.GET['q'] books = Book.objects.filter(title__icontains=q) return render_to_response('search_results.html', {'books': books, 'query': q}) else: **return render_to_response('search_form.html', {'error': True})**
(注意,将 search_form() 视图也包含进来以便查看)
这段代码里,我们改进来 search() 视图:在字符串为空时重新显示 search_form.html。并且给这个模板传递了一个变量 error,记录着错误提示信息。现在我们编辑一下 search_form.html,检测变量 error:
<html> <head> <title>Search</title> </head> <body> **{% if error %}** **<p style="color: red;">Please submit a search term.</p>** **{% endif %}** <form action="/search/" method="get"> <input type="text" name="q"> <input type="submit" value="Search"> </form> </body> </html>
我们修改了 search_form() 视图所使用的模板,因为 search_form() 视图没有传递 error 变量,所以在条用 search_form 视图时不会显示错误信息。
通过上面的一些修改,现在程序变的好多了,但是现在出现一个问题: 是否有必要专门编写 search_form() 来显示表单?按实际情况来说,当一个请求发送至 /search/(未包含 GET 的数据)后将会显示一个空的表单(带有错误信息)。所以,只要我们改变 search() 视图:当用户访问 /search/ 并未提交任何数据时就隐藏错误信息,这样就移去 search_form() 视图以及对应的 URLpattern。
def search(request): error = False if 'q' in request.GET: q = request.GET['q'] if not q: error = True else: books = Book.objects.filter(title__icontains=q) return render_to_response('search_results.html', {'books': books, 'query': q}) return render_to_response('search_form.html', {'error': error})
在改进后的视图中,若用户访问 /search/ 并且没有带有 GET 数据,那么他将看到一个没有错误信息的表单;如果用户提交了一个空表单,那么它将看到错误提示信息,还有表单;最后,若用户提交了一个非空的值,那么他将看到搜索结果。
最后,我们再稍微改进一下这个表单,去掉冗余的部分。 既然已经将两个视图与 URLs 合并起来,/search/ 视图管理着表单的显示以及结果的显示,那么在 search_form.html 里表单的 action 值就没有必要硬编码的指定 URL。原先的代码是这样:
<form action="/search/" method="get">
现在改成这样:
<form action="" method="get">
action=”“意味着表单将提交给与当前页面相同的 URL。这样修改之后,如果
search()
视图不指向其它页面的话,你将不必再修改 action
。
我们的搜索示例仍然相当地简单,特别从数据验证方面来讲;我们仅仅只验证搜索关键值是否为空。 然后许多HTML表单包含着比检测值是否为空更为复杂的验证。 我们都有在网站上见过类似以下的错误提示信息:
请输入一个有效的 email 地址,foo’ 并不是一个有效的 e-mail 地址。
请输入 5 位数的 U.S 邮政编码,123 并非是一个有效的邮政编码。
请输入 YYYY-MM-DD 格式的日期。
请输入 8 位数以上并至少包含一个数字的密码。
可以使用 Javascript 在客户端浏览器里对数据进行验证,这些知识已超出本书范围。要注意:即使在客户端已经做了验证,但是服务器端仍必须再验证一次。 因为有些用户会将 JavaScript 关闭掉,并且还有一些怀有恶意的用户会尝试提交非法的数据来探测是否有可以攻击的机会。
除了在服务器端对用户提交的数据进行验证(例如在视图里验证),我们没有其他办法。JavaScript 验证可以看作是额外的功能,但不能作为唯一的验证功能。
我们来调整一下 search() 视图,让她能够验证搜索关键词是否小于或等于 20 个字符。(为来让例子更为显著,我们假设如果关键词超过 20 个字符将导致查询十分缓慢)。那么该如何实现呢?最简单的方式就是将逻辑处理直接嵌入到视图里,就像这样:
def search(request): error = False if 'q' in request.GET: q = request.GET['q'] if not q: error = True **elif len(q) > 20:** **error = True** else: books = Book.objects.filter(title__icontains=q) return render_to_response('search_results.html', {'books': books, 'query': q}) return render_to_response('search_form.html', {'error': error})
现在,如果尝试着提交一个超过 20 个字符的搜索关键词,系统不会执行搜索操作,而是显示一条错误提示信息。但是,search_form.html 里的这条提示信息是:”Please submit a search term.”,这显然是错误的,所以我们需要更精确的提示信息:
<html> <head> <title>Search</title> </head> <body> {% if error %} <p style="color: red;">Please submit a search term 20 characters or shorter.</p> {% endif %} <form action="/search/" method="get"> <input type="text" name="q"> <input type="submit" value="Search"> </form> </body> </html>
但像这样修改之后仍有一些问题。我们包含万象的提示信息很容易使人产生困惑:提交一个空表单怎么会出现一个关于 20 个字符限制的提示?所以,提示信息必须是详细的,明确的,不会产生疑议。
问题的实质在于我们只使用来一个布尔类型的变量来检测是否出错,而不是使用一个列表来记录相应的错误信息。我们需要做如下的调整:
def search(request): **errors = []** if 'q' in request.GET: q = request.GET['q'] if not q: **errors.append('Enter a search term.')** elif len(q) > 20: **errors.append('Please enter at most 20 characters.')** else: books = Book.objects.filter(title__icontains=q) return render_to_response('search_results.html', {'books': books, 'query': q}) return render_to_response('search_form.html', {**'errors': errors** })
接着,我们要修改一下 search_form.html 模板,现在需要显示一个 errors 列表而不是一个布尔判断。
<html> <head> <title>Search</title> </head> <body> **{% if errors %}** **<ul>** **{% for error in errors %}** **<li>{{ error }}</li>** **{% endfor %}** **</ul>** **{% endif %}** <form action="/search/" method="get"> <input type="text" name="q"> <input type="submit" value="Search"> </form> </body> </html>
虽然我们一直使用书籍搜索的示例表单,并将起改进的很完美,但是这还是相当的简陋:只包含一个字段,q。这简单的例子,我们不需要使用Django表单库来处理。 但是复杂一点的表单就需要多方面的处理,我们现在来一下一个较为复杂的例子:站点联系表单。
这个表单包括用户提交的反馈信息,一个可选的 e-mail 回信地址。当这个表单提交并且数据通过验证后,系统将自动发送一封包含题用户提交的信息的e-mail给站点工作人员。
我们从contact_form.html模板入手:
<html> <head> <title>Contact us</title> </head> <body> <h1>Contact us</h1> {% if errors %} <ul> {% for error in errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <form action="/contact/" method="post"> <p>Subject: <input type="text" name="subject"></p> <p>Your e-mail (optional): <input type="text" name="email"></p> <p>Message: <textarea name="message" rows="10" cols="50"></textarea></p> <input type="submit" value="Submit"> </form> </body> </html>
我们定义了三个字段:主题,e-mail 和反馈信息。 除了 e-mail 字段为可选,其他两个字段都是必填项。 注意,这里我们使用 method=”post” 而非 method=”get”,因为这个表单会有一个服务器端的操作:发送一封e-mail。 并且,我们复制了前一个模板search_form.html中错误信息显示的代码。
如果我们顺着上一节编写 search() 视图的思路,那么一个 contact() 视图代码应该像这样:
from django.core.mail import send_mail from django.http import HttpResponseRedirect from django.shortcuts import render_to_response def contact(request): errors = [] if request.method == 'POST': if not request.POST.get('subject', ''): errors.append('Enter a subject.') if not request.POST.get('message', ''): errors.append('Enter a message.') if request.POST.get('email') and '@' not in request.POST['email']: errors.append('Enter a valid e-mail address.') if not errors: send_mail( request.POST['subject'], request.POST['message'], request.POST.get('email', 'noreply@example.com'), ['siteowner@example.com'], ) return HttpResponseRedirect('/contact/thanks/') return render_to_response('contact_form.html', {'errors': errors})
(如果按照书中的示例做下来,这这里可能乎产生一个疑问:contact() 视图是否要放在 books/views.py 这个文件里。 但是contact()视图与books应用没有任何关联,那么这个视图应该可以放在别的地方? 这毫无紧要,只要在 URLconf 里正确设置 URL 与视图之间的映射,Django 会正确处理的。笔者个人喜欢创建一个contact的文件夹,与books文件夹同级。这个文件夹中包括空的init.py和views.py两个文件。
现在来分析一下以上的代码:
确认 request.method 的值是 ’POST’。用户浏览表单时这个值并不存在,当且仅当表单被提交时这个值才出现。 (在后面的例子中,request.method 将会设置为 ’GET’,因为在普通的网页浏览中,浏览器都使用 GET,而非POST)。判断 request.method 的值很好地帮助我们将表单显示与表单处理隔离开来。我们使用 request.POST 代替 request.GET 来获取提交过来的数据。这是必须的,因为 contact_form.html 里表单使用的是 method=”post”。如果在视图里通过 POST 获取数据,那么 request.GET 将为空。这里,有两个必填项,subject 和 message,所以需要对这两个进行验证。注意,我们使用 request.POST.get() 方法,并提供一个空的字符串作为默认值;这个方法很好的解决了键丢失与空数据问题。虽然email非必填项,但如果有提交她的值则我们也需进行验证。 我们的验证算法相当的薄弱,仅验证值是否包含@字符。在实际应用中,需要更为健壮的验证机制(Django 提供这些验证机制,稍候我们就会看到)。我们使用了 django.core.mail.send_mail 函数来发送 e-mail。 这个函数有四个必选参数:主题,正文,寄信人和收件人列表。 send_mail 是 Django 的 EmailMessage 类的一个方便的包装,EmailMessage 类提供了更高级的方法,比如附件,多部分邮件,以及对于邮件头部的完整控制。注意,若要使用 send_mail() 函数来发送邮件,那么服务器需要配置成能够对外发送邮件,并且在 Django 中设置出站服务器地址。参见规范:http://docs.djangoproject.com/en/dev/topics/email/当邮件发送成功之后,我们使用HttpResponseRedirect对象将网页重定向至一个包含成功信息的页面。 包含成功信息的页面这里留给读者去编写(很简单 一个视图/URL映射/一份模板即可),但是我们要解释一下为何重定向至新的页面,而不是在模板中直接调用render_to_response()来输出。原因就是: 若用户刷新一个包含POST表单的页面,那么请求将会重新发送造成重复。 这通常会造成非期望的结果,比如说重复的数据库记录;在我们的例子中,将导致发送两封同样的邮件。 如果用户在POST表单之后被重定向至另外的页面,就不会造成重复的请求了。我们应每次都给成功的POST请求做重定向。 这就是web开发的最佳实践。
contact() 视图可以正常工作,但是她的验证功能有些复杂。 想象一下假如一个表单包含一打字段,我们真的将必须去编写每个域对应的if判断语句?
另外一个问题是表单的重新显示。若数据验证失败后,返回客户端的表单中各字段最好是填有原来提交的数据,以便用户查看哪里出现错误(用户也不需再次填写正确的字段值)。 我们可以手动地将原来的提交数据返回给模板,并且必须编辑HTML里的各字段来填充原来的值。
# views.py def contact(request): errors = [] if request.method == 'POST': if not request.POST.get('subject', ''): errors.append('Enter a subject.') if not request.POST.get('message', ''): errors.append('Enter a message.') if request.POST.get('email') and '@' not in request.POST['email']: errors.append('Enter a valid e-mail address.') if not errors: send_mail( request.POST['subject'], request.POST['message'], request.POST.get('email', `'noreply@example.com`_'), [`'siteowner@example.com`_'], ) return HttpResponseRedirect('/contact/thanks/') return render_to_response('contact_form.html', { 'errors': errors, **'subject': request.POST.get('subject', ''),** **'message': request.POST.get('message', ''),** **'email': request.POST.get('email', ''),** }) # contact_form.html <html> <head> <title>Contact us</title> </head> <body> <h1>Contact us</h1> {% if errors %} <ul> {% for error in errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} <form action="/contact/" method="post"> <p>Subject: <input type="text" name="subject" **value="{{ subject }}"** ></p> <p>Your e-mail (optional): <input type="text" name="email" **value="{{ email }}"** ></p> <p>Message: <textarea name="message" rows="10" cols="50">**{{ message }}**</textarea></p> <input type="submit" value="Submit"> </form> </body> </html>
这看起来杂乱,且写的时候容易出错。希望你开始明白使用高级库的用意——负责处理表单及相关校验任务。