您之前看到了 教程 3 。让我们继续开发 Web 投票应用,通过学习编写 简单的表单处理来简化我们的代码。
让我们更新上一节的投票详情的模板( “polls/detail.html” ) ,添加一个 <form> 元素:
<h1>{{ question.question_text }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>
快速介绍:
value 是每个关联问题选项的 ID ,
单选按钮的 name 是 "choice" ,这就意味着,当用户选中其中一个按钮并提交表单时,
会发送 POST 数据 choice=# , # 即是选中的选项的 ID ,这是 HTML 表单最基本的概念。action 设置为 {% url 'polls:vote' question.id %} ,同时我们设置了
method="post" ,设置 method="post" (而不是 method="get" )很重要,因为
我们提交这个表单的动作会改变服务端的数据。当您创建一个会影响服务端数据的表单时,记得使用
method="post" 。这个技巧不仅对 Django 而言,是一个 Web 开发的最佳实践。forloop.counter 表示当前 for 标签循环了多少次。{% csrf_token %} 模板标签即可。现在,让我们创建一个能处理所提交数据的 Django 视图。如果您记得,在 教程 3 中,我们已经创建了一个 URL 配置,请看下行:
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
我们同样也已经创建了一个没有任何作用的 vote() 函数。现在让我们真正实现它,添加下列内容
到 polls/views.py :
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from .models import Choice, Question
# ...
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# 重新打印“问题”投票表单
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
# 您在处理完 POST 数据后请使用 HttpResponseRedirect 跳转页面,通过这种方式
# 可以防止用户不小心点击后退按钮后重复提交表单
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
这些代码里有一些我们之前没介绍过的内容:
request.POST 是一个可以像字典那样使用的
对象,允许您通过键访问所提交的数据。在这里,通过 request.POST['choice'] 可以得到
所选中问题选项的 ID ,以字符串类型的形式。注意,通过 request.POST
得到的值始终是字符串。
注意 Django 同样为访问 GET 数据提供了 request.GET 。
但是我们这里明确使用 request.POST ,确保
我们的值是通过 POST 请求传过来的。
如果您没有在 POST 数据时提供 choice ,那么访问 request.POST['choice']
会抛出 KeyError 异常。上面的代码会检查 KeyError 异常,并在 choice
没有提供时重新打印“问题”投票表单。
当增加选项的计数时,我们的代码会返回 HttpResponseRedirect
而不是一个普通的 HttpResponse 。 HttpResponseRedirect
携带一个参数:希望用户被跳转到的 URL (请看下面了解我们是如何构造这个 URL 的)
正如上面的 Python 注释所标注的,您应该在成功处理 POST 数据后返回一个 HttpResponseRedirect
,这同样是一个 Web 开发的最佳实践而非仅 Django 才这么做。
我们在 HttpResponseRedirect 的构造器中使用了 reverse()
函数。这个函数能帮助我们避免对一个视图函数的 URL 进行硬编码。调用时给定视图的名称,以及
URL 模式中所需的参数信息。在这里,配合 教程 3 中的 URL 设置,
reverse() 调用会返回如下的字符串
'/polls/3/results/'
3 即是 question.id 的值。这个重定向 URL 会调用 'results' 视图来显示最终的页面。
正如 教程 3 中提到的, request 是一个 HttpRequest 对象。
想了解更多关于 HttpRequest 的信息的,请看 :doc:` 请求和
响应文档 </ref/request-response>` 。
当用户对某个“问题”投了票, vote() 视图会跳转至问题的统计结果页面,让我们来编写这个视图:
from django.shortcuts import get_object_or_404, render
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
这几乎和 教程 3 中的 detail() 视图一模一样,唯一的区别是
模板名称。我们稍后会解决这个冗余的问题。
现在,让我们创建一个 polls/results.html 模版:
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
好了,在浏览器中打开 /polls/1/ 试试投下票。您会看到一个显示着最新的投票计数的页面。
如果您不进行任何投票就提交表单,您会看到错误信息。
注解
我们的 vote() 视图有一个小问题。它首先取出数据库中的 selected_choice 对象,
接着计算得出新的 votes 值,最后将其存回数据库。如果两位用户 ** 几乎同一时间 ** 访问
您的站点并对同一个问题的相同选项做了投票,可能会发生错误:计数只加 1 。比方说取出 votes
的值是 42 ,在同一时刻,处理两位用户的请求都做出了 42 + 1 = 43 的计算,最后存入数据库
的都是 43 这个值,但其实 44 才是我们期望的值。
这个问题被称为 * 竞态条件 * ,如果您感兴趣,可以通过阅读 Avoiding race conditions using F() 来了解如何避免这类问题。
教程 3 中的 detail() 和 results() 视图都非常简单
——以及,正如上面提到的,冗余了,同样的情况还有 index() 视图函数。
这些视图反映了 Web 开发过程中的一种常见的例子:根据 URL 中的参数向数据库取出数据,加载一个模板 渲染并返回响应。因为这种情况实在太常见了,因此 Django 提供了快捷方式——一个被称为“通用视图”的机制。
通用视图将公共模式进行抽象,使得您无须编写 Python 代码就能实现应用所需的功能。
让我们使用通用视图机制改写投票应用,您会看到我们将删除一堆之前写过的代码。我们需要分几步来完成这个转换:
继续阅读以了解细节:
为什么要重新组织代码?
一般来说,当我们编写 Django 应用时,您会评估当前问题是否使用通用视图更合适,接着您会在一开始 就使用它,而不是像现在这样半路重构代码。但是本教程到现在为止一直“故意地”使用一种最“笨”的办法指导 您编写视图,是因为我们希望专注核心概念。
您总应该在拿起计算器使用它前先了解基本的加减乘除四则运算。
首先,打开 polls/urls.py URL 配置并修改它:
from django.conf.urls import url
from . import views
app_name = 'polls'
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
注意第二个和第三个的正则匹配模式已经从 <question_id> 修改为了 <pk> 。
接着,让我们来删除旧的 index 、 detail 、 和 results 视图,并使用 Django 的
通用视图进行改写。要这么做,先打开 polls/views.py 文件,并作下面的修改:
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
... # 和之前一样,不需要修改
这里我们使用了两种通用视图 ListView 和
DetailView 。这两种视图分别对应
“显示一系列的对象的列表”和“显示一个特定类型对象的详情”这两种抽象概念。
model 属性指定。DetailView 通用视图期望从 URL 中
获取到以 "pk" 命名的主键值,为此我们把 question_id 修改为 pk 。默认情况下, DetailView 通用视图使用
文件名是 <app name>/<model name>_detail.html 的模板。在我们的例子中,它
会使用 "polls/question_detail.html" 。属性 template_name 是为了
告诉 Django 使用指定的文件名而不是默认取的那个值。我们同样为 results 视图指定
了 template_name —— 确保结果视图和详情视图具备不同的渲染结果,虽然它们背后都是
class:~django.views.generic.detail.DetailView 的子类。
类似的, ListView 通用视图默认会取
<app name>/<model name>_list.html 作为模板名;我们使用 template_name
告诉 ListView 使用已有的 "polls/index.html" 。
在过去的教程章节中,模板里使用了 question 和 latest_question_list 上下文变量。
对于 DetailView 来说, question 变量已经自动提供了——因为我们使用了 Django 模型
( Question ), Django 会自动推导出最合适的上下文变量名 question 。然而,对于
ListView 而言,自动生成的变量名是 question_list 。为此我们通过 context_object_name
属性来覆盖这一设定。设置 context_object_name 属性,指定我们希望使用的``latest_question_list``
值。当然您也可以通过修改模板中的对应值来达到一样的目的,但是我们选择更简单的方式来完成这件事。
运行服务器,您就会看到最新的使用通用视图实现的投票应用。
对通用视图更详细的细节,请阅读 通用视图文档 。
当您熟悉了表单和通用视图后,请阅读 本教程的第5部分 来学习如何测试 您的投票应用。
9月 10, 2017