编写你的第一个 Django 应用,第 4 部分

本教程从教程 3结束的地方开始。我们将继续开发网络投票应用程序,并重点关注表单处理和代码精简。

获取帮助

如果您在学习本教程时遇到问题,请前往常见问题解答的获取帮助部分。

编写一个最简表单

让我们更新上一个教程中的投票详情模板(“polls/detail.html”),使其包含一个 HTML <form> 元素

polls/templates/polls/detail.html
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% 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 %}
</fieldset>
<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 带有一个有助于防止它的系统。简而言之,所有针对内部 URL 的 POST 表单都应使用 {% csrf_token %} 模板标签。

现在,让我们创建一个处理提交的数据并对其进行处理的 Django 视图。请记住,在教程 3中,我们为 polls 应用程序创建了一个 URLconf,其中包含以下行

polls/urls.py
path("<int:question_id>/vote/", views.vote, name="vote"),

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

polls/views.py
from django.db.models import F
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
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 = F("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 调用更改数据。

  • request.POST['choice'] 如果 POST 数据中未提供 choice,则将引发 KeyError。上述代码检查 KeyError,如果未给出 choice,则重新显示问题表单并显示错误消息。

  • F("votes") + 1 指示数据库 将投票数增加 1。

  • 递增选项计数后,代码返回 HttpResponseRedirect 而不是普通的 HttpResponseHttpResponseRedirect 接受一个参数:用户将被重定向到的 URL(有关在本例中如何构造 URL,请参见下一段)。

    如上面的 Python 注释所指出的,在成功处理 POST 数据后,您应该始终返回 HttpResponseRedirect。此提示并非特定于 Django;它通常是良好的 Web 开发实践。

  • 我们在此示例中使用了 reverse() 函数在 HttpResponseRedirect 构造函数中。此函数有助于避免在视图函数中硬编码 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})

这几乎与教程 3中的 detail() 视图完全相同。唯一的区别是模板名称。我们稍后将解决此冗余问题。

现在,创建一个 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/ 并对问题进行投票。您应该会看到一个结果页面,该页面在每次投票时都会更新。如果您在未选择选项的情况下提交表单,则应该会看到错误消息。

使用通用视图:更少的代码更好

教程 3开始的 detail()results() 视图非常简短,并且如上所述,是冗余的。index() 视图(显示投票列表)与此类似。

这些视图代表了基础 Web 开发中的一个常见案例:根据 URL 中传递的参数从数据库获取数据,加载模板并返回渲染后的模板。由于这种情况非常普遍,Django 提供了一个快捷方式,称为“通用视图”系统。

通用视图将常见模式抽象化到无需编写 Python 代码即可编写应用程序的程度。例如,ListViewDetailView 通用视图分别抽象了“显示对象列表”和“显示特定类型对象的详细信息页面”的概念。

让我们将我们的投票应用转换为使用通用视图系统,这样我们就可以删除很多我们自己的代码。我们将需要采取一些步骤来进行转换。我们将

  1. 转换 URLconf。

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

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

继续阅读以获取详细信息。

为什么要进行代码重排?

通常,在编写 Django 应用时,您会评估通用视图是否适合您的问题,并且您会从一开始就使用它们,而不是在编写代码的过程中进行重构。但是,本教程有意地将重点放在“困难的方式”编写视图上,直到现在,以便专注于核心概念。

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

修改 URLconf

首先,打开 polls/urls.py URLconf 并将其更改如下

polls/urls.py
from django.urls import path

from . import views

app_name = "polls"
urlpatterns = [
    path("", views.IndexView.as_view(), name="index"),
    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

请注意,第二个和第三个模式的路径字符串中匹配模式的名称已从 <question_id> 更改为 <pk>。这是必要的,因为我们将使用 DetailView 通用视图替换我们的 detail()results() 视图,并且它期望从 URL 中捕获的主键值被称为 "pk"

修改视图

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

polls/views.py
from django.db.models import F
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
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.
    ...

每个通用视图都需要知道它将作用于哪个模型。这可以通过使用 model 属性(在本例中,model = Question 用于 DetailViewResultsView)或通过定义 get_queryset() 方法(如 IndexView 中所示)来提供。

默认情况下,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 使用您想要的变量要容易得多。

运行服务器,并使用您基于通用视图的新投票应用。

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

当您熟悉表单和通用视图后,请阅读 本教程的第 5 部分,了解如何测试我们的投票应用。

返回顶部