ITEEDU

| 上一章 | 目 录 | 下一章 |

第九章 模板高级进阶

虽然和Django的模板语言的大多数交互都是模板作者的工作,但你可能想定制和扩展模板引擎,让它做一些它不能做的事情,或者是以其他方式让你的工作更轻松。

本章深入钻研Django的模板系统。 如果你想扩展模板系统或者只是对它的工作原理感觉到好奇,本章涉及了你需要了解的东西。 它也包含一个自动转意特征,当你继续使用django的时候随着时间推移你一定会注意这个安全考虑。

如果你想把Django的模版系统作为另外一个应用程序的一部分(比如,仅使用django的模板系统而不使用Django框架的其他部分),那你一定要读一下“配置独立模式下的模版系统”这一节。

模板语言回顾

首先,让我们快速回顾一下第四章介绍的若干专业术语

模板 是一个纯文本文件,或是一个用Django模板语言标记过的普通的Python字符串,一个模板可以包含区块标签和变量。 模板可以包含模板标签和变量。

区块标签 是在一个模板里面起作用的的标记,这个定义故意说的很含糊,比如,一个 区块标签可以生成内容,可以作为一个控制结构( if 语句或 for 循环), 可以获取数据库内容,或者访问其他的模板标签。 这个定义故意搞得模糊不清。 例如,一个模版标签能够产生作为控制结构的内容(一个 iffor 循环),

区块标签被 {%%} 包含:

{% if is_logged_in %}
    Thanks for logging in!
{% else %}
    Please log in.
{% endif %}

变量 是一个在模板里用来输出值的标记。

变量标签被 {{}} 包含:

My first name is {{ first_name }}. My last name is {{ last_name }}.

context 是一个传递给模板的名称到值的映射(类似Python字典)。

模板 渲染 就是是通过从context获取值来替换模板中变量并执行所有的区块标签。

关于这些基本概念更详细的内容,请参考第四章。

本章的其余部分讨论了扩展模板引擎的方法。 首先,我们快速的看一下第四章遗留的内容。

RequestContext和Context处理器

你需要一段context来解析模板。 一般情况下,这是一个 django.template.Context 的实例,不过在Django中还可以用一个特殊的子类, django.template.RequestContext ,这个运用起来稍微有些不同。 RequestContext 默认地在模板context中加入了一些变量,如 HttpRequest 对象或当前登录用户的相关信息。

当你不想在一系例模板中都明确指定一些相同的变量时,你应该使用 RequestContext 。例如,看下面的四个视图: 例如,考虑这两个视图:

from django.template import loader, Context

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am view 1.'
    })
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the second view.'
    })
    return t.render(c)

(注意,在这些例子中,我们故意 使用 render_to_response() 这个快捷方法,而选择手动载入模板,手动构造context对象然后渲染模板。 是为了能够清晰的说明所有步骤。

每个视图都给模板传入了三个相同的变量: appuserip_address 。如果我们能把这些冗余去掉会不会看起来更好?

创建 RequestContextcontext处理器 就是为了解决这个问题。 Context处理器允许你设置一些变量,它们会在每个context中自动被设置好,而不必每次调用 render_to_response() 时都指定。 要点就是,当你渲染模板时,你要用 RequestContext 而不是 Context

最直接的做法是用context处理器来创建一些处理器并传递给 RequestContext 。上面的例子可以用context processors改写如下:

from django.template import loader, RequestContext

def custom_proc(request):
    "A context processor that provides 'app', 'user' and 'ip_address'."
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = RequestContext(request, {'message': 'I am view 1.'},
            processors=[custom_proc])
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = RequestContext(request, {'message': 'I am the second view.'},
            processors=[custom_proc])
    return t.render(c)

我们来通读一下代码:

  • 首先,我们定义一个函数 custom_proc 。这是一个context处理器,它接收一个 HttpRequest 对象,然后返回一个字典,这个字典中包含了可以在模板context中使用的变量。 它就做了这么多。

  • 我们在这四个视图函数中用 RequestContext 代替了 Context 。在context对象的构建上有两个不同点。 一, RequestContext 的第一个参数需要传递一个 HttpRequest 对象,就是传递给视图函数的第一个参数( request )。二, RequestContext 有一个可选的参数 processors ,这是一个包含context处理器函数的list或者tuple。 在这里,我们传递了我们之前定义的函数 curstom_proc

  • 每个视图的context结构里不再包含 appuserip_address 等变量,因为这些由 custom_proc 函数提供了。

  • 每个视图 仍然 具有很大的灵活性,可以引入我们需要的任何模板变量。 在这个例子中, message 模板变量在每个视图中都不一样。

在第四章,我们介绍了 render_to_response() 这个快捷方式,它可以省掉调用 loader.get_template() ,然后创建一个 Context 对象,最后再调用模板对象的 render() 方法。 为了讲解context处理器底层是如何工作的,在上面的例子中我们没有使用 render_to_response() 。但是建议选择 render_to_response() 作为context的处理器。

from django.shortcuts import render_to_response
from django.template import RequestContext

def custom_proc(request):
    "A context processor that provides 'app', 'user' and 'ip_address'."
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view_1(request):
    # ...
    return render_to_response('template1.html',
        {'message': 'I am view 1.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

def view_2(request):
    # ...
    return render_to_response('template2.html',
        {'message': 'I am the second view.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

在这,我们将每个视图的模板渲染代码写成了一个单行。

虽然这是一种改进,但是,请考虑一下这段代码的简洁性,我们现在不得不承认的是在 另外 一方面有些过分了。 我们以代码冗余(在 processors 调用中)的代价消除了数据上的冗余(我们的模板变量)。 由于你不得不一直键入 processors ,所以使用context处理器并没有减少太多的打字次数。

Django因此提供对 全局 context处理器的支持。 TEMPLATE_CONTEXT_PROCESSORS 指定了 总是 使用哪些 context processors 。这样就省去了每次使用 RequestContext 都指定 processors 的麻烦^_^。

默认情况下, TEMPLATE_CONTEXT_PROCESSORS 设置如下:

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
)

这个设置是一个可调用函数的Tuple,其中的每个函数使用了和上文中我们的 custom_proc 相同的接口: 接收一个request对象作为参数,返回一个包含了将被合并到context中的项的字典。

每个处理器将会按照顺序应用。 也就是说如果你在第一个处理器里面向context添加了一个变量,而第二个处理器添加了同样名字的变量,那么第二个将会覆盖第一个。

Django提供了几个简单的context处理器,有些在默认情况下被启用的。

django.core.context_processors.auth

如果 TEMPLATE_CONTEXT_PROCESSORS 包含了这个处理器,那么每个 RequestContext 将包含这些变量:

  • user :一个 django.contrib.auth.models.User 实例,描述了当前登录用户(或者一个 AnonymousUser 实例,如果客户端没有登录)。

  • messages :一个当前登录用户的消息列表(字符串)。 在后台,对每一个请求这个变量都调用 request.user.get_and_delete_messages() 方法。 这个方法收集用户的消息然后把它们从数据库中删除。

  • permsdjango.core.context_processors.PermWrapper 的一个实例,包含了当前登录用户有哪些权限。

关于users、permissions和messages的更多内容请参考第12章。

django.core.context_processors.debug

这个处理器把调试信息发送到模板层。 如果包含TEMPLATE_CONTEXT_PROCESSORS处理器,每一个RequestContext将包含这些变量:

  • debug :你设置的 DEBUG 的值( TrueFalse )。你可以在模板里面用这个变量测试是否处在debug模式下。

  • sql_queries :包含类似于 ``{‘sql’: …, ‘time’: `` 的字典的一个列表, 记录了这个请求期间的每个SQL查询以及查询所耗费的时间。 这个列表是按照请求顺序进行排列的。

    System Message: WARNING/2 (<string>, line 315); backlink

    Inline literal start-string without end-string.

由于调试信息比较敏感,所以这个context处理器只有当同时满足下面两个条件的时候才有效:

  • DEBUG 参数设置为 True

  • 请求的ip应该包含在 INTERNAL_IPS 的设置里面。

Astute readers will notice that the debug template variable will never have the value False because, if DEBUG is False , then the debug template variable won’t be populated in the first place.

django.core.context_processors.i18n

如果这个处理器启用,每个 RequestContext 将包含下面的变量:

  • LANGUAGESLANGUAGES 选项的值。

  • LANGUAGE_CODE :如果 request.LANGUAGE_CODE 存在,就等于它;否则,等同于 LANGUAGE_CODE 设置。

附录E提供了有关这两个设置的更多的信息。

django.core.context_processors.request

如果启用这个处理器,每个 RequestContext 将包含变量 request , 也就是当前的 HttpRequest 对象。 注意这个处理器默认是不启用的,你需要激活它。

如果你发现你的模板需要访问当前的HttpRequest你就需要使用它:

{{ request.REMOTE_ADDR }}

写Context处理器的一些建议

编写处理器的一些建议:

  • 使每个context处理器完成尽可能小的功能。 使用多个处理器是很容易的,所以你可以根据逻辑块来分解功能以便将来重用。

  • 要注意 TEMPLATE_CONTEXT_PROCESSORS 里的context processor 将会在 每个 模板中有效,所以要变量的命名不要和模板的变量冲突。 变量名是大小写敏感的,所以processor的变量全用大写是个不错的主意。

  • 只要它们存放在你的Python的搜索路径中,它们放在哪个物理路径并不重要,这样你可以在 TEMPLATE_CONTEXT_PROCESSORS 设置里指向它们。 也就是说,你要把它们放在app或者project目录里名为 context_processors.py 的文件。

html自动转意

从模板生成html的时候,总是有一个风险——变量包了含会影响结果html的字符。 例如,考虑这个模板片段:

Hello, {{ name }}.

首先,这看起来是显示用户名的一个无害的途径,但是考虑如果用户输入如下的名字将会发生什么:

<script>alert('hello')</script>

用这个用户名,模板将被翻译成:

Hello, <script>alert('hello')</script>

这意味着浏览器将弹出JavaScript警告框!

Similarly, what if the name contained a '<' symbol, like this?

<b>username

那样的话模板结果被翻译成这样:

Hello, <b>username

页面的剩余部分变成了粗体!

显然,用户提交的数据不应该被盲目信任,直接插入到你的页面中。因为一个潜在的恶意的用户能够利用这类漏洞做坏事。 利用这类漏洞被Cross Site Scripting (XSS) 攻击。 关于安全的更多内容,请看20章

为了避免这个问题,你有两个选择:

  • 一是你可以确保每一个不被信任的变量通过过滤器,它把潜在有害的html字符转换为无害的。escape 这是最初几年django的默认方案,但是这样的问题是把责任推给you 自己,开发者、模版作者,来确保转意每一件事情。 很容易就忘记转意数据。

  • 二是,你可以利用django的自动html转意。 这一章的剩余部分描述自动转意是如何工作的。

在django里默认情况下,每一个模板自动转意每一个变量标签的输出。 尤其是这五个字符。

  • < is converted to <

  • > 被转换为>

  • ' (single quote) is converted to '

  • " (double quote) is converted to "

  • & is converted to &

另外,我强调一下这个行为默认是开启的。 如果你正在使用django的模板系统,那么你是被保护的。

如何关闭它

如果你不想数据被自动转意,在每一站点级别、每一模板级别或者每一变量级别你都能用两三中方法来关闭它。

为什么要关闭它? 因为有时候模板变量包含打算被翻译为原始html的数据,在这种情况下我们不想它们的内容被转意。 例如,你可能在数据库里存储了一块被信任的html,并且你想直接把它嵌入到你的模板里。 或者,你可能正在使用django的模板系统生成非html文本,比如一个e-mail信息。

对于特殊的变量

用safe过滤器为单独的变量关闭自动转意:

This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}

Think of safe as shorthand for safe from further escaping or can be safely interpreted as HTML . In this example, if data contains '<b>' , the output will be:

This will be escaped: &lt;b&gt;
This will not be escaped: <b>

For Template Blocks

为了控制模板的自动转意,用标签autoescape来包装整个模板(或者模板中常用的部分),就像这样

{% autoescape off %}
    Hello {{ name }}
{% endautoescape %}

autoescape 标签有两个参数on和off 同时,你可能想阻止一部分自动转意,对另一部分自动转意 这是一个模板的例子

Auto-escaping is on by default. Hello {{ name }}

{% autoescape off %}
    This will not be auto-escaped: {{ data }}.

    Nor this: {{ other_data }}
    {% autoescape on %}
        Auto-escaping applies again: {{ name }}
    {% endautoescape %}
{% endautoescape %}

auto-escaping 标签的作用域不仅可以影响到当前模板还可以通过include标签作用到其他标签,就像block标签一样 例如:

# base.html

{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
{% endautoescape %}

# child.html

{% extends "base.html" %}
{% block title %}This & that{% endblock %}
{% block content %}{{ greeting }}{% endblock %}

由于在base模板中自动转意被关闭,所以在child模板中自动转意也会关闭.因此,在下面一段HTML被提交时,变量greeting的值就为字符串Hello!

<h1>This & that</h1>
<b>Hello!</b>

备注

通常,模板作者没必要为自动转意担心. 基于pyhton的开发者(编写VIEWS视图和自定义过滤器)只需要考虑哪些数据不需要被转意,适时的标记数据,就可以让它们在模板中工作

如果你正在编写一个模板而不知道是否要关闭自动转意,那就为所有需要转意的变量添加一个escape过滤器. 当自动转意开启时,使用escape过滤器似乎会两次转意数据,但其实没有任何危险.因为escape过滤器不作用于被转意的变量.

Automatic Escaping of String Literals in Filter Arguments

就像我们前面提到的,过滤器也可以是字符串.

{{ data|default:"This is a string literal." }}

所有字符常量没有经过转义就被插入模板,就如同它们都经过了safe过滤 这是由于字符常量完全由模板作者决定,因此编写模板的时候必须确保文本的正确性.

这意味着你必须这样写

{{ data|default:"3 &lt; 2" }}

而不是这样

{{ data|default:"3 < 2" }}  <-- Bad! Don't do this.

来源于自变量的数据不受影响 如果必要,变量内容会自然的转义,因为它们始终都在模板作者的控制下.

模板加载的内幕

一般说来,你会把模板以文件的方式存储在文件系统中,但是你也可以使用自定义的 template loaders 从其他来源加载模板。

Django有两种方法加载模板

  • django.template.loader.get_template(template_name)get_template 根据给定的模板名称返回一个已编译的模板(一个 Template 对象)。 如果模板不存在,就触发 TemplateDoesNotExist 的异常。

  • django.template.loader.select_template(template_name_list)select_template 很像 get_template ,不过它是以模板名称的列表作为参数的,并且它返回第一个存在的模板。 如果模板都不存在,将会触发 TemplateDoesNotExist 异常。 If none of the templates exist, a TemplateDoesNotExist exception will be raised.

正如在第四章中所提到的,默认情况下这些函数使用 TEMPLATE_DIRS 的设置来载入模板。 但是,在内部这些函数可以指定一个模板加载器来完成这些繁重的任务。

一些加载器默认被禁用,但是你可以通过编辑 TEMPLATE_LOADERS 设置来激活它们。 TEMPLATE_LOADERS 应当是一个字符串的元组,其中每个字符串都表示一个模板加载器。 这些模板加载器随Django一起发布。

django.template.loaders.filesystem.load_template_source : 这个加载器根据 TEMPLATE_DIRS 的设置从文件系统加载模板。

django.template.loaders.app_directories.load_template_source : 这个加 载器从文件系统上的Django应用中加载模板。 对 INSTALLED_APPS 中的每个应用,这个加 载器会查找一个 templates 子目录。 如果这个目录存在,Django就在那里寻找模板。

这意味着你可以把模板和你的应用一起保存,从而使得Django应用更容易和默认模板一起发布。 例如,如果 INSTALLED_APPS 包含 ('myproject.polls','myproject.music') ,那么 get_template('foo.html') 会按这个顺序查找模板:

  • /path/to/myproject/polls/templates/foo.html

  • /path/to/myproject/music/templates/foo.html

请注意加载器在首次被导入的时候会执行一个优化: 它会缓存一个列表,这个列表包含了 INSTALLED_APPS 中带有 templates 子目录的包。

这个加载器默认启用。

django.template.loaders.eggs.load_template_source : 这个加载器类似 app_directories ,只不过它从Python eggs而不是文件系统中加载模板。 这个加载器默认被禁用;如果你使用eggs来发布你的应用,那么你就需要启用它。 (Python eggs are a way of compressing Python code into a single file.)

Django按照 TEMPLATE_LOADERS 设置中的顺序使用模板加载器。 它逐个使用每个加载器直至找到一个匹配的模板。

扩展模板系统

既然你已经对模板系统的内幕了解多了一些,让我们来看看如何使用自定义的代码来拓展这个系统吧。

绝大部分的模板定制是以自定义标签/过滤器的方式来完成的。 尽管Django模板语言自带了许多内建标签和过滤器,但是你可能还是需要组建你自己的标签和过滤器库来满足你的需要。 幸运的是,定义你自己的功能非常容易。

创建一个模板库

不管是写自定义标签还是过滤器,第一件要做的事是给 template library 创建使Django能够勾入的机制。

创建一个模板库分两步走:

第一,决定哪个Django应用应当拥有这个模板库。 如果你通过 manage.py startapp 创建了一个应用,你可以把它放在那里,或者你可以为模板库单独创建一个应用。 We’d recommend the latter, because your filters might be useful to you in future projects.

无论你采用何种方式,请确保把你的应用添加到 INSTALLED_APPS 中。我们稍后会解释这一点。 We’ll explain this shortly.

第二,在适当的Django应用包里创建一个 templatetags 目录。 这个目录应当和 models.pyviews.py 等处于同一层次。 For example:

books/
    __init__.py
    models.py
    templatetags/
    views.py

templatetags 中创建两个空文件: 一个 __init__.py (告诉Python这是 一个包含了Python代码的包)和一个用来存放你自定义的标签/过滤器定义的文件。 第二个文件 的名字稍后将用来加载标签。 例如,如果你的自定义标签/过滤器在一个叫作 poll_extras.py 的文件中,你需要在模板中写入如下内容:

{% load poll_extras %}

{% load %} 标签检查 INSTALLED_APPS 中的设置,仅允许加载已安装的Django应用程序中的模板库。 这是一个安全特性。

如果你写了一个不和任何模型/视图关联的模板库,那么得到一个仅包含 templatetags 包的Django应用程序包是完全正常的。 对于在 templatetags 包中放置多少个模块没有做任何的限制。 需要了解的是:

一旦创建了Python模块,你只需根据是要编写过滤器还是标签来相应的编写一些Python代码。

To be a valid tag library, the module must contain a module-level variable named register that is an instance of template.Library . This is the data structure in which all the tags and filters are registered. 这个 template.Library 实例是包含所有已注册的标签及过滤器的数据结构。

from django import template

register = template.Library()

Note

请阅读Django默认的过滤器和标签的源码,那里有大量的例子。 他们分别为: django/template/defaultfilters.pydjango/template/defaulttags.py 。某些应用程序在 django.contrib 中也包含模板库。

创建 register 变量后,你就可以使用它来创建模板的过滤器和标签了。

自定义模板过滤器

自定义过滤器就是有一个或两个参数的Python函数:

  • (输入)变量的值

  • 参数的值, 可以是默认值或者完全留空

例如,在过滤器 {{ var|foo:"bar" }} 中 ,过滤器 foo 会被传入变量 var 和参数 bar 的内容。

过滤器函数应该总有返回值,而且不能触发异常,它们都应该静静的失败。 如果有一个错误发生,它们要么返回原始的输入字符串,要么返回空的字符串,无论哪个都可以。 If there’s an error, they should return either the original input or an empty string, whichever makes more sense.

这里是一些定义过滤器的例子:

def cut(value, arg):
    "Removes all values of arg from the given string"
    return value.replace(arg, '')

And here’s an example of how that filter would be used to cut spaces from a variable’s value:

{{ somevariable|cut:" " }}

大多数过滤器并不需要参数。 下面的例子把参数从你的函数中拿掉了:

def lower(value): # Only one argument.
    "Converts a string into all lowercase"
    return value.lower()

当你在定义你的过滤器时,你需要用 Library 实例来注册它,这样就能通过Django的模板语言来使用了:

register.filter('cut', cut)
register.filter('lower', lower)

Library.filter() 方法需要两个参数:

  • 过滤器的名称(一个字串)

  • 过滤器函数本身

如果你使用的是Python 2.4或更新,你可以使用 register.filter() 作为一个装饰器:

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

像第二个例子中,如果你不使用 name 参数,那么Django将会使用函数名作为过滤器的名字。

下面是一个完整的模板库的例子,提供了一个 cut 过滤器:

from django import template

register = template.Library()

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

自定义模板标签

标签要比过滤器复杂些,标签几乎能做任何事情。

第四章描述了模板系统的两步处理过程: 编译和呈现。 为了自定义一个这样的模板标签,你需要告诉Django当遇到你的标签时怎样进行这过程。

当Django编译一个模板时,它将原始模板分成一个个 节点 。每个节点都是 django.template.Node 的一个实例,并且具备 render() 方法。 于是,一个已编译的模板就是 Node 对象的一个列表。 For example, consider this template:

Hello, {{ person.name }}.

{% ifequal name.birthday today %}
    Happy birthday!
{% else %}
    Be sure to come back on your birthday
    for a splendid surprise message.
{% endifequal %}

In compiled template form, this template is represented as this list of nodes:

  • Text node: "Hello, "

  • Variable node: person.name

  • Text node: ".\n\n"

  • IfEqual node: name.birthday and today

当你调用一个已编译模板的 render() 方法时,模板就会用给定的context来调用每个在它的节点列表上的节点的 render() 方法。 所以,为了定义一个自定义的模板标签,你需要明确这个模板标签转换为一个 Node (已编译的函数)和这个node的 render() 方法。 Thus, to define a custom template tag, you specify how the raw template tag is converted into a Node (the compilation function) and what the node’s render() method does.

在下面的章节中,我们将详细解说写一个自定义标签时的所有步骤。

编写编译函数

当遇到一个模板标签(template tag)时,模板解析器就会把标签包含的内容,以及模板解析器自己作为参数调用一个python函数。 这个函数负责返回一个和当前模板标签内容相对应的节点(Node)的实例。

例如,写一个显示当前日期的模板标签: {% current_time %},该标签会根据参数指定的 strftime 格式(参见:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>

Note

没错, 这个模板标签是多余的,Django默认的 {% now %} 用更简单的语法完成了同样的工作。 这个模板标签在这里只是作为一个例子。

这个函数的分析器会获取参数并创建一个 Node 对象:

from django import template

register = template.Library()

def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        msg = '%r tag requires a single argument' % token.split_contents()[0]
        raise template.TemplateSyntaxError(msg)
    return CurrentTimeNode(format_string[1:-1])

There’s a lot going here:

  • Each template tag compilation function takes two arguments, parser and token . parser is the template parser object. We don’t use it in this example. token is the token currently being parsed by the parser.

  • token.contents 是包含有标签原始内容的字符串。 在我们的例子中,它是 'current_time "%Y-%m-%d %I:%M %p"'

  • token.split_contents() 方法按空格拆分参数同时保证引号中的字符串在一起。 应该避免使用 token.contents.split() (仅是使用Python的标准字符串拆分),它不够健壮,因为它只是简单的按照 所有 空格进行拆分,包括那些引号引起来的字符串中的空格。 Its not as robust, as it naively splits on all spaces, including those within quoted strings.

  • 这个函数负责抛出 django.template.TemplateSyntaxError ,同时提供所有语法错误的有用信息。

  • 不要把标签名称硬编码在你的错误信息中,因为这样会把标签名称和你的函数耦合在一起。 token.split_contents()[0]

  • 这个函数返回一个 CurrentTimeNode (稍后我们将创建它),它包含了节点需要知道的关于这个标签的全部信息。 在这个例子中,它只是传递了参数 "%Y-%m-%d %I:%M %p" 。模板标签开头和结尾的引号使用 format_string[1:-1] 除去。

  • 模板标签编译函数 必须 返回一个 Node 子类,返回其它值都是错的。

编写模板节点

编写自定义标签的第二步就是定义一个拥有 render() 方法的 Node 子类。 继续前面的例子,我们需要定义 CurrentTimeNode

import datetime

class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = str(format_string)

    def render(self, context):
        now = datetime.datetime.now()
        return now.strftime(self.format_string)

这两个函数( __init__render )与模板处理中的两步(编译与渲染)直接对应。 这样,初始化函数仅仅需要存储后面要用到的格式字符串,而 render() 函数才做真正的工作。

与模板过滤器一样,这些渲染函数应该捕获错误,而不是抛出错误。 模板标签只能在编译的时候才能抛出错误。

注册标签

最后,你需要用你的模块 Library 实例注册这个标签。 注册自定义标签与注册自定义过滤器非常类似(如前文所述)。 实例化一个 template.Library 实例然后调用它的 tag() 方法。 For example:

register.tag('current_time', do_current_time)

tag() 方法需要两个参数:

  • 模板标签的名字(字符串)。

  • 编译函数。

和注册过滤器类似,也可以在Python2.4及其以上版本中使用 register.tag 修饰:

@register.tag(name="current_time")
def do_current_time(parser, token):
    # ...

@register.tag
def shout(parser, token):
    # ...

如果你像在第二个例子中那样忽略 name 参数的话,Django会使用函数名称作为标签名称。

在上下文中设置变量

前一节的例子只是简单的返回一个值。 很多时候设置一个模板变量而非返回值也很有用。 那样,模板作者就只能使用你的模板标签所设置的变量。

要在上下文中设置变量,在 render() 函数的context对象上使用字典赋值。 这里是一个修改过的 CurrentTimeNode ,其中设定了一个模板变量 current_time ,并没有返回它:

class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = str(format_string)

    def render(self, context):
        now = datetime.datetime.now()
        context['current_time'] = now.strftime(self.format_string)
        return ''

(We’ll leave the creation of a do_current_time2 function, plus the registration of that function to a current_time2 template tag, as exercises for the reader.)

注意 render() 返回了一个空字符串。 render() 应当总是返回一个字符串,所以如果模板标签只是要设置变量, render() 就应该返回一个空字符串。

你应该这样使用这个新版本的标签:

{% current_time2 "%Y-%M-%d %I:%M %p" %}
<p>The time is {{ current_time }}.</p>

但是 CurrentTimeNode2 有一个问题: 变量名 current_time 是硬编码的。 这意味着你必须确定你的模板在其它任何地方都不使用 {{ current_time }} ,因为 {% current_time2 %} 会盲目的覆盖该变量的值。

一种更简洁的方案是由模板标签来指定需要设定的变量的名称,就像这样:

{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

为此,你需要重构编译函数和 Node 类,如下所示:

import re

class CurrentTimeNode3(template.Node):
    def __init__(self, format_string, var_name):
        self.format_string = str(format_string)
        self.var_name = var_name

    def render(self, context):
        now = datetime.datetime.now()
        context[self.var_name] = now.strftime(self.format_string)
        return ''

def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        msg = '%r tag requires arguments' % token.contents[0]
        raise template.TemplateSyntaxError(msg)

    m = re.search(r'(.*?) as (\w+)', arg)
    if m:
        fmt, var_name = m.groups()
    else:
        msg = '%r tag had invalid arguments' % tag_name
        raise template.TemplateSyntaxError(msg)

    if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
        msg = "%r tag's argument should be in quotes" % tag_name
        raise template.TemplateSyntaxError(msg)

    return CurrentTimeNode3(fmt[1:-1], var_name)

现在 do_current_time() 把格式字符串和变量名传递给 CurrentTimeNode3

分析直至另一个块标签

模板标签可以像包含其它标签的块一样工作(想想 {% if %}{% for %} 等)。 要创建一个这样的模板标签,在你的编译函数中使用 parser.parse()

标准的 {% comment %} 标签是这样实现的:

def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''

parser.parse() 接收一个包含了需要分析块标签名的元组作为参数. 它返回一个 django.template.NodeList 实例, 它是一个包含了所有 Node 对象的列表,这些对象代表了分析器在遇到元组中任一标签名之 的内容. It returns an instance of django.template.NodeList , which is a list of all Node objects that the parser encountered before it encountered any of the tags named in the tuple.

因此在前面的例子中, nodelist 是在 {% comment %}{% endcomment %} 之间所有节点的列表,不包括 {% comment %}{% endcomment %} 自身。

parser.parse() 被调用之后,分析器还没有清除 {% endcomment %} 标签,因此代码需要显式地调用 parser.delete_first_token() 来防止该标签被处理两次。

之后 CommentNode.render() 只是简单地返回一个空字符串。 在 {% comment %}{% endcomment %} 之间的所有内容都被忽略。

分析直至另外一个块标签并保存内容

在前一个例子中, do_comment() 抛弃了在 {% comment %}{% endcomment %} 之间的所有内容。

例如,这个自定义模板标签:

{% upper %}
    This will appear in uppercase, {{ user_name }}.
{% endupper %}

就像前面的例子一样,我们将使用 parser.parse() 。这次,我们将产生的 nodelist 传递给 Node

def do_upper(parser, token):
    nodelist = parser.parse(('endupper',))
    parser.delete_first_token()
    return UpperNode(nodelist)

class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist

    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()

这里唯一的一个新概念是 UpperNode.render() 中的 self.nodelist.render(context) 。它对节点列表中的每个 Node 简单的调用 render()

更多的复杂渲染示例请查看 django/template/defaulttags.py 中的 {% if %}{% for %}{% ifequal %}{% ifchanged %} 的代码。

简单标签的快捷方式

许多模板标签接收单一的字符串参数或者一个模板变量 引用,然后独立地根据输入变量和一些其它外部信息进行处理并返回一个字符串. 例如, 我们先前写的 current_time 标签就是这样一个例子. 我们给它格式字符串, 然后它把时间作为字符串返回. For example, the current_time tag we wrote earlier is of this variety. We give it a format string, and it returns the time as a string.

为了简化这类标签,Django 提供了一个帮助函数:

我们之前的的 current_time 函数于是可以写成这样:

def current_time(format_string):
    try:
        return datetime.datetime.now().strftime(str(format_string))
    except UnicodeEncodeError:
        return ''

register.simple_tag(current_time)

在Python 2.4中,也可以使用修饰语法:

@register.simple_tag
def current_time(token):
    # ...

有关 simple_tag 辅助函数,需要注意下面一些事情:

  • 传递给我们的函数的只有(单个)参数。

  • 在我们的函数被调用的时候,检查必需参数个数的工作已经完成了,所以我们不需要再做这个工作。

  • 参数两边的引号(如果有的话)已经被截掉了,所以我们会接收到一个普通字符串。

包含标签

另外一类常用的模板标签是通过渲染 其他 模板显示数据的。 比如说,Django的后台管理界面,它使用了自定义的模板标签来显示新增/编辑表单页面下部的按钮。 那些按钮看起来总是一样的,但是链接却随着所编辑的对象的不同而改变。 这就是一个使用小模板很好的例子,这些小模板就是当前对象的详细信息。

这些排序标签被称为 包含标签 。如何写包含标签最好通过举例来说明。 Let’s write a tag that produces a list of books for a given Author object. We’ll use the tag like this:

{% books_for_author author %}

结果将会像下面这样:

<ul>
    <li>The Cat In The Hat</li>
    <li>Hop On Pop</li>
    <li>Green Eggs And Ham</li>
</ul>

首先,我们定义一个函数,通过给定的参数生成一个字典形式的结果。 需要注意的是,我们只需要返回字典类型的结果就行了,它将被用做模板片断的context。 (译注:

def books_for_author(author):
    books = Book.objects.filter(authors__id=author.id)
    return {'books': books}

接下来,我们创建用于渲染标签输出的模板。 在我们的例子中,模板很简单:

<ul>
{% for book in books %}
    <li>{{ book.title }}</li>
{% endfor %}
</ul>

最后,我们通过对一个 Library 对象使用 inclusion_tag() 方法来创建并注册这个包含标签。

在我们的例子中,如果先前的模板在 polls/result_snippet.html 文件中,那么我们这样注册标签:

register.inclusion_tag('book_snippet.html')(books_for_author)

Python 2.4 decorator syntax works as well, so we could have written this, instead:

@register.inclusion_tag('book_snippet.html')
def books_for_author(author):
    # ...

有时候,你的包含标签需要访问父模板的context。 为了解决这个问题,Django提供了一个 takes_context 选项。 如果你在创建模板标签时,指明了这个选项,这个标签就不需要参数,并且下面的Python函数会带一个参数: 就是当这个标签被调用时的模板context。

例如,你正在写一个包含标签,该标签包含有指向主页的 home_linkhome_title 变量。 Python函数会像这样:

@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }

函数的第一个参数 必须context

模板 link.html 可能包含下面的东西:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

然后您想使用自定义标签时,就可以加载它的库,然后不带参数地调用它,就像这样:

{% jump_link %}

编写自定义模板加载器

Djangos 内置的模板加载器(在先前的模板加载内幕章节有叙述)通常会满足你的所有的模板加载需求,但是如果你有特殊的加载需求的话,编写自己的模板加载器也会相当简单。 比如:

一个模板加载器,也就是 TEMPLATE_LOADERS 中的每一项,都要能被下面这个接口所调用:

load_template_source(template_name, template_dirs=None)

参数 template_name 是所加载模板的名称 (和传递给 loader.get_template() 或者 loader.select_template() 一样), 而 template_dirs 是一个可选的包含除去 TEMPLATE_DIRS 之外的搜索目录列表。

如果加载器能够成功加载一个模板, 它应当返回一个元组: (template_source, template_path) 。在这里的 template_source 就是将被模板引擎编译的的模板字符串,而 template_path 是被加载的模板的路径。 由于那个路径可能会出于调试目显示给用户,因此它应当很快的指明模板从哪里加载而来。

如果加载器加载模板失败,那么就会触发 django.template.TemplateDoesNotExist 异常。

每个加载函数都应该有一个名为 is_usable 的函数属性。 这个属性是一个布尔值,用于告知模板引擎这个加载器是否在当前安装的Python中可用。 例如,如果 pkg_resources 模块没有安装的话,eggs加载器(它能够从python eggs中加载模板)就应该把 is_usable 设为 False ,因为必须通过 pkg_resources 才能从eggs中读取数据。

一个例子可以清晰地阐明一切。 这儿是一个模板加载函数,它可以从ZIP文件中加载模板。 它使用了自定义的设置 TEMPLATE_ZIP_FILES 来取代了 TEMPLATE_DIRS 用作查找路径,并且它假设在此路径上的每一个文件都是包含模板的ZIP文件:

from django.conf import settings
from django.template import TemplateDoesNotExist
import zipfile

def load_template_source(template_name, template_dirs=None):
    "Template loader that loads templates from a ZIP file."

    template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])

    # Try each ZIP file in TEMPLATE_ZIP_FILES.
    for fname in template_zipfiles:
        try:
            z = zipfile.ZipFile(fname)
            source = z.read(template_name)
        except (IOError, KeyError):
            continue
        z.close()
        # We found a template, so return the source.
        template_path = "%s:%s" % (fname, template_name)
        return (source, template_path)

    # If we reach here, the template couldn't be loaded
    raise TemplateDoesNotExist(template_name)

# This loader is always usable (since zipfile is included with Python)
load_template_source.is_usable = True

我们要想使用它,还差最后一步,就是把它加入到 TEMPLATE_LOADERS 。如果我们把这部分代码放到一个叫做 mysite.zip_loader 的包中,我们就需要把 mysite.zip_loader.load_template_source 加入到 TEMPLATE_LOADERS 中去。 If we put this code in a package called mysite.zip_loader , then we add mysite.zip_loader.load_template_source to TEMPLATE_LOADERS .

配置独立模式下的模板系统

Note

这部分只针对于对在其他应用中使用模版系统作为输出组件感兴趣的人。 如果你是在Django应用中使用模版系统,请略过此部分。

通常,Django会从它的默认配置文件和由 DJANGO_SETTINGS_MODULE 环境变量所指定的模块中加载它需要的所有配置信息。 但是当你想在非Django应用中使用模版系统的时候,采用环境变量并不是很好的方法。

为了解决这个问题,你需要使用附录E中所描述的手动配置选项。

你可能会考虑至少要设置 TEMPLATE_DIRS (如果你打算使用模板加载器), DEFAULT_CHARSET (尽管默认的 utf-8 编码相当好用),以及 TEMPLATE_DEBUG 。所有可用的选项都在附录E中详细描述,所有以 TEMPLATE_ 开头的选项都可能使你感兴趣的。

下一章

Continuing this section’s theme of advanced topics, the next chapter covers advanced usage of Django models.

| 上一章 | 目 录 | 下一章 |