ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] 这一节我们将继续编写投票应用,并专注于简单的表单处理,以及精简我们的代码。 <br /> ## **一、表单form** 为了接收用户的投票选择,我们需要在前端页面显示一个投票界面。让我们重写先前的`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"。这意味着,当你选择了其中某个按钮,并提交表单,一个包含数据`choice=#`的POST请求将被发送到指定的url,`#`是被选择的选项的ID。这就是HTML表单的基本概念。 * 如果你有一定的前端开发基础,那么form标签的action属性和method属性你应该很清楚它们的含义,action表示你要发送的目的url,method表示提交数据的方式,一般分post和get。 * `forloop.counter`是Django模板系统专门提供的一个变量,用来表示你当前循环的次数,一般用来给循环项目添加有序数标。 * 由于我们发送了一个POST请求,就必须考虑一个跨站请求伪造的安全问题,简称CSRF(具体含义请百度)。Django为你提供了一个简单的方法来避免这个困扰,那就是在form表单内添加一条`{% csrf_token %}`标签,标签名不可更改,固定格式,位置任意,只要是在form表单内。这个方法对form表单的提交方式方便好使,但如果是用ajax的方式提交数据,那么就不能用这个方法了。 现在,让我们创建一个处理提交过来的数据的视图。前面我们已经写了一个“占坑”的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`是一个类似字典的对象,允许你通过键名访问提交的数据。本例中,`request.POST[’choice’]`返回被选择选项的ID,并且值的类型永远是string字符串,哪怕它看起来像数字!同样的,你也可以用类似的手段获取GET请求发送过来的数据,一个道理。 * `request.POST[’choice’]`有可能触发一个KeyError异常,如果你的POST数据里没有提供choice键值,在这种情况下,上面的代码会返回表单页面并给出错误提示。 * 在选择计数器加一后,返回的是一个`HttpResponseRedirect`而不是先前我们常用的`HttpResponse`。`HttpResponseRedirect`需要一个参数:重定向的URL。这里有一个建议,当你成功处理POST数据后,应当保持一个良好的习惯,始终返回一个`HttpResponseRedirect`。这不仅仅是对Django而言,它是一个良好的WEB开发习惯。 * 我们在上面`HttpResponseRedirect`的构造器中使用了一个`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_course\mysite>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> ~~~ <br /> ## **二、 使用通用视图:减少重复代码** 上面的detail、index和results视图的代码非常相似,有点冗余,这是一个程序猿不能忍受的。他们都具有类似的业务逻辑,实现类似的功能:通过从URL传递过来的参数去数据库查询数据,加载一个模板,利用刚才的数据渲染模板,返回这个模板。由于这个过程是如此的常见,Django很善解人意的帮你想办法偷懒,于是它提供了一种快捷方式,名为“通用视图”。 现在,让我们来试试看将原来的代码改为使用通用视图的方式,整个过程分三步走: * 修改URLconf设置 * 删除一些旧的无用的视图 * 采用基于类视图的新视图 **PS:为什么本教程的代码来回改动这么频繁?** <br /> 答:通常在写一个Django的app时,我们一开始就要决定使用通用视图还是不用,而不是等到代码写到一半了才重构你的代码成通用视图。但是本教程为了让你清晰的理解视图的内涵,“故意”走了一条比较曲折的路,因为我们的哲学是`在你使用计算器之前你得先知道基本的数学知识`。 <br /> Django的视图类型可以分为函数视图和类视图,也就是FBV和CBV,两者各有优缺点,CBV不一定就高大上。大多数场景下,函数视图更简单易懂,代码量更少。但是在需要继承、封装某些视图的时候,CBV就能发挥优势。 <br /> 这节介绍的通用视图其实就是Django内置的一些类视图,可以拿来直接使用。但非常简单,只适用于一些简单场景,如果业务逻辑比较复杂,依然需要改造。 <br /> ### 1.**改良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>`. <br /> ### 2.**修改视图** 接下来,打开`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>`。 <br /> 默认情况下,`DetailView`通用视图使用一个称作`<app name>/<model name>_detail.html`的模板。在本例中,实际使用的是`polls/detail.html`。`template_name`属性就是用来指定这个模板名的,用于代替自动生成的默认模板名。(一定要仔细观察上面的代码,对号入座,注意细节。)同样的,在results列表视图中,指定`template_name`为`'polls/results.html'`,这样就确保了虽然resulst视图和detail视图同样继承了DetailView类,使用了同样的model:Qeustion,但它们依然会显示不同的页面。(模板不同嘛!so easy!) <br /> 类似的,ListView通用视图使用一个默认模板称为`<app name>/<model name>_list.html`。我们也使用`template_name`这个变量来告诉ListView使用我们已经存在的`"polls/index.html"`模板,而不是使用它自己默认的那个。 <br /> 在教程的前面部分,我们给模板提供了一个包含`question`和`latest_question_list`的上下文变量。而对于DetailView,question变量会被自动提供,因为我们使用了Django的模型(Question),Django会智能的选择合适的上下文变量。然而,对于ListView,自动生成的上下文变量是`question_list`。为了覆盖它,我们提供了`context_object_name`属性,指定说我们希望使用`latest_question_list`而不是`question_list`。 <br /> 现在可以运行开发服务器,然后试试基于类视图的应用程序了,效果和前面的函数视图是一样的。 <br /> 类视图是Django比较高级的一种用法,初学可能不太好理解,没关系,我们先有个印象。更多内容可以学习博客:https://www.liujiangblog.com/blog/37/ <br /> 简要分析: 1. 类视图相比函数视图具有类的特性,可封装可继承,利于代码重用 2. 通用视图是类视图的一种 3. 通用视图的代码虽然少了,但学习成本高了 4. 我们在享受便利的同时,要记住更多通用视图的用法和规则,有得有失 5. 其实我们完全可以自己编写新的通用视图,自己定规则定规矩,不必使用Django提供的,这相当于造轮子 6. 不要沉迷于类视图的强大。在编程的世界其实有句话也很适合:一切的馈赠在初始就定好了代价。获得越多,失去也多,这里方便了,那里就复杂了。