Part 4:表单和类视图

阅读: 134045     评论:72

这一节我们将继续编写投票应用,并专注于简单的表单处理,以及精简我们的代码。

一、表单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而不是先前我们常用的HttpResponseHttpResponseRedirect需要一个参数:重定向的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>

二、 使用通用视图:减少重复代码

上面的detail、index和results视图的代码非常相似,有点冗余,这是一个程序猿不能忍受的。他们都具有类似的业务逻辑,实现类似的功能:通过从URL传递过来的参数去数据库查询数据,加载一个模板,利用刚才的数据渲染模板,返回这个模板。由于这个过程是如此的常见,Django很善解人意的帮你想办法偷懒,于是它提供了一种快捷方式,名为“通用视图”。

现在,让我们来试试看将原来的代码改为使用通用视图的方式,整个过程分三步走:

  • 修改URLconf设置
  • 删除一些旧的无用的视图
  • 采用基于类视图的新视图

PS:为什么本教程的代码来回改动这么频繁?

答:通常在写一个Django的app时,我们一开始就要决定使用通用视图还是不用,而不是等到代码写到一半了才重构你的代码成通用视图。但是本教程为了让你清晰的理解视图的内涵,“故意”走了一条比较曲折的路,因为我们的哲学是在你使用计算器之前你得先知道基本的数学知识

Django的视图类型可以分为函数视图和类视图,也就是FBV和CBV,两者各有优缺点,CBV不一定就高大上。大多数场景下,函数视图更简单易懂,代码量更少。但是在需要继承、封装某些视图的时候,CBV就能发挥优势。

这节介绍的通用视图其实就是Django内置的一些类视图,可以拿来直接使用。但非常简单,只适用于一些简单场景,如果业务逻辑比较复杂,依然需要改造。

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>.

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):
    ... # 同前面的一样,不需要修改

在这里,我们使用了两种通用视图ListViewDetailView(它们是作为父类被继承的)。这两者分别代表“显示一个对象的列表”和“显示特定类型对象的详细页面”的抽象概念。

  • 每一种通用视图都需要知道它要作用在哪个模型上,这通过model属性提供。

  • DetailView需要从url捕获到的称为"pk"的主键值,因此我们在url文件中将2和3条目的<question_id>修改成了<pk>

默认情况下,DetailView通用视图使用一个称作<app name>/<model name>_detail.html的模板。在本例中,实际使用的是polls/detail.htmltemplate_name属性就是用来指定这个模板名的,用于代替自动生成的默认模板名。(一定要仔细观察上面的代码,对号入座,注意细节。)同样的,在results列表视图中,指定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"模板,而不是使用它自己默认的那个。

在教程的前面部分,我们给模板提供了一个包含questionlatest_question_list的上下文变量。而对于DetailView,question变量会被自动提供,因为我们使用了Django的模型(Question),Django会智能的选择合适的上下文变量。然而,对于ListView,自动生成的上下文变量是question_list。为了覆盖它,我们提供了context_object_name属性,指定说我们希望使用latest_question_list而不是question_list

现在可以运行开发服务器,然后试试基于类视图的应用程序了,效果和前面的函数视图是一样的。

类视图是Django比较高级的一种用法,初学可能不太好理解,没关系,我们先有个印象。更多内容可以学习博客:https://www.liujiangblog.com/blog/37/

简要分析:

  1. 类视图相比函数视图具有类的特性,可封装可继承,利于代码重用
  2. 通用视图是类视图的一种
  3. 通用视图的代码虽然少了,但学习成本高了
  4. 我们在享受便利的同时,要记住更多通用视图的用法和规则,有得有失
  5. 其实我们完全可以自己编写新的通用视图,自己定规则定规矩,不必使用Django提供的,这相当于造轮子
  6. 不要沉迷于类视图的强大。在编程的世界其实有句话也很适合:一切的馈赠在初始就定好了代价。获得越多,失去也多,这里方便了,那里就复杂了。

 Part 3:视图和模板 Part 5:测试 

评论总数: 72


点击登录后方可评论

pk=request.POST['choice']这里的choice是哪里来的 还是固定就是这样。



刘老师,请问下面这个问题怎么回事啊?严格按教程一步步做下来的。 --------------------------------------------------------- Django Version: 3.2.13 Exception Type: TemplateDoesNotExist Exception Value: polls/detail.html Exception Location: D:\mysite\lj_env\lib\site-packages\django\template\loader.py, line 19, in get_template Python Executable: D:\mysite\lj_env\Scripts\python.exe Python Version: 3.10.4 Python Path: ['D:\\mysite\\ljstudy', 'D:\\Program Files\\Python310\\python310.zip', 'D:\\Program Files\\Python310\\DLLs', 'D:\\Program Files\\Python310\\lib', 'D:\\Program Files\\Python310', 'D:\\mysite\\lj_env', 'D:\\mysite\\lj_env\\lib\\site-packages'] Server time: Thu, 28 Apr 2022 23:04:25 +0800 Template-loader postmortem Django tried loading these templates, in this order: Using engine django: django.template.loaders.filesystem.Loader: D:\mysite\ljstudy\polls\Templates\polls\detail.html (Source does not exist) django.template.loaders.app_directories.Loader: D:\mysite\lj_env\lib\site-packages\django\contrib\admin\templates\polls\detail.html (Source does not exist) django.template.loaders.app_directories.Loader: D:\mysite\lj_env\lib\site-packages\django\contrib\auth\templates\polls\detail.html (Source does not exist) django.template.loaders.app_directories.Loader: D:\mysite\ljstudy\polls\templates\polls\detail.html (Source does not exist)



重做了一遍。犯了个低级错误,detail写成了details。搞定!



TemplateDoesNotExist at /polls/1/ polls/detail.html Request Method: GET Request URL: http://127.0.0.1:8000/polls/1/ Django Version: 4.0 Exception Type: TemplateDoesNotExist Exception Value: polls/detail.html Exception Location: D:\Python38\lib\site-packages\django\template\loader.py, line 19, in get_template Python Executable: D:\Python38\python.exe Python Version: 3.8.6 Python Path: ['D:\\pycharm\\pycharm_fess', 'D:\\pycharm\\pycharm_fess', 'D:\\PyCharm 2021.3\\plugins\\python\\helpers\\pycharm_display', 'D:\\Python38\\python38.zip', 'D:\\Python38\\DLLs', 'D:\\Python38\\lib', 'D:\\Python38', 'C:\\Users\\wfr\\AppData\\Roaming\\Python\\Python38\\site-packages', 'D:\\Python38\\lib\\site-packages', 'D:\\PyCharm 2021.3\\plugins\\python\\helpers\\pycharm_matplotlib_backend'] Server time: Wed, 22 Dec 2021 16:21:00 +0800



浏览器进polls/1/出现这个错误了啊................



老师您好,请问pluralize过滤器中,会出现个数为0,加s的情况,有没有办法解决呢?————————————————————————————————————— You see what? See your eyes ------ 1 vote See my food ------ 0 votes None of your business ------ 2 votes Vote again?



一般我们都会对0,也就是没有数据做if/else单独处理。



class IndexView(generic.ListView): template_name = 'polls/index.html' context_object_name = 'latest_question_list' 这里面的'latest_question_list'拼写错误了,应该改成 'lastest_question_list' 不然在改成通用模板之后,index页会出现No polls are available.



英语latest和lastest的区别



detail模板里面不是for choice in question.choice_set.all那,经过for循环不是应该有很多个按钮吗,为什么就一个,而且循环只进行了一次



request.post['choice']这里有问题,得改成request.post['choice.id']



最后一步改成通用模版是报错Django __init__() takes 1 positional argument but 2 were given 解决办法: 修改urls.py时,调用函数as_view(),闭包调用,捕获参数。path('', views.IndexView.as_view(), name='index')



request.POST[’choice’,None]应该是request.POST.get(’choice’,None)吧?



是的



/ polls / 1 / vote /处的ValueError 以10为底的int()的无效文字:'' <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br> 里面的forloop报输入错误 是跟这个有关系么



http://127.0.0.1:8000/polls/1/ 一直不懂这个polls后面的id是怎么来的 还有怎么进入后面的polls的urls的 求解求解 不胜感激



这需要了解Http的请求方法,<a>标签标识一个超链接,请求的方法是GET,会将参数自动附在链接的后面,这就是id的由来。



NoReverseMatch at /polls/2/ Reverse for 'polls.vote' not found. 'polls.vote' is not a valid view function or pattern name. 出现了这个问题,跟着刘老师说的做



url写错了吧?再对照一下urls.py的代码



按照步骤走,修改前 /polls/1/不会展示detail页面,用通用视图步骤就报下面的错误了: TypeErrorat /polls/1/ 'set' object is not reversible Request Method: GET Request URL: http://localhost:8000/polls/1/ Django Version: 2.2.6 Exception Type: TypeError Exception Value: 'set' object is not reversible Exception Location: D:\IDEA\Python\lib\site-packages\django\urls\resolvers.py in _populate, line 447 Python Executable: D:\IDEA\Python\python.exe Python Version: 3.7.4 Python Path: ['D:\\IDEA\\PycharmProjects\\mysite', 'D:\\IDEA\\PycharmProjects\\mysite', 'D:\\IDEA\\pycharm\\PyCharm 2019.1\\helpers\\pycharm_display', 'D:\\IDEA\\Python\\python37.zip', 'D:\\IDEA\\Python\\DLLs', 'D:\\IDEA\\Python\\lib', 'D:\\IDEA\\Python', 'D:\\IDEA\\Python\\lib\\site-packages', 'D:\\IDEA\\pycharm\\PyCharm 2019.1\\helpers\\pycharm_matplotlib_backend'] Server time: Thu, 14 Nov 2019 17:12:30 +0800 Error during template rendering In template D:\IDEA\PycharmProjects\mysite\polls\templates\polls\detail.html, error at line 12



再重新对照一下代码吧。这个实例是一环扣一环,过程中任何一个地方出错都会影响后面。



我的HTML在浏览器打开,只有 h1标题和vote提交标签,没有选择choice的



看看是不是没有创建choice选项,导致for循环的是一个空的列表



找到一个线索了,但还没解决问题,老师帮忙看下~我删掉migrations中的0001_inital.py后,再次运行python manage.py makemigrations发现,只提示创建Question和Choice两张表,但没有提示Add question To choiceFeild,但后来我检查代码还是卡主了,我拷贝代码过来也还没有Add question to choiceFeild。 我连接的是mysql,这应该没影响的



上面是 Add field question to choice



你不光要删除0001_inital.py,还要删除mysql数据库中的migrations表



刘老师您好,我的html模板中,以下这段代码貌似没有执行,不知道 哪里出错了,复制过去的也不行。 {% 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 %}



改为通用视图之后,需要把polls文件夹中的index.html、detail.html、results.html文件删除么?



通用视图是视图层面也就是python代码的复用,依然使用原来的html模板,所以这些文件不删除



打开/polls/1/ 后,没有看到投票结果,不知道在哪一步操作错了。



def vote(request, question_id): ... # 同前面的一样,不需要修改 这里要把question_id 改成pk吧。老师的教程真棒



selected_choice = question.choice_set.get(pk=request.POST['choice']) 这一行报错了 {TypeError}__call__() got an unexpected keyword argument 'pk' 不知道为什么



每一种通用视图都需要知道它要作用在哪个模型上,这通过model属性提供。 老师,在ListView中为什么不用指定model? get_queryset是代替model的作用吗?



照着教程写的,报错了



index页面访问报错啊 django.urls.exceptions.NoReverseMatch: Reverse for 'index' with arguments '(2,)' not found. 1 pattern(s) tried: ['polls\\/$']



from selenium import webdriver import os from threading import Thread from time import sleep def cmd(a,b): os.system('python manage.py runserver') def test(a,b): driver = webdriver.Chrome(r'D:\chromedriver_win32\chromedriver.exe') driver.get('http://127.0.0.1:8000/polls/') thread1=Thread( target=cmd, args=(0,0) ) sleep(0.5) thread2=Thread( target=test, args=(0,0) ) thread1.start() thread2.start() 给大家分享自己写的一个web自动化测试的代码,代码肯定有缺陷,大神不喜勿喷



其中的webdriver软件需要自己安装的,比如我的浏览器是chrome,就下载一个chromedirver,然后用两个线程执行就可以了



views.py return render(request, 'polls/results.html', {'question': question})返回页面多了个s hhhh



哦~自己的是results.html页面哈哈



为何一直显示No polls are avaible



同样一直显示No polls are avaible 但是如果把detail.html中的latest_question_list 改成 question_list (我理解为django自动创建的对象) 首页就可以显示正常显示 怀疑是 context_objcet_name 没有起作用,目前找不到原因。 麻烦找到问题同学所在的回复



我一开始也是显示No polls are avaible,然后检查了一遍代码,发现有个地方我写错了,IndexView中的‘latest_question_list’,我写成了‘latest question list’(没写下划线)才导致出这个错误。



网页报错:Exception Value:no such column: polls_choice.votes 在Python运行 q.Choice_set.all()和q.choice_set.create(choice_text='Not much', votes=0)总是报错,调试半天总是调试不出来,请给我看一下



一不小心创建多了且重复了



如题



按照教程加上默认值None之后request.POST['choice',None],无论选择什么choice都会提示You didn't select a choice.。是只有我这样吗?



各位好!投票后在结果页未累计投票数,请问一下这个是什么问题?我对了一下,和博主的代码一样的。如果有遇到过这问题并解决的可以帮忙回答一下,谢谢了。



就本人所遇到的该问题,之前发现在下行代码中votes写成vote,所以未显示投票数,修改后显示了。你可以试下把博主的代码直接贴上去运行一下。 <li>{{ choice.choice_text }} -- {{ choice.votes }} vote {{ choice.vote| pluralize }}</li>



刘老师,你好,关于类视图实例很是不理解,有什么学习方法或者参考的思路吗?谢谢



对类这里没有啥基础



http://127.0.0.1:8000/polls/1/vote/ 以上界面访问报错。 提示错误点在detail.html中这句: <form action="{% url 'polls:vote' question.id %}" method="post"> 报错内容是:Reverse for 'vote' with arguments '('',)' not found. 1 pattern(s) tried: ['polls/(?P<question_id>[0-9]+)/vote/$'] 比对了好久没有找到原因,请博主指点一下。



需要在views修改对应的方法 class DetailView(generic.DetailView): template_name='polls/detail.html' model=Questions context_object_name='question' 最后一句必须与html中的一致起来才行



generic报错



这个问题解决了吗?我也遇到了,不知道怎么解决。用的是py 3.7,django2



原来打错个符号。。。



那怕它看起来像数字 => 哪怕



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 def vote(request,question_id): # return HttpResponse("You are voting for question %s."% 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 don't select a choice.", }) else: selected_choice.votes += 1 selected_choice.save() return HttpResponseRedirect(reverse('polls:results',args=(question.id,))) 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 ResultView(generic.DetailView): model = Question template_name = 'polls/results.html' 报错: Traceback (most recent call last): File "D:/Python/study/mysite/polls/views.py", line 5, in <module> from .models import Choice, Question ModuleNotFoundError: No module named '__main__.models'; '__main__' is not a package



这个是我自己打开方式不对,不好意思.....



class IndexView(generic.ListView): template_name = 'polls/index.html' context_object_name = 'latest_question_list' def get_queryset(self): """返回最近发布的5个问卷.""" return Question.objects.order_by('-pub_date')[:5] 这里的return,我这边报错,说out of function,怎么办啊,哇呜



get_object_or_404(klass,, pk=question_id) klass may be a Model, Manager, or QuerySet object. All other passed arguments and keyword arguments are used in the get() query. pk是id的意思,当然也可写成id = question_id,或者写成question_text="你想查询的question_text"



get() 为queryset下的get方法



谢谢刘老师



vote{{ choice.votes|pluralize }} 啥意思



我们知道英文中的名词分复数和单数形式,比如apple和apples。 pluralize是Django内置的一个过滤器,当choice.votes这个变量的值大于1时,也就是复数时,就在字符串vote后面加个s。如果值是1,也就是单数形式,就不加s。 Django很细对吧?这么小的细节都考虑到了!



考虑很详细,讲解很详细



我在点击vote的时候出现 _reverse_with_prefix() argument after * must be an iterable, not int 错误



确保是完全按照教程进行的。



我自己的经历就是,你在用revers()函数的时候,传入的参数args=(question.id,)缺少了",",元组中如果只有一个参数时,应该加个,或者你可以直接用args=[question.id]的传参方式



在detail.html中,form表单的action的url是不是写错了,改成"{% url 'polls:results' question.id %} 不过不影响 细心的人应该都会看出来。



不好意我理解错了,看错了



你再仔细看一下。



深入浅出,浅显易懂