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