这篇教程会从第三节结束的地方继续,我们继续开始处理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>
简要说明:
- 上面的模板里为每一个
question
的choice
提供一个单选按钮,每一个单选按钮的value
都会关联到一个question
的choice
对应的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
而不是之前的HttpResponse
。HttpResponseRedirect
只带一个参数:用户会被重定向的位置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应用转换到通用视图视图,所以我们可以删除很大一部分代码,我们需要执行下面的几步来执行我们转换:
- 转换URLconf
- 删除旧的不需要的视图
- 介绍新的基于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>
。
修改视图
下一步,我们来移除旧的index
、detail
和 results
视图,使用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.
我们在这里使用了连个通用视图:ListView
和DetailView
。这两个视图的思想分别抽象自“显示一个对象列表”和“为一个特定类型的对象显示一个详细页面”。
- 每个通用视图需要知道它要采用哪种模式,这是通过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"
模板。
在这个教程之前的部分中,提供了一个模板和一个包含question
和latest_question_list
变量的上下文管理器。对于DetailView
,question
变量是自动提供的—从我们开始使用一个Django模型(Question)开始,Django能够为上下文管理器变量确定一个不同的值。然后,对于ListViews
来说,自动生成的上下文管理器变量是question_list
。为了覆盖这个变量,我们提供context_object_name
属性,指定我们想要使用latest_question_list
。作为一种替代方法,你可以改变你的模板来匹配新的默认上下文管理器变量。但是直接告诉Django你想要使用的变量更简单。
运行开发服务器,并且根据通用视图使用新的poll应用程序。
对于更多的通用视图信息,可以查看Django官方文档里通用视图的内容。
当你对表单和通用视图的内容都理解了以后,就可以继续学习第5节,学习如何测试我们的投票应用。