定义视图

A view callable 在一个 Pyramid 应用程序通常是一个简单的python函数,它接受一个名为 request . 假定视图可调用返回 response 对象。

请求对象有一个字典作为名为 matchdict . 一 matchdict 映射匹配URL中的占位符 pattern 到中路径的子字符串 request 网址。例如,如果 pyramid.config.Configurator.add_route() 有模式 /{{one}}/{{two}} 和用户访问 http://example.com/foo/bar ,我们的模式将与 /foo/bar 以及 matchdict 看起来像 {{'one':'foo', 'two':'bar'}} .

添加 docutils 依赖项

请记住,在上一章中,我们添加了 bcrypt 包。同样,我们的应用程序中的视图代码将依赖于一个不依赖于原始“教程”应用程序的包。

我们需要在 docutils 包装给我们 tutorial 包装的 setup.py 通过将此依赖项分配给 requires 名单。

正常开放 tutorial/setup.py 并编辑如下:

11requires = [
12    'alembic',
13    'bcrypt',
14    'docutils',
15    'plaster_pastedeploy',
16    'pyramid',
17    'pyramid_debugtoolbar',
18    'pyramid_jinja2',
19    'pyramid_retry',
20    'pyramid_tm',
21    'SQLAlchemy',
22    'transaction',
23    'waitress',
24    'zope.sqlalchemy',
25]

只需添加突出显示的行。

同样,正如我们在上一章中所做的,依赖项现在需要安装,因此重新运行 $VENV/bin/pip install -e . 命令。

Static Assets

我们的模板命名Static Assets,包括CSS和图像。我们不需要在包中创建这些文件 static 目录,因为它们是在我们创建项目时提供的。

例如,CSS文件将通过 http://localhost:6543/static/theme.css 凭借对 add_static_view 我们在 tutorial/routes.py 文件。任何数量和类型的静态资产都可以放在这个目录(或子目录)中,并且只是通过URL或使用便利方法引用的。 static_url ,例如, request.static_url('<package>:static/foo.css') 在模板中。

将路由添加到 routes.py

这就是 URL Dispatch 教程,让我们从向我们的应用程序添加一些URL模式开始。稍后我们将附加视图来处理URL。

这个 tutorial/routes.py 文件包含 pyramid.config.Configurator.add_route() 用于向应用程序添加路由的调用。首先,我们将去掉模板使用名称创建的现有路由。 'home' . 这只是一个例子,与我们的应用程序无关。

然后我们需要在 add_route . 请注意 排序 这些声明中的一个非常重要。路由声明按注册顺序匹配。

  1. 添加映射模式的声明 / (表示根URL)到名为的路由 view_wiki . 在下一步中,我们将把它映射到 view_wiki 查看可调用的 @view_config 附加到 view_wiki 视图功能,依次由 route_name='view_wiki' .

  2. 添加映射模式的声明 /{{pagename}} 到指定的路线 view_page . 这是页面的常规视图。同样,在下一步中,我们将把它映射到 view_page 查看可调用的 @view_config 附加到 view_page 视图功能,依次由 route_name='view_page' .

  3. 添加映射模式的声明 /add_page/{{pagename}} 到指定的路线 add_page . 这是新页面的添加视图。我们会把它映射到我们的 add_page 查看可调用的 @view_config 附加到 add_page 视图功能,依次由 route_name='add_page' .

  4. 添加映射模式的声明 /{{pagename}}/edit_page 到指定的路线 edit_page . 这是页面的编辑视图。我们会把它映射到我们的 edit_page 查看可调用的 @view_config 附加到 edit_page 视图功能,依次由 route_name='edit_page' .

由于我们的编辑, tutorial/routes.py 文件应如下所示:

1def includeme(config):
2    config.add_static_view('static', 'static', cache_max_age=3600)
3    config.add_route('view_wiki', '/')
4    config.add_route('view_page', '/{pagename}')
5    config.add_route('add_page', '/add_page/{pagename}')
6    config.add_route('edit_page', '/{pagename}/edit_page')

突出显示的行是需要添加或编辑的行。

警告

路线的顺序很重要!如果你放置 /{{pagename}}/edit_page before /add_page/{{pagename}}, then we would never be able to add pages. This is because the first route would always match a request to /add_page/edit_page whereas we want /add_page/.. 有优先权。在这个特定的应用程序中,这并不是一个大问题,因为wiki页面总是像骆驼一样,但是在你自己的应用程序中注意到这种行为是很重要的。

CSRF保护

当处理改变数据库中数据的HTML表单时,我们需要验证表单提交是否合法,而不是来自嵌入在第三方网站中的URL。这是通过在每个表单中添加第三方无法轻易猜到的唯一令牌来完成的。有关CSRF的更多信息,请访问 防止跨站点请求伪造攻击 . 在本教程中,我们将在cookie中存储活动CSRF令牌。

让我们添加一个新的 tutorial/security.py 文件:

1from pyramid.csrf import CookieCSRFStoragePolicy
2
3
4def includeme(config):
5    config.set_csrf_storage_policy(CookieCSRFStoragePolicy())
6    config.set_default_csrf_options(require_csrf=True)

因为我们添加了一个新的 tutorial/security.py 模块,我们需要包括它。打开文件 tutorial/__init__.py 并编辑以下行:

 1from pyramid.config import Configurator
 2
 3
 4def main(global_config, **settings):
 5    """ This function returns a Pyramid WSGI application.
 6    """
 7    with Configurator(settings=settings) as config:
 8        config.include('pyramid_jinja2')
 9        config.include('.security')
10        config.include('.routes')
11        config.include('.models')
12        config.scan()
13    return config.make_wsgi_app()

对于改变数据的表单,我们将确保将CSRF令牌添加到表单中,使用 pyramid.csrf.get_csrf_token() .

在中添加视图函数 views/default.py

是时候进行重大变革了。正常开放 tutorial/views/default.py 并替换为以下内容:

 1from docutils.core import publish_parts
 2from html import escape
 3from pyramid.httpexceptions import (
 4    HTTPNotFound,
 5    HTTPSeeOther,
 6)
 7from pyramid.view import view_config
 8import re
 9
10from .. import models
11
12
13# regular expression used to find WikiWords
14wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
15
16@view_config(route_name='view_wiki')
17def view_wiki(request):
18    next_url = request.route_url('view_page', pagename='FrontPage')
19    return HTTPSeeOther(location=next_url)
20
21@view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2')
22def view_page(request):
23    pagename = request.matchdict['pagename']
24    page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
25    if page is None:
26        raise HTTPNotFound('No such page')
27
28    def add_link(match):
29        word = match.group(1)
30        exists = request.dbsession.query(models.Page).filter_by(name=word).all()
31        if exists:
32            view_url = request.route_url('view_page', pagename=word)
33            return '<a href="%s">%s</a>' % (view_url, escape(word))
34        else:
35            add_url = request.route_url('add_page', pagename=word)
36            return '<a href="%s">%s</a>' % (add_url, escape(word))
37
38    content = publish_parts(page.data, writer_name='html')['html_body']
39    content = wikiwords.sub(add_link, content)
40    edit_url = request.route_url('edit_page', pagename=page.name)
41    return dict(page=page, content=content, edit_url=edit_url)
42
43@view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2')
44def edit_page(request):
45    pagename = request.matchdict['pagename']
46    page = request.dbsession.query(models.Page).filter_by(name=pagename).one()
47    if request.method == 'POST':
48        page.data = request.params['body']
49        next_url = request.route_url('view_page', pagename=page.name)
50        return HTTPSeeOther(location=next_url)
51    return dict(
52        pagename=page.name,
53        pagedata=page.data,
54        save_url=request.route_url('edit_page', pagename=page.name),
55    )
56
57@view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2')
58def add_page(request):
59    pagename = request.matchdict['pagename']
60    if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
61        next_url = request.route_url('edit_page', pagename=pagename)
62        return HTTPSeeOther(location=next_url)
63    if request.method == 'POST':
64        body = request.params['body']
65        page = models.Page(name=pagename, data=body)
66        page.creator = (
67            request.dbsession.query(models.User).filter_by(name='editor').one())
68        request.dbsession.add(page)
69        next_url = request.route_url('view_page', pagename=pagename)
70        return HTTPSeeOther(location=next_url)
71    save_url = request.route_url('add_page', pagename=pagename)
72    return dict(pagename=pagename, pagedata='', save_url=save_url)

我们添加了一些导入,并创建了一个正则表达式来查找“wikiwords”。

我们摆脱了 my_view 视图函数及其修饰符,在我们选择 sqlalchemy CookiCutter中的后端选项。这只是一个例子,与我们的应用程序无关。我们还删除了 db_err_msg 字符串。

然后我们加了四个 view callable 我们的功能 tutorial/views/default.py 模块,如前一步所述:

  • view_wiki() -显示wiki本身。它将在根URL上应答。

  • view_page() -显示单个页面。

  • edit_page() -允许用户编辑页面。

  • add_page() -允许用户添加页面。

我们将在下面的部分中对每一个进行简要描述。

备注

文件名没有什么特别之处 default.py 但它是一个python模块。在任意命名的模块中,一个项目的代码库中可能有许多视图可调用文件。实现视图可调用文件的模块通常 view 以它们的名义(或者可能位于名为的应用程序包的python子包中) views 但这只是惯例,不是要求。

这个 view_wiki 视图函数

下面是 view_wiki 视图函数及其修饰器:

16@view_config(route_name='view_wiki')
17def view_wiki(request):
18    next_url = request.route_url('view_page', pagename='FrontPage')
19    return HTTPSeeOther(location=next_url)

view_wiki()default view 当对wiki的根URL发出请求时会调用它。它总是重定向到表示“FrontPage”路径的URL。

这个 view_wiki View Callable始终重定向到名为“FrontPage”的网页资源的URL。为此,它返回 pyramid.httpexceptions.HTTPSeeOther 类(其实例实现 pyramid.interfaces.IResponse 界面,像 pyramid.response.Response )它使用 pyramid.request.Request.route_url() 用于构造 FrontPage 页面(即 http://localhost:6543/FrontPage ,并将其用作 HTTPSeeOther 响应,形成HTTP重定向。

这个 view_page 视图函数

这是密码 view_page 视图函数及其修饰器:

21@view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2')
22def view_page(request):
23    pagename = request.matchdict['pagename']
24    page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
25    if page is None:
26        raise HTTPNotFound('No such page')
27
28    def add_link(match):
29        word = match.group(1)
30        exists = request.dbsession.query(models.Page).filter_by(name=word).all()
31        if exists:
32            view_url = request.route_url('view_page', pagename=word)
33            return '<a href="%s">%s</a>' % (view_url, escape(word))
34        else:
35            add_url = request.route_url('add_page', pagename=word)
36            return '<a href="%s">%s</a>' % (add_url, escape(word))
37
38    content = publish_parts(page.data, writer_name='html')['html_body']
39    content = wikiwords.sub(add_link, content)
40    edit_url = request.route_url('edit_page', pagename=page.name)
41    return dict(page=page, content=content, edit_url=edit_url)

view_page() 用于显示wiki的单个页面。它渲染了 reStructuredText 页面正文(存储为 data 的属性 Page 模型对象)作为HTML。然后它用HTML锚替换每个锚 WikiWord 使用已编译的正则表达式在呈现的HTML中引用。

名为 add_link 用作 wikiwords.sub ,表示应该调用它为内容中找到的每个wikiword匹配项提供一个值。如果wiki已经包含一个具有匹配wikiword名称的页面, add_link() 生成一个视图链接,用作替换值并返回它。如果wiki不包含具有匹配wikiword名称的网页, add_link() 生成一个“添加”链接作为替换值并返回它。

因此, content 变量现在是一个完整的HTML格式,包含各种视图,并根据当前页面对象的内容为wikiwords添加链接。

然后,我们生成一个编辑URL,因为在这里比在模板中更容易执行,并且我们返回一个带有许多参数的字典。事实是 view_page() 返回字典(与 response 对象)是指向 Pyramid 它应该尝试使用 renderer 与呈现响应的视图配置关联。在我们的例子中,使用的渲染器将是 view.jinja2 模板,如 @view_config 应用于的装饰器 view_page() .

如果页面不存在,那么我们需要通过提升 pyramid.httpexceptions.HTTPNotFound 触发404处理,定义见 tutorial/views/notfound.py .

备注

使用 raise 对战 return 除了HTTP例外,这是一个很重要的区别,通常会把人搞得一团糟。在 tutorial/views/notfound.py 有一个 exception view 注册处理 HTTPNotFound 例外。只有引发的异常才会触发异常视图。如果 HTTPNotFound 返回,然后它有一个内部的“stock”模板,将使用该模板将自己呈现为响应。如果您没有看到异常视图被执行,这很可能是问题所在!见 在视图可调用文件中使用特殊异常 有关异常视图的详细信息。

这个 edit_page 视图函数

这是密码 edit_page 视图函数及其修饰器:

43@view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2')
44def edit_page(request):
45    pagename = request.matchdict['pagename']
46    page = request.dbsession.query(models.Page).filter_by(name=pagename).one()
47    if request.method == 'POST':
48        page.data = request.params['body']
49        next_url = request.route_url('view_page', pagename=page.name)
50        return HTTPSeeOther(location=next_url)
51    return dict(
52        pagename=page.name,
53        pagedata=page.data,
54        save_url=request.route_url('edit_page', pagename=page.name),
55    )

edit_page() 当用户单击视图窗体上的“编辑此页”按钮时调用。它呈现一个编辑表单,但它也充当它所呈现表单的处理程序。这个 matchdict 传递给的请求的属性 edit_page 视图将具有 'pagename' 与用户要编辑的页面名称匹配的键。

如果视图执行 is 提交表格的结果(即。, request.method == 'POST' )视图抓取 body 元素,并将其设置为 data 页对象的属性。然后重定向到 view_page 维基页面的视图。

如果视图执行是 not 表单提交的结果(即表达式 request.method != 'POST' ,视图只呈现编辑表单,传递页面对象和 save_url 将用作生成表单的操作。

备注

自从我们 request.dbsession 上一章中的定义是在 pyramid_tm 事务管理器,我们对会话管理的对象所做的任何更改都将自动提交。如果出现错误(甚至在稍后的模板代码中),更改将被中止。这意味着视图本身不需要关注提交/回滚逻辑。

这个 add_page 视图函数

这是密码 add_page 视图函数及其修饰器:

57@view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2')
58def add_page(request):
59    pagename = request.matchdict['pagename']
60    if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
61        next_url = request.route_url('edit_page', pagename=pagename)
62        return HTTPSeeOther(location=next_url)
63    if request.method == 'POST':
64        body = request.params['body']
65        page = models.Page(name=pagename, data=body)
66        page.creator = (
67            request.dbsession.query(models.User).filter_by(name='editor').one())
68        request.dbsession.add(page)
69        next_url = request.route_url('view_page', pagename=pagename)
70        return HTTPSeeOther(location=next_url)
71    save_url = request.route_url('add_page', pagename=pagename)
72    return dict(pagename=pagename, pagedata='', save_url=save_url)

add_page() 当用户单击 WikiWord 它还没有在系统中表示为一个页面。这个 add_link 中的函数 view_page 视图生成指向此视图的URL。 add_page() 还充当要添加页面对象时生成的表单的处理程序。这个 matchdict 传递给的请求的属性 add_page() 视图将具有构造URL和查找模型对象所需的值。

这个 matchdict 将有一个 'pagename' 与要添加的页的名称匹配的键。例如,如果通过调用添加视图, http://localhost:6543/add_page/SomeName 的价值 'pagename'matchdict'SomeName' .

接下来执行检查以确定 Page 数据库中已存在。如果它已经存在,则客户端将重定向到 edit_page 视图,否则我们继续下一个检查。

如果视图执行 is 表单提交的结果(即表达式 request.method == 'POST' ,我们从表单数据中获取页面主体,创建一个具有此页面主体和取自的名称的页面对象。 matchdict['pagename'] ,并使用将其保存到数据库中 request.dbession.add . 由于我们还没有涉及身份验证,因此我们没有登录用户可以添加为页面的 creator . 在我们到达教程中的那个点之前,我们假设所有页面都是由 editor 用户。因此,我们查询该对象,并将其设置为 page.creator . 最后,我们将客户机重定向回 view_page 新创建页面的视图。

如果视图执行是 not 表单提交的结果(即表达式 request.method != 'POST'False )视图可调用呈现模板。为此,它生成一个 save_url 模板在呈现期间用作表单发布URL。我们在这里很懒惰,所以我们将使用相同的模板 (templates/edit.jinja2 )对于添加视图和页面编辑视图。为此,我们创建一个虚拟对象 Page 对象以满足编辑窗体的 some 页面对象公开为 page . Pyramid 将与此视图关联的模板呈现为响应。

添加模板

这个 view_pageadd_pageedit_page 我们添加了引用A的视图 template . 每个模板都是 Jinja2 模板。这些模板将位于 templates 教程包的目录。Jinja2模板必须具有 .jinja2 延期应视为延期。

这个 layout.jinja2 模板

更新 tutorial/templates/layout.jinja2 内容如下,以强调的线条表示:

 1<!DOCTYPE html>
 2<html lang="{{request.locale_name}}">
 3  <head>
 4    <meta charset="utf-8">
 5    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7    <meta name="description" content="pyramid web application">
 8    <meta name="author" content="Pylons Project">
 9    <link rel="shortcut icon" href="{{request.static_url('tutorial:static/pyramid-16x16.png')}}">
10
11    <title>{% block subtitle %}{% endblock %}Pyramid tutorial wiki (based on TurboGears 20-Minute Wiki)</title>
12
13    <!-- Bootstrap core CSS -->
14    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
15
16    <!-- Custom styles for this scaffold -->
17    <link href="{{request.static_url('tutorial:static/theme.css')}}" rel="stylesheet">
18
19    <!-- HTML5 shiv and Respond.js IE8 support of HTML5 elements and media queries -->
20    <!--[if lt IE 9]>
21      <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
22      <script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></script>
23    <![endif]-->
24  </head>
25
26  <body>
27
28    <div class="starter-template">
29      <div class="container">
30        <div class="row">
31          <div class="col-md-2">
32            <img class="logo img-responsive" src="{{request.static_url('tutorial:static/pyramid.png') }}" alt="pyramid web framework">
33          </div>
34          <div class="col-md-10">
35            <div class="content">
36            {% block content %}{% endblock %}
37            </div>
38          </div>
39        </div>
40        <div class="row">
41          <div class="links">
42            <ul>
43              <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
44              <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://web.libera.chat/#pyramid">IRC Channel</a></li>
45              <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
46            </ul>
47          </div>
48        </div>
49        <div class="row">
50          <div class="copyright">
51            Copyright &copy; Pylons Project
52          </div>
53        </div>
54      </div>
55    </div>
56
57
58    <!-- Bootstrap core JavaScript
59    ================================================== -->
60    <!-- Placed at the end of the document so the pages load faster -->
61    <script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
62    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
63  </body>
64</html>

因为我们使用的是模板引擎,所以我们可以将页面模板中的公共样板文件分解为可重用的组件。这样做的一个方法是通过块继承模板。

  • 我们在布局模板中定义了两个占位符,其中子模板可以覆盖内容。这些块被命名为 subtitle (第11行) content (第36行)。

  • 请参阅 Jinja2 documentation 有关模板继承的详细信息。

这个 view.jinja2 模板

创造 tutorial/templates/view.jinja2 并增加以下内容:

 1{% extends 'layout.jinja2' %}
 2
 3{% block subtitle %}{{page.name}} - {% endblock subtitle %}
 4
 5{% block content %}
 6<p>{{ content|safe }}</p>
 7<p>
 8<a href="{{ edit_url }}">
 9    Edit this page
10</a>
11</p>
12<p>
13    Viewing <strong>{{page.name}}</strong>, created by <strong>{{page.creator.name}}</strong>.
14</p>
15<p>You can return to the
16<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
17</p>
18{% endblock content %}

此模板由使用 view_page() 用于显示单个wiki页面。

  • 我们从扩展 layout.jinja2 上面定义的模板,它提供了页面的框架(第1行)。

  • 我们推翻了 subtitle 从基本布局中阻止,将页面名称插入页面标题(第3行)。

  • 我们推翻了 content 从基本布局阻止将标记插入正文(第5-18行)。

  • 我们使用的变量被替换为 content 视图提供的值(第6行)。 content 包含HTML,因此 |safe 过滤器用于防止其转义(例如,将“>”更改为“&gt;”)。

  • 我们创建一个指向“编辑”URL的链接,当单击该链接时,将调用 edit_page 所请求页面的视图(第8-10行)。

这个 edit.jinja2 模板

创造 tutorial/templates/edit.jinja2 并增加以下内容:

 1{% extends 'layout.jinja2' %}
 2
 3{% block subtitle %}Edit {{pagename}} - {% endblock subtitle %}
 4
 5{% block content %}
 6<p>
 7Editing <strong>{{pagename}}</strong>
 8</p>
 9<p>You can return to the
10<a href="{{request.route_url('view_page', pagename='FrontPage')}}">FrontPage</a>.
11</p>
12<form action="{{ save_url }}" method="post">
13<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
14<div class="form-group">
15    <textarea class="form-control" name="body" rows="10" cols="60">{{ pagedata }}</textarea>
16</div>
17<div class="form-group">
18    <button type="submit" class="btn btn-default">Save</button>
19</div>
20</form>
21{% endblock content %}

此模板服务于两个用例。它被使用 add_page()edit_page() 用于添加和编辑wiki页面。它显示一个包含表单的页面,并提供以下内容:

  • 我们再次扩展 layout.jinja2 模板,提供页面的框架(第1行)。

  • 重写 subtitle 阻止以影响 <title> 标签 head 第页(第3行)。

  • 将CSRF令牌添加到表单中(第13行)。如果没有此行,尝试编辑页面将导致 400 Bad Request 错误。

  • 10行乘60列 textarea 字段名 body 它在呈现时被任何现有的页面数据填充(第15行)。

  • 提交按钮(第18行)。

  • 表单将回发到 save_url 视图提供的参数(第12行)。视图将使用 body 价值。

这个 404.jinja2 模板

替换 tutorial/templates/404.jinja2 with the following content:

1{% extends "layout.jinja2" %}
2
3{% block content %}
4<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
5<p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
6{% endblock content %}

此模板链接自 notfound_view 定义在 tutorial/views/notfound.py 如图所示:

1from pyramid.view import notfound_view_config
2
3
4@notfound_view_config(renderer='tutorial:templates/404.jinja2')
5def notfound_view(request):
6    request.response.status = 404
7    return {}

关于此配置,需要注意以下几点:

  • 这个 notfound_view 在上面的代码片段中,称为 exception view . 有关详细信息,请参阅 在视图可调用文件中使用特殊异常 .

  • 这个 notfound_view 将响应状态设置为404。可以通过以下方式影响渲染器使用的响应对象: 呈现响应的不同属性 .

  • 这个 notfound_view 注册为异常视图并将被调用 only 如果 pyramid.httpexceptions.HTTPNotFound 作为异常引发。这意味着对于通常从视图返回的任何响应都不会调用它。例如,在第27行 tutorial/views/default.py 引发将触发视图的异常。

最后,我们可以删除 tutorial/templates/mytemplate.jinja2 通过选择的后端选项提供的模板 sqlalchemy ,因为我们已经为wiki创建了自己的模板。

备注

我们的模板使用 request 对象,我们的教程视图都没有返回到它们的字典中。 request 是使用模板呈现器时模板中“默认”可用的几个名称之一。见 渲染期间使用的系统值 有关将模板用作渲染器时默认情况下可用的其他名称的信息。

在浏览器中查看应用程序

我们最终可以在浏览器中检查我们的应用程序(请参见 启动应用程序 )启动浏览器并访问以下每个URL,检查结果是否符合预期: