这一节我们将继续编写投票应用,并专注于简单的表单处理,以及精简我们的代码。
为了接收用户的投票选择,我们需要在前端页面显示一个投票界面。让我们重写先前的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>
简要说明:
choice=#
的POST请求将被发送到指定的url,#
是被选择的选项的ID。这就是HTML表单的基本概念。现在,让我们创建一个处理提交过来的数据的视图。前面我们已经写了一个“占坑”的vote视图的url(polls/urls.py):
path('<int:question_id>/vote/', views.vote, name='vote'),
以及“占坑”的vote视图函数(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): return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: selected_choice.votes += 1 selected_choice.save() return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
有些新的东西,我们要解释一下:
request.POST[’choice’]
返回被选择选项的ID,并且值的类型永远是string字符串,那怕它看起来像数字!同样的,你也可以用类似的手段获取GET请求发送过来的数据,一个道理。request.POST[’choice’]
有可能触发一个KeyError异常,如果你的POST数据里没有提供choice键值,在这种情况下,上面的代码会返回表单页面并给出错误提示。PS:通常我们会给个默认值,防止这种异常的产生,例如request.POST[’choice’,None]
,一个None解决所有问题。HttpResponseRedirect
而不是先前我们常用的HttpResponse
。HttpResponseRedirect需要一个参数:重定向的URL。这里有一个建议,当你成功处理POST数据后,应当保持一个良好的习惯,始终返回一个HttpResponseRedirect。这不仅仅是对Django而言,它是一个良好的WEB开发习惯。reverse()
函数。它能帮助我们避免在视图函数中硬编码URL。它首先需要一个我们在URLconf中指定的name,然后是传递的数据。例如'/polls/3/results/'
,其中的3是某个question.id
的值。重定向后将进入polls:results
对应的视图,并将question.id
传递给它。白话来讲,就是把活扔给另外一个路由对应的视图去干。当有人对某个问题投票后,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})
同样,还需要写个模板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/
了,投票吧。你会看到一个结果页面,每投一次,它的内容就更新一次。如果你提交的时候没有选择项目,则会得到一个错误提示。
如果你在前面漏掉了一部分操作没做,比如没有创建choice选项对象,那么可以按下面的操作,补充一下:
F:Django_coursemysite>python manage.py shell Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> from polls.models import Question >>> q = Question.objects.get(pk=1) >>> q.choice_set.create(choice_text='Not much', votes=0) <Choice: Choice object> >>> q.choice_set.create(choice_text='The sky', votes=0) <Choice: Choice object> >>> q.choice_set.create(choice_text='Just hacking again', votes=0) <Choice: Choice object>
上面的detail、index和results视图的代码非常相似,有点冗余,这是一个程序猿不能忍受的。他们都具有类似的业务逻辑,实现类似的功能:通过从URL传递过来的参数去数据库查询数据,加载一个模板,利用刚才的数据渲染模板,返回这个模板。由于这个过程是如此的常见,Django很善解人意的帮你想办法偷懒,于是它提供了一种快捷方式,名为“通用视图”。
现在,让我们来试试看将原来的代码改为使用通用视图的方式,整个过程分三步走:
PS:为什么本教程的代码来回改动这么频繁?
答:通常在写一个Django的app时,我们一开始就要决定使用通用视图还是不用,而不是等到代码写到一半了才重构你的代码成通用视图。但是本教程为了让你清晰的理解视图的内涵,“故意”走了一条比较曲折的路,因为我们的哲学是在你使用计算器之前你得先知道基本的数学公式
。
打开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'), ]
请注意:在上面的的第2,3条目中将原来的<question_id>
修改成了<pk>
.
接下来,打开polls/views.py
文件,删掉index、detail和results视图,替换成Django的通用视图,如下所示:
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): ... # 同前面的一样,不需要修改
在这里,我们使用了两种通用视图ListView
和DetailView
(它们是作为父类被继承的)。这两者分别代表“显示一个对象的列表”和“显示特定类型对象的详细页面”的抽象概念。
每一种通用视图都需要知道它要作用在哪个模型上,这通过model属性提供。
DetailView
需要从url捕获到的称为"pk"的主键值,因此我们在url文件中将2和3条目的<question_id>
修改成了<pk>
。
默认情况下,DetailView
通用视图使用一个称作<app name>/<model name>_detail.html
的模板。在本例中,实际使用的是polls/detail.html
。template_name
属性就是用来指定这个模板名的,用于代替自动生成的默认模板名。(一定要仔细观察上面的代码,对号入座,注意细节。)同样的,在resutls列表视图中,指定template_name
为'polls/results.html'
,这样就确保了虽然resulst视图和detail视图同样继承了DetailView类,使用了同样的model:Qeustion,但它们依然会显示不同的页面。(模板不同嘛!so easy!)
类似的,ListView通用视图使用一个默认模板称为<app name>/<model name>_list.html
。我们也使用template_name
这个变量来告诉ListView使用我们已经存在的 "polls/index.html"
模板,而不是使用它自己默认的那个。
在教程的前面部分,我们给模板提供了一个包含question
和latest_question_list
的上下文变量。而对于DetailView,question变量会被自动提供,因为我们使用了Django的模型(Question),Django会智能的选择合适的上下文变量。然而,对于ListView,自动生成的上下文变量是question_list
。为了覆盖它,我们提供了context_object_name
属性,指定说我们希望使用latest_question_list
而不是question_list
。
现在可以运行开发服务器,然后试试基于类视图的应用程序了。类视图是Django比较高级的一种用法,初学可能不太好理解,没关系,我们先有个印象。