本教程上接Tutorial 2。 我们将继续开发网页投票这个应用,并将注意力集中在如何创建一个对外的界面 —— “视图”上。
视图(view)是Django应用中的一“类”网页,它通常使用一个特定的函数提供服务,并且具有一个特定的模板。 例如,在博客应用中,可能有以下视图:
在我们的投票应用中,将有以下四个视图:
在Django中,网页的页面和其他内容都是由视图来传递的(视图对WEB请求进行回应)。 每个视图都是由一个简单的Python函数(或者是基于类的视图的方法)表示的。 Django通过检查请求的URL(准确地说,是URL里域名之后的那部分)来选择使用哪个视图。
平日你上网时,可能会遇到像 “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B”这样 "优美" 的URL。 你将会愉快地了解到,Django允许我们使用更加优雅的URL模式。
URL模式就是一个URL的通用形式 —— 例如: /newsarchive/<year>/<month>/
。
Django使用叫做‘URLconfs’的配置来为URL匹配视图。 一个URLconf负责使用正则表达式将URL模式匹配到视图。
本教程提供使用URLconfs的基本指南,你可以参考django.urls
了解更多信息。
现在让我们给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)
通过下面的url()
调用将这些新的视图和polls.urls
模块关联起来:
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'),
]
看看你的浏览器,输入“/34/” 它将运行detail()
方法并显示你在URL中提供的ID。 再试一下“/polls/34/results/”和“/polls/34/vote/” —— 它们将显示出对应的结果界面和投票界面。
当有人从你的网站请求一个页面 — 例如“/polls/34/”时,Django将加载mysite.urls
Python模块,因为它被指向ROOT_URLCONF
设置。 它寻找名为urlpatterns
的变量并按顺序匹配其中的正则表达式。 在'^polls/'
找到匹配后,它将取消匹配的文本("polls/"
),并发送剩余的文本 - "34/"
- 到'polls.urls'URLconf进行进一步处理。 它匹配r'^(?P<question_id>[0-9]+)/$'
,导致调用detail()
视图,如下所示:
detail(request=<HttpRequest object>, question_id='34')
question_id='34'
部分来自(?P<question_id>[0-9]+)
。 使用模式周围的括号“捕获”该模式匹配的文本,并将其作为参数发送给视图函数; ?P<question_id>
定义将用于识别匹配模式的名称;并且[0-9]+
是用于匹配数字序列(即,数字)的正则表达式。
因为URL模式是正则表达式,你如何使用它们没有什么限制。 不需要添加像.html
这样繁琐的URL —— 除非你执意这么做,在这种情况下你可以这样做:
url(r'^polls/latest\.html$', views.index),
但是,真的不要这样做。 这很蠢。
每个视图函数只负责处理两件事中的一件:返回一个包含所请求页面内容的 HttpResponse
对象,或抛出一个诸如Http404
异常。 该如何去做这两件事,就看你自己的想法了。
你的视图可以从数据库中读取记录,或者不读取数据库。 你还可以动态地生成一个PDF文件、输出XML文件、创建一个ZIP文件或者使用你想用的Python 库生成任何想要的形式。
Django只要求返回的是一个HttpResponse
。 或者抛出一个异常。
因为它很方便,我们使用Django自己的数据库API,这是我们在Tutorial 2中介绍的。 下面是一个新的index()
视图,它显示系统中最新发布的5条questions记录,并用逗号分隔:
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)
# Leave the rest of the views (detail, results, vote) unchanged
这里有一个问题:页面的设计被硬编码在视图中。 如果你想更改页面的外观,就得编辑这段Python代码。 因此,让我们使用Django的模板系统,通过创建一个视图能够调用的模板,将页面的设计从Python中分离出来。
首先,在你的polls
目录下创建一个叫做 templates
的目录。
Django将在这里查找模板。
你项目的TEMPLATES
设置描述了Django将如何加载并渲染模板。 默认的设置文件settings.py配置了一个DjangoTemplates
后端,其中将APP_DIRS
选项设置为True
。 按照惯例,DjangoTemplates
在 INSTALLED_APPS
所包含的每个应用的目录下查找名为"templates"子目录。
在你刚刚创建的templates
目录中,创建另外一个目录polls
,并在其中创建一个文件index.html
。 换句话讲,你的模板应该位于 polls/templates/polls/index.html
。 由于app_directories
模板加载器按照上面描述的方式工作,在Django中你可以简单地用polls/index.html
引用这个模板。
模板命名空间
现在,我们可以直接将我们的模板放在polls/templates
中(而不用创建另外一个polls
子目录),但实际上这是个坏主意。 Django将选择它找到的名字匹配的第一个模板文件,如果你在不同 的应用有相同名字的模板文件,Django将不能区分它们。 我们需要将Django指向正确的模板,最简单的方式是使用命名空间。 具体实现方式是,将这些模板文件放在以应用的名字来命名的另一个目录下。
将以下的代码放入模板文件:
{% 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
视图来使用模板:
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
模板,并传给它一个context。 context是一个由(变量名,python对象)组成的字典(译者注:变量名对应html模板中的名称)。
通过将浏览器指向“/polls/”来载入页面,你应该看到一个列表,包含教程2中的“What's up”问题。 其链接指向Question的detail页面。
render()
¶常见的习惯是载入一个模板、填充一个context 然后返回一个含有模板渲染结果的HttpResponse
对象。 Django为此提供一个快捷方式。 下面是重写后的index()
视图:
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)
一旦我们在views中这么做了,我们就不需要在引用loader
和 HttpResponse
(如果你在detail
, results
或 vote
函数中使用了stub方法,你也许想要保留HttpResponse
).
render()
函数将请求对象作为它的第一个参数,模板的名字作为它的第二个参数,一个字典作为它可选的第三个参数。 它返回一个HttpResponse
对象,含有用给定的context 渲染后的模板。
现在,让我们处理Question 详细页面的视图 —— 显示Question内容的页面: 下面是该视图:
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的Question,这个视图引发一个Http404
异常。
我们将在以后讨论你可以在polls/detail.html
模板文件里放些什么代码,但如果你想快点运行上面的例子,文件仅仅包含:
{{ question }}
现在你可以开始了。
get_object_or_404()
¶一种常见的习惯是使用get()
并在对象不存在时引发Http404
。 Django为此提供一个快捷方式。 下面是重写后的detail()
视图:
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})
get_object_or_404()
函数将一个Django模型作为它的第一个参数,任意数量的关键字参数作为它的第二个参数,它会将这些关键字参数传递给模型管理器中的get()
函数。 如果对象不存在,它就引发一个 Http404
异常。
理念
为什么我们要使用一个辅助函数get_object_or_404()
而不是在更高层自动捕获ObjectDoesNotExist
异常,或者让模型的API 引发 Http404
而不是ObjectDoesNotExist
?
因为那样做将会使模型层与视图层耦合在一起。 Django最重要的一个设计目标就是保持松耦合。 一些可控的耦合将会在django.shortcuts
模块中介绍。
还有一个get_list_or_404()
函数,它的工作方式类似get_object_or_404()
—— 差别在于它使用filter()
而不是get()
。 如果列表为空则引发Http404
。
回到我们投票应用的detail()
视图。 根据context 变量question
,下面是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
对象上做字典查询。 如果失败,Django会接着尝试属性查询 —— 在这个例子中,属性查询会成功。 如果属性查询也失败,Django将尝试列表索引查询。
方法调用发生在{% for %}
循环中:Choice
被解释为Python的代码question.choice_set.all
,它返回一个由question.choice_set.all()
对象组成的可迭代对象,并将其用于{% for %}
标签。
查看template guide(模板指南)来了解更多关于模板的信息。
请记住,当我们在polls/index.html
模板中编写一个指向Question的链接时,链接中一部分是硬编码的:
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
这种硬编码、紧耦合的方法有一个问题,就是如果我们想在拥有许多模板文件的项目中修改URLs,那将会变得很有挑战性。 然而,因为你在polls.urls
模块的url()
函数中定义了name 参数,你可以通过使用{% url %}
模板标签来移除对你的URL配置中定义的特定的URL的依赖:
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
它的工作原理是在polls.urls
模块里查找指定的URL的定义。 你可以看到名为‘detail’的URL的准确定义:
...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...
如果你想把polls应用中detail视图的URL改成其它样子比如polls/specifics/12/
,就可以不必在该模板(或者多个模板)中修改它,只需要修改polls/urls.py
:
...
# added the word 'specifics'
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...
教程中的这个项目只有一个应用polls
。 在真实的Django项目中,可能会有五个、十个、二十个或者更多的应用。 Django如何区分它们URL的名字呢? 例如,polls
应用具有一个detail
视图,相同项目中的博客应用可能也有这样一个视图。 当使用模板标签{% url %}
时,人们该如何做才能使得Django知道为一个URL创建哪个应用的视图?
答案是添加命名空间到你的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/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>
当你对你写的视图感到满意后,请阅读本教程的第4部分来了解简单的表单处理和通用视图。
2017年9月6日