The Django Book

第十章: 深入模板引擎

虽然大多数Django模板语言之间的作用和模板作者需要的作用一样,但您仍可以定制并扩展模板引擎,让它做一些原本不能做到的事,或用同样的方式更容易地完成您的工作。

本章深入钻研Django的模板系统。如果你想扩展模板系统或者只是对它的工作原理感觉到好奇,本章涉及了你需要了解的东西.

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

模板语言回顾

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

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

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

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

{% 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)

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

def view_4(request):
    # ...
    t = loader.get_template('template4.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the fourth 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)

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

def view_4(request):
    # ...
    t = loader.get_template('template4.html')
    c = RequestContext(request, {'message': 'I am the fourth 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 函数提供了。

O5akXl <a href=”http://xrdylvofgyks.com/“>xrdylvofgyks</a>, [url=http://jpknyzrymcvm.com/]jpknyzrymcvm[/url], [link=http://kanomsrvtkmd.com/]kanomsrvtkmd[/link], http://avajxtraqqjg.com/

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

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]))

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

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

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

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

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

ilR94N <a href=”http://bxcyktdovtcx.com/“>bxcyktdovtcx</a>, [url=http://svkmchcyqtpi.com/]svkmchcyqtpi[/url], [link=http://orlgauoznlpg.com/]orlgauoznlpg[/link], http://hcmltazhxlgv.com/

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中的项的字典。请注意 TEMPLATE_CONTEXT_PROCESSORS 中的值是以 strings 的形式给出的,这意味着这些处理器必须在你的python路径中的某处(这样你才能在设置中引用它们)

RkbRhS <a href=”http://ssvglzgqktrs.com/“>ssvglzgqktrs</a>, [url=http://tzwexwjpftqx.com/]tzwexwjpftqx[/url], [link=http://siituwbkjtet.com/]siituwbkjtet[/link], http://nyplxetpyqkq.com/

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

django.core.context_processors.auth

cZgBaF <a href=”http://wqijjqhtuafe.com/“>wqijjqhtuafe</a>, [url=http://ymyewzllgpwg.com/]ymyewzllgpwg[/url], [link=http://ynrosouewqcv.com/]ynrosouewqcv[/link], http://taptrifhfbme.com/

  • 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

ae2cpK <a href=”http://zekedeavaywg.com/“>zekedeavaywg</a>, [url=http://kqzsnzwpeoko.com/]kqzsnzwpeoko[/url], [link=http://ncoswfhxxpgm.com/]ncoswfhxxpgm[/link], http://vrswcfxjqbnl.com/

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

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

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

VB156E <a href=”http://xsstdaarnodo.com/“>xsstdaarnodo</a>, [url=http://sjvwzwjuntje.com/]sjvwzwjuntje[/url], [link=http://rllxfurtgmdb.com/]rllxfurtgmdb[/link], http://liaaqrivlsgl.com/

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

django.core.context_processors.i18n

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

  • LANGUAGESLANGUAGES 选项的值。

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

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

django.core.context_processors.request

zEZko4 <a href=”http://davktzvegpga.com/“>davktzvegpga</a>, [url=http://dvvitkvkrqhj.com/]dvvitkvkrqhj[/url], [link=http://zmxecatgkrei.com/]zmxecatgkrei[/link], http://qxqjkagkmvbm.com/

写Context处理器的一些建议

编写处理器的一些建议:

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

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

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

模板加载的内幕

一般说来,你会把模板以文件的方式存储在文件系统中,但是你也可以使用自定义的 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 异常。

正如在第四章中所提到的,默认情况下这些函数使用 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

zS7gE0 <a href=”http://xihwijxziqzw.com/“>xihwijxziqzw</a>, [url=http://latxmxlseqcl.com/]latxmxlseqcl[/url], [link=http://fwlitcrfihxf.com/]fwlitcrfihxf[/link], http://yxtsdtenuwcv.com/

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

这个加载器默认启用。

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

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

扩展模板系统

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

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

创建一个模板库

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

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

第一,决定哪个Django应用应当拥有这个模板库。如果你通过 manage.py startapp 创建了一个应用,你可以把它放在那里,或者你可以为模板库单独创建一个应用。

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

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

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

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

{% load poll_extras %}

{% load %} 标签检查 INSTALLED_APPS 中的设置,仅允许加载已安装的Django应用程序中的模板库。这是一个安全特性。它可以让你在一台电脑上部署很多的模板库的代码,而又不用把它们暴露给每一个Django安装。

如果你写了一个不和任何模型/视图关联的模板库,那么得到一个仅包含 templatetags 包的Django应用程序包是完全正常的。对于在 templatetags 包中放置多少个模块没有做任何的限制。需要了解的是: {% load %} 语句会为指定的Python模块名(而非应用程序名)加载标签或过滤器。

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

要成为有效的标签库,模块必须包含一个模块级的变量: register ,这是一个 template.Library 的实例。这个 template.Library 实例是包含所有已注册的标签及过滤器的数据结构。因此,在模块的顶部位置插入下述代码:

from django import template

register = template.Library()

备注

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

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

自定义模板过滤器

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

  • (输入)变量的值

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

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

过滤器函数应该总有返回值,而且不能触发异常,它们都应该静静的失败。如果有一个错误发生,它们要么返回原始的输入字符串,要么返回空的字符串,无论哪个都可以。

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

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

这里是一些如何使用过滤器的例子:

{{ somevariable|cut:"0" }}

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

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 对象的一个列表。

当你调用一个已编译模板的 render() 方法时,模板就会用给定的context来调用每个在它的节点列表上的节点的 render() 方法。所以,为了定义一个自定义的模板标签,你需要明确这个模板标签转换为一个 Node (已编译的函数)和这个node的 render() 方法。

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

编写编译函数

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

例如,写一个显示当前日期的模板标签:{% current_time %},该标签会根据参数指定的 strftime 格式(参见: http://www.djangoproject.com/r/python/strftime/ )显示当前时间。在继续做其它事情以前,先决定标签的语法是一个好主意。在我们的例子里,该标签将会像这样被使用:

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

备注

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

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

from django import template

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.contents[0]
        raise template.TemplateSyntaxError(msg)
    return CurrentTimeNode(format_string[1:-1])

其实这儿包含了不少东西:

  • parser 是模板分析器对象,在这个例子中我们没有使用它。

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

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

  • 这个函数负责抛出 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 = format_string

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

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

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

注册标签

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

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 = format_string

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

注意 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 = 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 对象的列表,这些对象代表了分析器在遇到元组中任一标签名之 的内容.

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

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

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

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

在前一个例子中, do_comment() 抛弃了在 {% comment %}{% endcomment %} 之间的所有内容。同样,也可以对块标签之间的代码进行处理。

例如,这个自定义模板标签: {% upper %} ,它把自己和 {% endupper %} 之间的所有内容都变成大写:

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

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

@register.tag
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 标签就是这样一个例子. 我们给它格式字符串, 然后它把时间作为字符串返回.

为了简化这类标签,Django 提供了一个帮助函数: simple_tag 。这个函数是 django.template.Library 的一个方法,它接受一个只有一个参数的函数作参数,把它包装在 render 函数和之前提及过的其他的必要单位中,然后通过模板系统注册标签。

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

def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

register.simple_tag(current_time)

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

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

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

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

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

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

包含标签

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

这些排序标签被称为 包含标签 。如何写包含标签最好通过举例来说明。我们来写一个可以生成一个选项列表的多选项对象 Poll 。标签这样使用:

{% show_results poll %}

结果将会像下面这样:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

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

def show_books_for_author(author):
    books = author.book_set.all()
    return {'books': books}

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

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

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

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

register.inclusion_tag('books/books_for_author.html')(show_books_for_author)

和往常一样,我们也可以使用Python 2.4中的修饰语法,所以我们还可以这么写:

@register.inclusion_tag('books/books_for_author.html')
def show_books_for_author(show_books_for_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 内置的模板加载器(在先前的模板加载内幕章节有叙述)通常会满足你的所有的模板加载需求,但是如果你有特殊的加载需求的话,编写自己的模板加载器也会相当简单。比如:你可以从数据库加载模板,或者使用 Subversions的Python实现直接从Subversion库加载模板,再或者(稍后展示)从zip文件加载模板。

一个模板加载器,也就是 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文件:

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

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 中去。

使用内置的模板参考

Django管理界面包含一个完整的参考资料,里面有所有的可以在特定网站上使用的模板标签和过滤器。它设计的初衷是Django程序员提供给模板开发人员的一个工具。你可以点击管理页面右上角的文档链接来查看这些资料。

参考说明分为4个部分:标签、过滤器、模型和视图。 标签过滤器 部分描述了所有内置的标签(实际上,第4章中用到的标签和过滤器都直接来源于那几页)以及一些可用的自定义标签和过滤器库。

视图 页面是最有价值的。网站中的每个URL都在这儿有独立的入口。如果相关的视图包含一个 文档字符串, 点击URL,你就会看到:

  • 生成本视图的视图函数的名字

  • 视图功能的一个简短描述

  • 上下文或一个视图模板中可用的变量的列表

  • 视图使用的模板的名字

要想查看关于视图文档的更详细的例子,请阅读Django的通用 object_list 视图部分的源代码,它位于 django/views/generic/list_detail.py 文件中。

通常情况下,由Django构建的网站都会使用数据库对象, 模型 页面描述了系统中所有类型的对象,以及该对象对应的所有可用字段。

总之,这些文档告诉你在模板中的所有可用的标签、过滤器、变量和对象。

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

备注

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

通常,Django会从它的默认配置文件和由 DJANGO_SETTINGS_MODULE 环境变量所指定的模块中加载它需要的所有配置信息。但是当你想在非Django应用中使用模版系统的时候,采用环境变量并不是很好的方法。比起为模版系统单独采用配置文件并用环境变量来指向它,你可能更希望能够在你的应用中采用一致的配置方法来配置模版系统和其他部分

为了解决这个问题,你需要使用附录E中所描述的手动配置选项。简单来说,你需要引入合适的模板系统,并且在调用任何模板函数 之前 调用 django.conf.settings.configure() 来指定任何你想要的设置。

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

接下来?

迄今为止,本书假定您想展示的内容为HTML。对于一个有关Web开发的书来说,这不是一个 不好的假设,但有时你想用Django输出其他数据格式。

下一章将讲解如何使用Django生成图像、PDF、还有你可以想到的其他数据格式。

Copyright 2006 Adrian Holovaty and Jacob Kaplan-Moss.
This work is licensed under the GNU Free Document License.
Hosting graciously provided by media temple
Chinese translate hosting by py3k.cn. 粤ICP备16122281号-1