编写你的第一个 Django 应用程序,第 4 部分 — Django 文档

来自菜鸟教程
Django/docs/2.2.x/intro/tutorial04
跳转至:导航、​搜索

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

本教程从 教程 3 停止的地方开始。 我们将继续使用网络投票应用程序,并将专注于简单的表单处理和减少我们的代码。

写一个简单的表格

让我们更新上一个教程中的投票详细信息模板(“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 表示 :ttag:`for` 标签经历了多少次循环
  • 由于我们正在创建一个 POST 表单(可以具有修改数据的效果),因此我们需要担心跨站点请求伪造。 值得庆幸的是,您不必太担心,因为 Django 带有一个非常易于使用的系统来防止它。 简而言之,所有针对内部 URL 的 POST 表单都应该使用 :ttag:`{% csrf_token %} ` 模板标签。

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

polls/urls.py

path('<int:question_id>/vote/', views.vote, name='vote'),

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

polls/views.py

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 += 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 调用。

  • 如果 choice 未在 POST 数据中提供,request.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})

这与 教程 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/ 并为问题投票。 您应该会看到每次投票时都会更新的结果页面。 如果您在未选择选项的情况下提交表单,您应该会看到错误消息。

笔记

我们的 vote() 视图的代码确实有一个小问题。 它首先从数据库中获取selected_choice对象,然后计算votes的新值,然后将其保存回数据库。 如果您网站的两个用户尝试在 完全相同的时间 投票,这可能会出错:将为 votes 检索相同的值,例如 42。 然后,对于两个用户,计算并保存新值 43,但 44 将是预期值。

这称为 竞争条件 。 如果您有兴趣,可以阅读 Avoiding Race conditions using 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.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>


改良视图

接下来,我们将删除旧的 indexdetailresults 视图并使用 Django 的通用视图代替。 为此,请打开 polls/views.py 文件并像这样更改它:

polls/views.py

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.

我们在这里使用两个通用视图: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[ X199X] 幕后花絮。

类似地,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 部分 以了解如何测试我们的投票应用程序。