接Django 2.0 学习(06):Django 视图(进阶),我们将聚焦在使用简单的表单进行处理和精简代码。
编写简单表单
我们将用下面的代码,来替换之前的detail模板("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>
快速理解:
- 上述模板为每个问题选项添加了一个单选按钮,该按钮的值与问题选项的ID关联,每个按钮的名字都是"choice".这就意味着,当有人选择某个单选按钮并且提交的时候,选择的单选按钮会发送POST数据choice=#,其中#就是选择的choice;
- 设置表单的action为{% url 'polls:vote' question.id %},设置method="post";这部分内容已经超出Django的范畴,属于Web开发的范畴;
- 在使用POST表单时,需要考虑到跨站点请求伪造(Cross Site Request Forgeries).幸运的是,我们不需要考虑的太多,因为Django已经帮我们处理的很好了。简言之,我们所需要做的是:在POST表单请求里面添加{% csrf_token %}模板标签。
接下来,创建视图函数来对提交的数据进行处理。编辑polls/views.py,其代码如下:
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是个像字典的对象,可以通过该对象的关键字(key)来获取提交数据。在这里,request.POST['choice']返回被选择选项的ID,request.POST返回值总是字符串;
- 如果在POST数据中没有提供choice,request.POST['choice']将会抛出KeyError异常。如果未提供choice,上述代码会检查KeyError并且通过一个错误信息来重新显示该问题;
- 在增加选择计数后,代码返回HttpResponseRedirct而不是HttpResponse.HttpResponseRedirect携带一个单独的参数:用户被重定向到哪里的URL;作为优秀的Web开发,POST请求成功应该总是返回HttpResponseRedirect,这并不是Django特别要求的;
- 在HttpResponseRedirect构造其中,我们使用了reverse()方法。该方法避免在视图函数中使用URL硬编码,他会提供我们想传递的视图函数的名字并且在ULR模式中找到该视图。
在有人对某个问题投票后,vote()视图会把结果重定向到结果页面,下面代码是完善的视图函数:
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
现在创建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>
request就是个HttpRequest对象,更多信息详见HttpRequest
现在,在浏览器中访问/polls/1/并且对问题进行投票。我们会看到如下页面:
如果没有选择任何选项,会看到错误信息,如下图所示:
注:上面的vote()视图有个小小的问题。该函数第一次从数据库中获取selected_choice对象,接下来计算新的votes的值,并且将其保存回数据库中。同一时刻,如果有两个用户都发起投票,就会出现下面的错误:将会得到相同的votes值,假设现在的值是42,然而对两个用户来说43这个新的值将会被计算和保存,但是我们期望的值是44。
通用视图:更少的代码更好
上述视图函数代表了基本Wbe开发的一种通用情况:根据URL传递的参数从数据库中获取数据,加载模板并返回渲染后的模板。因为这是公共的,所以Django提供了一种快捷方式,称之为"通用视图"系统。
让我们使用通用视图系统来改造之前的代码,其步骤如下:
1、改变URLconf;
2、删除旧的、非必要的视图函数;
3、基于Django的通用视图,引入新的视图;
首先,修改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
其次,修改视图函数(使用Django的通用视图系统替换旧的index,detail和results视图):打开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 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):
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,)))
# return HttpResponse("You're voting on question %s." % question_id)
我们这里使用两个通用视图:ListView和DetailView,这两个视图分别代表了"显示对象的列表"和"显示某个对象的详细信息"。
- 使用模型属性,每个通用视图需要知道它所作用的模型;
- DetailView通用视图期望主键值能通过URL中获取到,所以在通用视图中我们把question_id改成pk;
默认情况,DetailView通用视图使用一个叫做app name/model name_detail.html的模板。在我们的示例中,将会使用模板"polls/question_detail.html"。template_name属性用来告诉Django使用一个具体的名字代替自动生成的默认的模板名字。
同样的,ListView通用视图使用默认模板app name/model name_list.html;我们使用template_name告诉ListView使用我们的polls/index.html模板。