编写你的第一个Django应用程序,第3部分

概述

视图(view)是Django应用中的一“类”网页,它通常使用一个特定的函数提供服务,并且具有一个特定的模板。

例如,在博客应用中,可能有以下视图:

  • 博客首页 —— 显示最新发表的博客。
  • 博客“详细”页面 —— 单篇博客的固定链接页面。
  • 基于年份的归档页面 —— 显示某给定年份里所有月份发表过的博客。
  • 基于月份的归档页面 —— 显示在给定月份中发表过博客的所有日期。
  • 基于日份的归档页面 —— 显示在给定日份中发表过博客的所有日期。
  • 评论 —— 对给定的博客发表评论

在我们的投票应用中,将有以下四个视图:

  • Question首页 —— 显示最新发布的几个Question。
  • Question“详细”页面 —— 显示单个Question的具体内容,不显示该议题的当前投票结果,而是提供一个投票的表单。
  • Question“结果”页面 —— 显示特定的Question的投票结果。
  • 投票功能 —— 处理对Question中Choice的投票。

在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模式匹配到视图。

写更多视图

现在让我们给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 模块关联起来:
polls/urls.py

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。下面是一个新的 index() 视图,它显示系统中最新发布的5条questions记录,并用逗号分隔:
polls/views.py

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 。按照惯例, DjangoTemplatesINSTALLED_APPS 所包含的每个应用的目录下查找名为"templates"子目录。

在你刚刚创建的 templates 目录中,创建另外一个目录 polls ,并在其中创建一个文件 index.html 。换句话讲,你的模板应该位于 polls/templates/polls/index.html 。由于 app_directories 模板加载器按照上面描述的方式工作,在Django中你可以简单地用 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视图来使用模板:

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 渲染后的模板。一旦我们在views中这么做了,我们就不需要在引用 loader HttpResponse (如果你在 detail , resultsvote 函数中使用了stub方法,你也许想要保留 HttpResponse ).

render() 函数将请求对象作为它的第一个参数,模板的名字作为它的第二个参数,一个字典作为它可选的第三个参数。它返回一个 HttpResponse 对象,含有用给定的context 渲染后的模板。

抛出404异常

现在,让我们处理Question 详细页面的视图 —— 显示Question内容的页面:

下面是该视图:
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的Question,这个视图引发一个Http404异常。

我们将在以后讨论你可以在polls/detail.html模板文件里放些什么代码,但如果你想快点运行上面的例子,仅仅包含:这里有一个新概念:如果没有找到所请求ID的Question,这个视图引发一个 Http404 异常。

我们将在以后讨论你可以在 polls/detail.html 模板文件里放些什么代码,但如果你想快点运行上面的例子,仅仅包含:
polls/templates/polls/detail.html
{{ question }}
的文件就可以让你开始。

一个快捷方式:get_object_or_404()

一种常见的习惯是使用get()并在对象不存在时引发Http404。 Django为此提供一个快捷方式。 下面是重写后的detail()视图:一种常见的习惯是使用 get() 并在对象不存在时引发 Http404

Django为此提供一个快捷方式。

下面是重写后的 detail() 视图:
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})

get_object_or_404() 函数将一个Django模型作为它的第一个参数,任意数量的关键字参数作为它的第二个参数,它会将这些关键字参数传递给模型管理器中的 get() 函数。如果对象不存在,它就引发一个 Http404 异常。

还有一个get_list_or_404()函数,它的工作方式类似get_object_or_404() —— 差别在于它使用filter()而不是get()。 如果列表为空则引发Http404。还有一个 get_list_or_404() 函数,它的工作方式类似 get_object_or_404() —— 差别在于它使用 filter() 而不是 get() 。如果列表为空则引发 Http404

使用模板系统

回到我们投票应用的 detail() 视图。根据context 变量 question ,下面是 polls/detail.html 模板可能的样子:
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 对象上做字典查询。如果失败,Django会接着尝试属性查询 —— 在这个例子中,属性查询会成功。如果属性查询也失败,Django将尝试列表索引查询。
方法调用发生在 {%for%} 循环中: Choice 被解释为Python的代码 question.choice_set.all ,它返回一个由 question.choice_set.all() 对象组成的可迭代对象,并将其用于 {%for%} 标签。

移除模板中硬编码的URL

请记住,当我们在polls/index.html模板中编写一个指向Question的链接时,链接中一部分是硬编码的:

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

这种硬编码、紧耦合的方法有一个问题,就是如果我们想在拥有许多模板文件的项目中修改URLs,那将会变得很有挑战性。然而,因为你在 polls.urls 模块的 url() 函数中定义了name 参数,你可以通过使用 {%url%} 模板标签来移除对你的URL配置中定义的特定的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'),

命名空间URL名称

教程中的这个项目只有一个应用 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>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容