民意调查Django实现(四)
我们接着上一小节的末尾开始学习,在上一个小节中,我们主要是了解了Django中的模板,即templates的使用。在这个小节中,我们主要关注于简单的表单处理并且裁剪我们的代码。
编写一个简单的表单
我们来更新一下我们的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展示了一组单选按钮。每一个按钮的值(value)是相关问题的选项ID。每一个按钮的名称是”choice”。也就是意味着,当用户选择其中一个选项框并且提交表单的时候,将会发送post数据choice=#
,其中#就是对应的选择的选项的ID。这个是HTML表单的基本概念。
- 我们设置表单的action为{% url 'polls:vote' question.id %}
,并且我们设置method="post"
。使用post方式(和使用get方式对应)是非常重要的,因为提交表单的行为将会改变服务端的数据。无论你什么时候创建能够修改服务端数据的表单的时候,一定要使用methon="post"
。这个技巧不是Django专有的。这个仅仅就是好的网页开发技巧。
- forloop.counter标示for标签在这个循环中出现的次数。
- 既然我们创建了一个POST表单(该表单可能会影响修改数据),我们就需要去担心通过站点请求的伪装。幸运的是,我们不必太过担心,因为Django提供了一个简单易用的系统来阻止他。简单来说,目标是内部URLs的所有的POST表单都应该使用{% crsf_token %}
模板标签。
现在,我们来创建处理提交数据的视图。在上一个小节中,我们创建了包含下面这一行的URLconf。
polls/urls.py
url(r'^(?P<question_id>\d+)/vote/$',views.vote,name='vote'),
我们也创造了一个vote()函数的临时实现。现在我们来真正使用起来。向polls/views.py中添加下面的代码:
# -*- coding:utf-8 -*-
from django.http import HttpResponse,HttpResponseRedirect
from django.shortcuts import get_object_or_404,render
from django.core.urlresolvers import reverse
from polls.models import Question
....
def vote(request,question_id):
p = get_object_or_404(Question,pk=question_id)
try:
selected_choice = p.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExit):
return render(request,'polls/detail.html',{
'question':p,
'errot_message':"You did not select a choice."
})
else:
selected_choice.votes += 1
selected_choice.save()
# 每次成功处理POST数据之后一定要返回HttpResponseRedirect。
# 这个可以避免二次提交
return HttpResponseRedirect(reverse('polls/results',args=(p.id)))
该代码包含下面几个事情:
request.POST是一个类似于字典的对象,能够使我们通过键名称去访问提交的数据。在这个例子中,
request.POST['choice']
返回的是选中的选项的ID,这里是字符串。request.POST
值一直是字符串。
注意,Django也提供了request.GET
去访问GET数据的方式,但是对于request.POST
来说只有在代码中才能获取到相应的数据,这个大家可以去了解get方式请求和post方式请求的区别。如果在POST数据中没有提供choice选项,那么
request.POST['choice']
将会抛出KeyError异常。上面的代码检查KeyError异常,如果没有提供choice的话,重新显示带有错误信息的question表单。- 在增加了选项数量之后,代码返回一个HttpResponseRedirect而不是一个正常的HttpResponse。HttpResponseRedirect使用一个单一的参数:也就是用户将要重定向的url地址。
- 在这个例子的HttpResponseRedirect结构中我们使用reverse()函数。这个函数帮助我们避免发生视图函数中硬编码一个URL地址。他被给予我们想要传递到的视图名称和指向视图的URL正则中的变量部分。在这个例子中,使用我们在上一小节中设置的URLconf,最终reverse()调用将会返回类似于下面的字符串:
'/polls/3/results'
,其中的3是p.id
的值。这个重定向URL然后调用’results’视图去展示最终的页面。
在用户对相应问题投票之后,vote()视图将会重定向到这个问题的results页面。现在我们来重写这个页面:
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
下面我们来添加results模板:
创建一个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/中去为相应的问题去投票。每次投票后你都能看到相应的结果更新。如果你没有选择选项而点击提交的话,你将会看到错误信息。
下面来看一下我的运行结果:
使用通用视图:减少代码更好
detail()和results()视图是非常简单的 - 就像是上面提到的,太冗余了。展示投票列表的index()视图也是一样的。
这些视图代表着基本Web开发的通用例子:通过URL传来的参数从数据库获取数据,加载模板并且返回一个渲染模板。因为这些都是雷同的,所以Django提供了一个快捷方式,成为”通用视图”系统。
通用试图系统抽象除了通用正则,这使得我们可能不需要编写Python代码也能构件一个应用。
下面让我们的应用使用通用视图,所以我们可以删除一大些代码了。我们仅仅需要几步就能做出转换。
- 转换URLconf
- 删除一些旧的,不需要的视图
- 基于Django的通用视图引入新的视图
修改URLconf
首先,打开polls/urls.py并且像下面这样修改:
from django.conf.urls import patterns,url
from polls import views
urlpatterns = patterns('',
# ex: /polls/
url(r'^$', views.IndexView.as_view(),name='index'),
# ex: /polls/5/
url(r'^(?P<pk>\d+)/$',views.DetailView.as_view(),name='detail'),
# ex: /polls/5/results
url(r'^(?P<pk>\d+)/results/$',views.ResultsView.as_view(),name='results'),
# ex: /polls/5/vote/
url(r'^(?P<question_id>\d+)/vote/$',views.vote,name='vote'),
)
注意,正则匹配中间的<question_id>
改成了<pk>
。
修改视图
下一步,我们将要移除我们旧的index,detail和results视图,然后使用Django的通用视图。打开polls/view.py文件,并且像下面这样修改:
# -*- coding:utf-8 -*-
from django.http import HttpResponse,HttpResponseRedirect
from django.shortcuts import get_object_or_404,render
from django.core.urlresolvers import reverse
from django.views import generic
from polls.models import 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”的主键,这就是为什么刚刚在urls.py中修改question_id
为pk
的原因了。
默认情况下,DetailView通用视图使用称为<app name>/<model name>_detail.html
作为模板。在我们的例子中,他会使用模板”polls/question_detail.html”。属性template_name
被用来告诉Django使用能够哪一个模板代替默认模板。我们也为results指定了template_name。
相似的,ListView通用视图使用默认的模板,称为<app name>/<model name>_list.html
,我们同样为他指定了我们自己的模板。
在之前的小节中,模板已经被提供了context,也就是上下文,该上下文包含question和latest_question_list变量。对于DetailView,question已经被自动提供了。我们使用Django的模型(Question),所以Django能够决定为上下文变量使用一个近似的名称。然而,对于ListView,自动生成的上下文变量是question_list。为了重写,我们可以使用context_object_name
属性,指定我们想要使用的latest_question_list
。这样的话,通过Django通用视图提供的属性,我们就可以指定我们想使用的变量了。
下面我们来运行一下,看一下效果:
运行效果和上面的。