Skip to main content

编写您的第一个Django应用程序,第4部分

本教程从 教程3 离开时开始。我们将继续Web轮询应用程序,并将专注于简单的表单处理和缩减我们的代码。

写一个简单的表单

让我们更新最后一个教程的轮询详细信息模板(“polls/detail.html”),以便模板包含一个HTML <form> 元素:

polls/templates/polls/detail.html
<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 标签经过其循环的次数

  • 因为我们创建一个POST表单(可以有修改数据的效果),我们需要担心跨站点请求伪造。幸运的是,你不必担心太难,因为Django提供了一个非常易于使用的系统来保护它。简而言之,针对内部网址的所有POST表单都应使用 {% csrf_token %} 模板标记。

现在,让我们创建一个Django视图来处理提交的数据并做一些事情。记住,在 教程3 中,我们为包含此行的polls应用程序创建了一个URLconf:

polls/urls.py
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),

我们还创建了 vote() 函数的虚拟实现。让我们创建一个真实版本。将以下内容添加到 polls/views.py

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):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

此代码包括我们在本教程中尚未涵盖的一些内容:

  • request.POST 是一个类似字典的对象,它允许您通过键名访问提交的数据。在这种情况下,request.POST['choice'] 将所选择的选项的ID作为字符串返回。 request.POST 值总是字符串。

    请注意,Django也提供 request.GET 以相同的方式访问GET数据 - 但是我们在代码中明确使用 request.POST,以确保数据只通过POST调用进行更改。

  • 如果POST数据中没有提供 choicerequest.POST['choice'] 将提升 KeyError。上面的代码检查 KeyError,如果没有给出 choice,重新显示一个错误信息的问题表。

  • 在递增选择计数之后,代码返回 HttpResponseRedirect 而不是正常 HttpResponseHttpResponseRedirect 使用一个参数:用户将被重定向到的URL(在这种情况下,请参阅以下点如何构造URL)。

    正如上面的Python注释指出的,你应该在成功处理POST数据后总是返回一个 HttpResponseRedirect。这个提示不是Django特有的;它只是好的Web开发实践。

  • 我们在本例中使用 HttpResponseRedirect 构造函数中的 reverse() 函数。此函数有助于避免在视图函数中对URL进行硬编码。它被赋予我们想要传递控制的视图的名称和指向该视图的URL模式的可变部分。在这种情况下,使用我们在 教程3 中设置的URLconf,这个 reverse() 调用将返回一个字符串

    '/polls/3/results/'
    

    其中 3question.id 的值。然后,此重定向的URL将调用 'results' 视图显示最终页面。

教程3 中所述,request 是一个 HttpRequest 对象。有关 HttpRequest 对象的更多信息,请参阅 请求和响应文档

在某人在问题中投票之后,vote() 视图重定向到问题的结果页面。让我们写这个视图:

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})

这几乎与 教程3detail() 视图完全相同。唯一的区别是模板名称。我们将在以后修复这个冗余。

现在,创建 polls/results.html 模板:

polls/templates/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 的新值,然后将其保存回数据库。如果您的网站的两个用户尝试在 完全相同的时间 投票,这可能会出错:将为 votes 检索相同的值,例如42。然后,对于两个用户,计算并保存新值43,但是44将是期望值。

这被称为 竞争条件。如果你有兴趣,你可以阅读 使用 F() 避免竞争条件,了解如何解决这个问题。

使用通用视图:代码越少越好

detail() (来自 教程3)和 results() 视图是非常简单的 - 并且,如上所述,冗余。显示投票列表的 index() 视图是类似的。

这些视图表示基本Web开发的一种常见情况:根据URL中传递的参数从数据库获取数据,加载模板并返回呈现的模板。因为这是很常见的,Django提供了一个快捷方式,称为“通用视图”系统。

通用视图抽象通用模式到你甚至不需要编写Python代码来编写应用程序的点。

让我们将我们的轮询应用程序转换为使用通用视图系统,因此我们可以删除一堆我们自己的代码。我们只需要采取几个步骤来进行转换。我们会:

  1. 转换URLconf。

  2. 删除一些旧的,不需要的视图。

  3. 基于Django的通用视图引入新视图。

阅读详细信息。

为什么是代码洗牌?

通常,当编写Django应用程序时,您将评估通用视图是否适合您的问题,并且您将从头开始使用它们,而不是重构代码中途。但是这个教程的目的是专注于写“观点”直到现在,关注核心概念。

在开始使用计算器之前,您应该了解基本数学。

修改URLconf

首先,打开 polls/urls.py URLconf并更改它:

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<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>

修改视图

接下来,我们将删除旧的 indexdetailresults 视图,并改用Django的通用视图。为此,请打开 polls/views.py 文件并进行如下更改:

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):
    ... # same as above, no changes needed.

我们在这里使用两个通用视图:ListViewDetailView。分别地,这两个视图抽象了“显示对象列表”和“显示特定类型的对象的详细页面”的概念。

  • 每个通用视图需要知道它将执行什么模型。这是使用 model 属性提供的。

  • DetailView 通用视图期望从URL中捕获的主键值称为 "pk",因此我们将 question_id 更改为 pk 以获取通用视图。

默认情况下,DetailView 通用视图使用名为 <app name>/<model name>_detail.html 的模板。在我们的例子中,它将使用模板 "polls/question_detail.html"template_name 属性用于告诉Django使用特定的模板名称,而不是自动生成的默认模板名称。我们还为 results 列表视图指定了 template_name - 这确保了结果视图和详细视图在呈现时具有不同的外观,即使它们都是 DetailView 幕后。

类似地,ListView 通用视图使用称为 <app name>/<model name>_list.html 的默认模板;我们使用 template_name 来告诉 ListView 使用我们现有的 "polls/index.html" 模板。

在教程的前面部分中,模板已经提供了包含 questionlatest_question_list 上下文变量的上下文。对于 DetailViewquestion 变量是自动提供的 - 因为我们使用的是Django模型(Question),Django能够为上下文变量确定一个合适的名称。但是,对于ListView,自动生成的上下文变量是 question_list。要覆盖这个,我们提供 context_object_name 属性,指定我们要使用 latest_question_list。作为一种替代方法,您可以更改模板以匹配新的默认上下文变量,但是更容易让Django使用您想要的变量。

运行服务器,并使用基于通用视图的新轮询应用程序。

有关通用视图的完整详细信息,请参阅 通用视图文档

当您习惯使用表单和通用视图时,请阅读 part 5 of this tutorial 了解如何测试我们的投票应用程序。