Part 3:视图和模板

阅读: 36038


一、概述

一个视图就是一个页面,通常提供特定的功能,使用特定的模板。例如:在一个博客应用中,你可能会看到下列视图:

  • 博客主页:显示最新发布的一些内容
  • 每篇博客的详细页面:博客的永久链接
  • 基于年的博客页面:显示指定年内的所有博客文章
  • 基于月的博客页面:显示指定月内的所有博客文章
  • 基于天的博客页面:显示指定日内的所有博客文章
  • 发布评论:处理针对某篇博客发布的评论

在我们的投票应用中,我们将建立下面的视图:

  • 问卷“index”页:显示最新的一些问卷
  • 问卷“detail”页面:显示一个问卷的详细文本内容,没有调查结果但是有一个投票或调查表单。
  • 问卷“results”页面:显示某个问卷的投票或调查结果。
  • 投票动作页面:处理针对某个问卷的某个选项的投票动作。

在Django中,网页和其它的一些内容都是通过视图来处理的。视图其实就是一个简单的Python函数(在基于类的视图中称为方法)。Django通过对比请求的URL地址来选择对应的视图。

二、编写视图

下面,打开polls/views.py文件,输入下列代码:

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

然后,在polls/urls.py文件中加入下面的url模式,将其映射到我们上面新增的视图。

from django.conf.urls import url
from . import views

urlpatterns = [
    # ex: /polls/
    url(r'^$', views.index, name='index'),
    # ex: /polls/5/
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    # ex: /polls/5/results/
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    # ex: /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

现在去浏览器中访问/polls/34/(注意:这里省略了域名。另外,使用了二级路由后,url中都要添加polls部分,参考前面的章节),它将调用detail()函数,然后在页面中显示你在url里提供的ID。访问/polls/34/results//polls/34/vote/,将分别显示预定义的伪结果和投票页面。(PS:这里就不贴图了,请大家务必自己动手测试,多实践。)

上面访问的路由过程如下:当有人访问/polls/34/地址时,Django将首先加载mysite.urls模块,因为它是settings文件里设置的根URL配置文件。在该文件里,Django发现了urlpatterns变量,于是在其内按顺序进行匹配。当它匹配上了^polls/,就裁去url中匹配的文本polls/,然后将剩下的文本“34/”,传递给polls.urls进行下一步的处理。在polls.urls中,又匹配到了r’^(?P<question_id>[0-9]+)/$’,最终结果就是调用该模式对应的detail()视图,也就是下面的函数:

detail(request=<HttpRequest object>, question_id='34')

函数中的question_id=’34’参数,是由(?P<question_id>[0-9]+)而来。在正则表达式中通过一个双圆括号,Django会捕获它匹配到的值并传递给对应的视图,作为视图的位置参数之一,而?P<question_id>则表示我要给这个捕获的值指定一个特殊的变量名,在视图中可以通过question_id这个变量名随意的引用它,形成一个关键字参数,不用考虑参数的位置。至于[0-9]+则是一个很简单的原生正则表达式,用于匹配一系列连续的数字,它匹配到的值也就是具体要传递的参数值。

所有的URL模式都是正则表达式,Django不限制你在url模式中的书写方式。但是,你真的没必要书写一个如下的较为愚蠢的包含.html的模式,它显然是没必要,不够简练的:

url(r'^polls/latest\.html$', views.index),

你完全可以用下面的模式代替上面的:

url(r'^polls/latest$', views.index),

三、编写能实际干点活的视图

每个视图至少做两件事之一:返回一个包含请求页面的HttpResponse对象或者弹出一个类似Http404的异常。其它的则随你便,你爱干嘛干嘛。

下面是一个新的index()视图,用于替代先前无用的index,它会根据发布日期显示最近的5个投票问卷。

from django.http import HttpResponse

from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# 下面是那些没改动过的视图(detail, results, vote)

这里有个非常重要的问题:在当前视图中的HTML页面是硬编码的。如果你想改变页面的显示内容,就必须修改这里的Python代码。为了解决这个问题,需要使用Django提供的模板系统,解耦视图和模板之间的硬连接。

首先,在polls目录下创建一个新的templates目录,Django会在它里面查找模板文件。在templates目录中,再创建一个新的子目录名叫polls,进入该子目录,创建一个新的html文件index.html。换句话说,你的模板文件应该是polls/templates/polls/index.html。可以在DJango中直接使用polls/index.html引用该文件。

注意:在Pycharm中,templates文件夹通常已经帮你创建好了!

模板命名空间:

你也许会想,为什么不把模板文件直接放在polls/templates目录下,而是费劲的再建个子目录polls呢?设想这么个情况,有另外一个app,它也有一个名叫index.html的文件,当Django在搜索模板时,有可能就找到它,然后退出搜索,这就命中了错误的目标,不是我们想要的结果。解决这个问题的最好办法就是在templates目录下再建立一个与app同名的子目录,将自己所属的模板都放到里面,从而达到独立命名空间的作用,不会再出现引用错误。

现在,将下列代码写入文件polls/templates/polls/index.html:

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
    <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

同时,修改视图文件polls/views.py,让新的index.html文件生效:

from django.http import HttpResponse
from django.template import loader
from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
    'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

上面的代码会加载polls/index.html文件,并传递给它一个参数。这个参数是一个字典,包含了模板变量名和Python对象之间的映射关系。

在浏览器中通过访问/polls/,你可以看到一个列表,包含“What’s up”的问卷,以及连接到其对应详细内容页面的链接点。

如果你显示的是No polls are available.说明你前面没有添加Questions对象。没关系,我们手动添加一下就可以。

进入admin界面,选择Questions,点击右上角的Add question,如下操作。

image.png-31.9kB

添加完后,刷新/polls/页面。

快捷方式:render()

在实际运用中,加载模板、传递参数,返回HttpResponse对象是一整套再常用不过的操作了,为了节省力气,Django提供了一个快捷方式:render函数,一步到位!看如下代码:

polls/views.py

from django.shortcuts import render
from .models import Question
def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

render()函数的第一个位置参数是请求对象(就是view函数的第一个参数),第二个位置参数是模板。还可以有一个可选的第三参数,一个字典,包含需要传递给模板的数据。最后render函数返回一个经过字典数据渲染过的模板封装而成的HttpResponse对象。

四、返回404错误

现在让我们来编写返回具体问卷文本内容的视图polls/views.py

from django.http import Http404
from django.shortcuts import render
from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

这里有个新知识点,如果请求的问卷ID不存在,那么会弹出一个Http404错误。

新建polls/detail.html文件,暂时写入下面的代码:

{{ question }}

快捷方式:get_object_or_404()

就像render函数一样,Django同样为你提供了一个偷懒的方式,替代上面的多行代码,那就是get_object_or_404()方法,参考下面的代码:

polls/views.py

from django.shortcuts import get_object_or_404, render
from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

别说我没提醒你,和render一样,也需要从Django内置的快捷方式模块中导出get_object_or_404()

get_object_or_404()方法将一个Django模型作为第一个位置参数,后面可以跟上任意个数的关键字参数,如果对象不存在则弹出Http404错误。

同样,还有一个get_list_or_404()方法,和上面的get_object_or_404()类似,只不过是用来替代filter()函数,当查询列表为空时弹出404错误。(filter是模型API中用来过滤查询结果的函数,它的结果是一个列表集。而get则是查询一个结果的方法,和filter是一个和多个的区别!)

五、 使用模板系统

detail()视图会将上下文变量question传递给对应的polls/templates/polls/detail.html模板,修改该模板的内容,如下所示:

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

在模板系统中圆点.是万能的魔法师,你可以用它访问对象的属性。在例子{{ question.question_text }}中,DJango首先会在question对象中尝试查找一个字典,如果失败,则尝试查找属性,如果再失败,则尝试作为列表的索引进行查询。

{% for %}循环中的方法调用——question.choice_set.all其实就是Python的代码question.choice_set.all(),它将返回一组可迭代的Choice对象,并用在{% for %}标签中。

这里我们对Django模板语言有个简单的印象就好,更深入的介绍放在后面。

六、删除模板中硬编码的URLs

polls/index.html文件中,还有一部分硬编码存在,也就是href里的“/polls/”部分:

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

它对于代码修改非常不利。设想如果你在urls.py文件里修改了正则表达式,那么你所有的模板中对这个url的引用都需要修改,这是无法接受的!

我们前面给urls定义了一个name别名,可以用它来解决这个问题。具体代码如下:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

Django会在polls.urls文件中查找name='detail'的url,具体的就是下面这行:

url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),

举个栗子,如果你想将polls的detail视图的URL更换为polls/specifics/12/,那么你不需要在模板中重新修改url地址了,仅仅只需要在polls/urls.py文件中,将对应的正则表达式改成下面这样的就行了,所有模板中对它的引用都会自动修改成新的链接:

# 添加新的单词'specifics'
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),

七、URL names的命名空间

本教程例子中,只有一个app也就是polls,但是在现实中很显然会有5个、10个、更多的app同时存在一个项目中。Django是如何区分这些app之间的URL name呢?

答案是使用URLconf的命名空间。在polls/urls.py文件的开头部分,添加一个app_name的变量来指定该应用的命名空间:

from django.conf.urls import url
from . import views

app_name = 'polls'  # 关键是这行
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

现在,让我们将代码修改得更严谨一点,将polls/templates/polls/index.html中的

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

修改为:

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

注意引用方法是冒号而不是圆点也不是斜杠!!!!!!!!!!!!



评论总数: 37



user_image
受益匪浅

在看这之前是看另外一个教程。那个教程呢,只能说一般,教会用而不知其意。 博主的这篇真的是讲到了初学者的痛点,讲理论,也实际操作,重要的是,把文件路径说明白,因为有些教程,特么滴往往就是不说这点, 让人好生烦恼。我之前的疑问都在这里得到解答。 博主真的很用心在写,感谢博主

user_image
已走通 mark 20181107

已走通 mark 20181107

user_image
真诚

确实写得好,希望人气越来越旺,做的大好事啊。

user_image
《三、编写能实际干点活的视图》范例中views向html传递对象为空

刘老师,本章节第三小节的范例中,通过render(request, 'polls/index.html', context),context中对象实际上是一个QuerySet, 但是在html中,即使我的Question表中是有数据的,html中显示的永远是No polls are available. 我反复尝试,如果在view里for..in.. 传递是dict,html显示就没有问题。请问这是啥原因,研究两天了,我发现其他范例,传递的是对象,也是这种情况,请问是啥原因? 我的django版本是2.0.5, 谢谢

user_image
《三、编写能实际干点活的视图》范例中views向html传递对象为空

context应该是一个dict才对吧{key,value},value为QuerySet

user_image
没有问题

无论是1.11版本还是2.05,这里都没有问题。请检查自己的代码,继续学习下去。

user_image
template 目录写错了?

教程中 polls/templates/polls/detail.html 应该是 mysite/templates/polls/detail.html吧! mysite 是项目, polls是app

user_image
继续深入学习下去

这是Django的设计原则之一

user_image
django2.0.5中polls.urls.py的写法

urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='result'), path('<int:question_id>/vote/', views.vote, name='vote'), ]

user_image
前面少了一个'/'

应该是urlpatterns = [ path('', views.index, name='index'), path('</int:question_id>/', views.detail, name='detail'), path('</int:question_id>/results/', views.results, name='result'), path('</int:question_id>/vote/', views.vote, name='vote'), ],这样可以用polls/1访问

user_image
你的“/” 写错位置了把

跟你一样的是错的的

user_image
NoReverseMatch at /polls/

刘老师好,我照着你这篇博客写了一遍代码,前面都没问题,在最后“删除模板中硬编码的URLs”这一节的时候报错。 <a href="/polls/{{ question.id }}/">{{ question.question_text }}</a>这样写没问题。 改成 <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> 或者 <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>就报错了。 错误信息:Reverse for 'detail' with arguments '('',)' and keyword arguments '{}' not found. 1 pattern(s) tried: ['polls/(?P<question_id>[0-9]+)/$'] 我的polls/urls.py配置也是跟你的博客里一样的: app_name = 'polls' urlpatterns = [ url(r'^$', views.index, name='index'), url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), url(r'^(?P<question_id>[0-9]+)/result/$', views.results, name='results'), url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ]

user_image
是我写错了参数了。

刚检查了一下,是我写错了参数了。

user_image
TemplateDoesNotExist at /polls/

刘老师您好,最近一直在看你的这个博客,在设置模板时报了如题的错,下面是我的配置: settings.py TEMPLATE_DIRS = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], #'DIRS': ['polls/templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] polls/views.py def index(request): latest_question_list= qs.objects.order_by('-pub_date')[:5] #template=loader.get_template('/polls/index.html') context={ "latest_question_list",latest_question_list, } #output=','.join([q.question_text for q in latest_question_list]) return render(request,'polls/index.html',context) index.html 所在目录为polls/templates/polls/index.html 另外 我是用pycharm 工具,在工程目录下也有个templates目录 D:\pythonworkspace\mysite\templates 报上面这个错,是setings 里配置错了吗

user_image
花了半个小时,排查你的问题。

设置里的配置项目的名字不是TEMPLATE_DIRS,而是TEMPLATES。我估计是这个原因。你是从老的Django版本里拷贝过来的配置吧?

user_image
我最开始配置的时候就是用的这个TEMPLATES

后来在网上看到说是要修改为TEMPLATES_DIRS 所以改成这个了

user_image
非常感谢刘老师!

非常感谢刘老师!

user_image
django版本

Django version 1.11.12

user_image
揪到一处错别词哈

刘老师, 五、 使用模板系统的倒数第二句: poll.choice_set.all, 应为question.choice_set.all.

user_image
感谢指出

已修改

user_image
刘老师,本人在“模板命名空间”那里稍微有点困惑。不知道能否请教一下??

上面原文这样说的: “模板命名空间: 你也许会想,为什么不把模板文件直接放在polls/templates目录下,而是费劲的再建个子目录polls呢?设想这么个情况,有另外一个app,它也有一个名叫index.html的文件,当Django在搜索模板时,有可能就找到它,然后退出搜索,这就命中了错误的目标,不是我们想要的结果。解决这个问题的最好办法就是在templates目录下再建立一个与app同名的子目录,将自己所属的模板都放到里面,从而达到独立命名空间的作用,不会再出现引用错误。” 我感觉这里有点不合理。 假如有两个 app 分别叫 app_a 和 app_b,为什么 Django 搜索 app_a 的模板文件时,会跑到 app_b 的模板目录里面搜索呢? 另外,你上面又说解决这个问题的方法是在 polls 的 templates 再建立一个 polls 的子目录(为了区分其他 app),难道说其他 app 里面的模板文件也要放在 polls 的 templates 目录吗? 希望老师能指点迷津,不胜感激!!!

user_image
这种做法是Django的内在机制和设计理念之一

要这么理解: 1. 你可能有很多个app,其中甚至有不少app是别人写的,你拿来用而已 2. app的排序不一定是你希望的那个,Django只会按照既定的规则顺序查找每个app 3. Django查找模版的时候,会去每个app的templates目录下查找,这是核心机制!是每个!而不是只查找自己的html文件目录! 4.如果有多个app同时有index.html模板,那么Django找到的第一个index会被调用,而这往往不是你想要的。比如app_a排在app_b前面,那么app_a没问题了,但app_b会使用app_a中的index.html文件。 5. 为了解决这个问题,在每个app的templates目录下再创建一级目录,就相当于增加了模版命名空间限制。 多看几遍吧,能理解最好,不行就先往后放放

user_image
感谢!!!

非常感谢老师的耐心教导!!!茅塞顿开!!!谢谢!!!

user_image
老师,part3第二章,访问/polls/34/报错404

已经按照教程检查了代码,没有错误。找了找网上的相关问答,似乎是二级路由没有生效?请问是什么原因呢?这里没办法贴图片,拜托老师解答下!

user_image
问题补充:我的环境配置

我是在Windows7下,用的pycharm编译器,Django版本是2.0.1,Python版本是3.6.2。麻烦老师看一下是否是这个版本下内容有差异呢?谢谢

user_image
贴一下报错内容

这是后台页面显示的报错内容,请老师过目: Page not found (404) Request Method: GET Request URL: http://127.0.0.1:8000/admin/polls/43/ Using the URLconf defined in perfectCRM.urls, Django tried these URL patterns, in this order: admin/ [name='index'] admin/ login/ [name='login'] admin/ logout/ [name='logout'] admin/ password_change/ [name='password_change'] admin/ password_change/done/ [name='password_change_done'] admin/ jsi18n/ [name='jsi18n'] admin/ r/<int:content_type_id>/<path:object_id>/ [name='view_on_site'] admin/ auth/group/ admin/ auth/user/ admin/ polls/question/ admin/ ^(?P<app_label>auth|polls)/$ [name='app_list'] ^polls/ The current path, admin/polls/43/, didn't match any of these. You're seeing this error because you have DEBUG = True in your Django settings file. Change that to False, and Django will display a standard 404 page. 在polls/后的正则都匹配不到,不知何故

user_image
本教程使用的是Django1.11版本

Django2.0在url的配置上和1.11差别较大。 建议你,pip uninstall django,然后pip install django==1.11.7

user_image
谢谢老师解答!

好的,谢谢!我去修改下

user_image
和楼上相同的问题

一样的问题,一样的报错,并且我看您的建议,也改成了1.11.7版本的Django了,但还是不行

user_image
你搞定了吗?

我也是这个错误,但是我还没有搞定,想问问你怎么弄的?

user_image
老师,part3第二章,访问/polls/34/不行啊,只能访问/34/

是需要有什么特殊的设置吗,比如r'^(?P<question_id>[0-9]+)/$'前面加个polls

user_image
要按照教程的步骤来

教程里分了二级路由,你需要分别编写根路由urls.py和polls应用中的urls.py。 从头开始,再检查一下代码吧。

user_image
刘老师,你那个加了二级路由不能再^开投了吧

/polls/urls.py url(r'(?P<question_id>[0-9]+)/$', views.detail, name='detail'), 这个二级路由就不能以^开头了 ,因为全局的路由里面已经指定了'^polls',所以这里一加'^',就访问404了

user_image
教程很棒

适合已经对django有一定了解的用户当作补充教材,完全的新手看这个系列的教程会萌币

user_image
确实, 不过也不完全对

也不完全这样, 我就是完全的新手 第一个就是看这里的, 后来边写东西,边查看其它资料, 后来返回来看, 感觉又发现新大陆。

user_image
本页最后严谨的URL没整明白

'polls' is not a registered namespace 改成最后的polls:detail后,进入网页提示这个

user_image
在urls.py文件中加入下面这行!

app_name = 'polls' # 关键是这行