编写第一个django应用程序,第4部分

本教程从以下位置开始 Tutorial 3 停下来了。我们将继续使用网络投票应用程序,并将重点放在表单处理和减少代码上。

从何处获得帮助:

如果您在阅读本教程时遇到困难,请转到 Getting Help 常见问题部分。

写一个最小的表格

让我们从上一个教程中更新我们的投票详细信息模板(“poll s/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是相关问题选项的ID。 name 每个单选按钮的 "choice" . 这意味着,当有人选择其中一个单选按钮并提交表单时,它将发送日志数据。 choice=# 其中是所选选项的ID。这是HTML表单的基本概念。

  • 我们设置了表单的 action{% url 'polls:vote' question.id %} ,然后我们设置 method="post" 。vbl.使用 method="post" (相对于 method="get" )非常重要,因为提交此表单的行为将改变数据服务器端。每当您创建更改数据服务器端的表单时,请使用 method="post" 。这篇技巧并不是专门针对Django的;总的来说,它是很好的Web开发实践。

  • forloop.counter 指示 for 标签已通过其循环

  • 因为我们要创建一个POST表单(它可能具有修改数据的效果),所以我们需要担心跨站点的请求伪造。谢天谢地,您不必太担心,因为Django附带了一个有用的系统来防范它。简而言之,所有以内部url为目标的POST表单都应该使用 {{% csrf_token %}} 模板标签。

现在,让我们创建一个Django视图,它处理提交的数据并对其进行处理。记住,在 Tutorial 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 在我们的代码中,为了确保数据只通过一个后调用进行更改。

  • request.POST['choice'] 将提高 KeyError 如果 choice 在post数据中没有提供。以上代码检查 KeyError 如果 choice 没有给出。

  • 增加选项计数后,代码返回 HttpResponseRedirect 而不是正常的 HttpResponse . HttpResponseRedirect 只接受一个参数:用户将被重定向到的URL(请参阅以下几点,了解在本例中如何构造URL)。

    正如上面的Python注释所指出的,您应该始终返回一个 HttpResponseRedirect 在成功地处理了POST数据之后。这篇技巧并不是专门针对Django的;总的来说,它是很好的Web开发实践。

  • 我们正在使用 reverse() 功能在 HttpResponseRedirect 本例中的构造函数。此函数有助于避免在视图函数中硬编码URL。它给出了我们想要传递控制权的视图的名称,以及指向该视图的URL模式的变量部分。在这种情况下,使用我们在中设置的urlconf Tutorial 3 ,这个 reverse() 调用将返回如下字符串:

    "/polls/3/results/"
    

    何处 3 是价值 question.id . 然后,此重定向的URL将调用 'results' 查看以显示最后一页。

如上所述 Tutorial 3request 是一个 HttpRequest 对象。更多关于 HttpRequest 对象,请参见 request and response documentation .

在某人在一个问题上投票后, 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})

这和 detail() 从视图 Tutorial 3 . 唯一的区别是模板名。我们稍后会解决这个冗余问题。

现在,创建一个 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 ,然后将其保存回数据库。如果你网站的两个用户试图投票 完全相同的时间 ,这可能会出错:相同的值,例如42,将被检索到 votes . 然后,对两个用户来说,新值43被计算并保存,但44将是预期值。

这叫A 竞争条件 . 如果你感兴趣,你可以阅读 避免比赛条件使用 F() 学习如何解决这个问题。

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

这个 detail() (从 Tutorial 3results() 视图非常短,正如上面提到的,是多余的。这个 index() 显示投票列表的视图类似。

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

泛型视图将常见模式抽象到您甚至不需要编写Python代码来编写应用程序的程度。例如, ListViewDetailView 泛型视图分别抽象了“显示对象列表”和“显示特定类型对象的详细信息页”的概念。

让我们转换我们的投票应用程序使用通用视图系统,这样我们就可以删除一堆我们自己的代码。我们得采取一些步骤来进行转换。我们将:

  1. 转换urlconf。

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

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

请继续阅读以了解详细信息。

为什么要洗牌?

通常,在编写Django应用程序时,您将评估通用视图是否适合您的问题,并从一开始就使用它们,而不是中途重构代码。但直到现在,本教程有意将重点放在“硬方法”视图的编写上,以集中于核心概念。

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

乌尔康夫修正案

首先,打开 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.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 = QuestionDetailViewResultsView )或通过定义 get_queryset() 方法(如中所示 IndexView )。

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

同样, ListView 通用视图使用名为 <app name>/<model name>_list.html 我们使用 template_name 告诉 ListView 利用我们现有的 "polls/index.html" 模板。

在本教程的前几部分中,为模板提供了一个上下文,其中包含 questionlatest_question_list 上下文变量。为了 DetailView 这个 question 变量是自动提供的——因为我们使用的是Django模型 (Question ,django可以确定上下文变量的适当名称。但是,对于ListView,自动生成的上下文变量是 question_list . 为了克服这一点,我们提供 context_object_name 属性,指定要使用的 latest_question_list 相反。作为一种替代方法,您可以更改模板以匹配新的默认上下文变量,但是告诉Django使用您想要的变量要容易得多。

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

有关常规视图的完整详细信息,请参见 generic views documentation .

当您对表单和一般视图感到满意时,请阅读 part 5 of this tutorial 了解如何测试我们的投票应用程序。