===================================== 编写您的第一个Django应用, 第4部分 ===================================== 您之前看到了 :doc:`教程 3 ` 。让我们继续开发 Web 投票应用,通过学习编写 简单的表单处理来简化我们的代码。 编写一个简单的表单 =================== 让我们更新上一节的投票详情的模板( "polls/detail.html" ) ,添加一个 ``
`` 元素: .. snippet:: html+django :filename: polls/templates/polls/detail.html

{{ question.question_text }}

{% if error_message %}

{{ error_message }}

{% endif %} {% csrf_token %} {% for choice in question.choice_set.all %}
{% endfor %}
快速介绍: * 上面的模板为每个问题显示了一个单选按钮。单选按钮的 ``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部分 ` 来学习如何测试 您的投票应用。