Part4 表单处理和通用视图

这篇教程会从第三节结束的地方继续,我们继续开始处理web投票应用,并且将精力放在简单表单处理,并且分割我们的代码。

写一个简单的表单

我们来升级一下上一节里的poll detail模板("polls/detail.html"),模板包含一个HTML<form>元素:

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

简要说明:

  • 上面的模板里为每一个questionchoice提供一个单选按钮,每一个单选按钮的value都会关联到一个questionchoice对应的ID。每个单选按钮的名称是choice。这意味着,当某人选择其中一个单独按钮,并保存表单。就会发送POST数据choice=#。而#就是被选择的choice的ID。这就是HTML表单的基本思想。
  • 我们设置表单的动作是{% 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视图来处理提交的数据。记住,在上一节中,我们为polls应用创建了下面的URLconf:

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

我们还创建了vote()方法的一个虚假实现。现在我们来创建一个有真正功能的版本,把下面的代码添加到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']以字符串形式返回被选择的choice的ID。request.POST值一般都是字符串
    注意,Django也提供request.GET用相同的方式来访问GET数据。但是我们在我们的代码里明确使用request.POST,来保证数据只能通过一个POST调用来修改。
  • requeset.POST['choice']在POST数据中没有提供chocie数据的时候会抛出一个KeyError异常。上面的代码会在没有给出choice的时候检查KeyError,并且重新显示带有一个错误信息的question表单。
  • 增加选择计数以后,代码返回一个HttpResponseRedirect而不是之前的HttpResponseHttpResponseRedirect只带一个参数:用户会被重定向的位置URL(继续看下面的内容,我们会说明这个例子里的URL)
    上面的Python说明里指出,你在成功处理完POST数据以后应该返回一个HttpResponseRedirect,这个提示也不是针对Django,也只是一个很好的Web开发实践。
  • 我们在这个例子里的HttpResponseRedirect构造函数里使用reverse()方法,这个方法帮助避免在视图函数里硬编码URL。它给出了我们想要传递控制权的视图的名称以及指向该视图的URL模式的可变部分。在这个例子里,使用我们在上一节中设置的URLconf,这个reverse()调用会返回一个像下面这样的字符串:
    '/polls/3/results/'
    3就是question.id的值,这个重定向URL稍后会调用results视图来显示最终的页面

像上一节里提到的,request是一个HttpRequest对象,想知道这个对象的更多信息,可以查看Django官方的帮助文档。

当某人在给某个question投了一票,vote()视图就会重定向到这个question的结果页面。我们来写这个视图:

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模板:

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

现在,在你的浏览器里输入地址"http://localhost:8000/polls/1/",然后给这个问题投票。你会看到一个结果页面,你投票一次,结果就会更新一次。如果你在提交页面的时候没有选择任何选项,会给出错误信息。

注意
我们的vote()视图的代码还有一点小问题。它首先从数据中取到selected_choice对象,然后计算votes的新值,再然后将它存回数据库。如果你的网站上有两个用户同时为同一个问题投票。就可能会出现问题:例如投票之前的值是42,会作为vote()视图获取到的值,两个用户同时投票时都是计算得到新的值43然后保存到数据库,而我们想要的值却是44.
这种情况被称为竞赛情况,如果你对这个感兴趣,你可以查看官方文档《使用F()来避免竞赛情况》来解决这个问题。

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

detail()results()视图都非常简单,像上面提到的那样,很多内容也是多余的。index()视图用来显示polls列表,也和它们功能类似。
这些视图显示了Web开发过程中的一个典型例子:根据URL里传递的参数到数据库中取数据,加载模板并且返回渲染后的模板。因为这些都是功能都是一样的,所以Django提供了更简单的方式,叫做通用视图系统。
通用视图抽象常用的模式,甚至不需要编写Python代码来编写应用。
我们来将poll应用转换到通用视图视图,所以我们可以删除很大一部分代码,我们需要执行下面的几步来执行我们转换:

  1. 转换URLconf
  2. 删除旧的不需要的视图
  3. 介绍新的基于Django通用视图系统的视图

阅读下面的详细信息

为什么要代码重构?

通常情况下,写一个Django应用的时候,你会去评估使用通用视图,对于你遇到的问题来说是不是一个很好的解决方式。并且从最开始就使用这种方式,而不是在中途开始重构你的代码。但是这个教程中故意把重点放在视图上,直到现在才开始关注核心思想。
就像在你开始使用一个计算器的时候你必须知道一些基础的数学知识一样。

修改URLconf

首先,打开polls/urls.py里的URLconf,然后修改成下面的样子:

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'),
]

注意第二个和第三个模式里path字符串中的匹配模式名称,将<question_id>修改为<pk>

修改视图

下一步,我们来移除旧的indexdetailresults视图,使用Django通用视图来代替。要这样做的话,打开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>/<modelname>_detail.html的模板,在我们的例子里,我们使用模板"polls/question_detail.html"template_name属性用来告诉Django使用一个特定的模板名称,而不是一个自动生成的默认模板名称。我们也为results列表视图指定templat_name。这保证了results视图和detail视图在渲染的时候会有一个不同的样式,即使是它们在后台使用了相同的DetailView

相似的,ListView通用视图使用一个叫做 <app name>/<model name>_list.html的模板,我们使用template_name告诉ListView来使用现有的"polls/index.html"模板。

在这个教程之前的部分中,提供了一个模板和一个包含questionlatest_question_list变量的上下文管理器。对于DetailViewquestion变量是自动提供的—从我们开始使用一个Django模型(Question)开始,Django能够为上下文管理器变量确定一个不同的值。然后,对于ListViews来说,自动生成的上下文管理器变量是question_list。为了覆盖这个变量,我们提供context_object_name属性,指定我们想要使用latest_question_list。作为一种替代方法,你可以改变你的模板来匹配新的默认上下文管理器变量。但是直接告诉Django你想要使用的变量更简单。

运行开发服务器,并且根据通用视图使用新的poll应用程序。

对于更多的通用视图信息,可以查看Django官方文档里通用视图的内容。

当你对表单和通用视图的内容都理解了以后,就可以继续学习第5节,学习如何测试我们的投票应用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342