扩展模板系统(自定义模版系统)
既然你已经对模板系统的内幕了解多了一些,让我们来看看如何使用自定义的代码来拓展这个系统吧。
绝大部分的模板定制是以自定义标签/过滤器的方式来完成的。尽管Django模板语言自带了许多内建标签和过滤器,但是你可能还是需要组建你自己的标签和过滤器库来满足你的需要。幸运的是,定义你自己的功能非常容易。
创建一个模板库
不管是写自定义标签还是过滤器,第一件要做的事是给 template library 创建使Django能够勾入的机制。
创建一个模板库分两步走:
第一,决定哪个Django应用应当拥有这个模板库。如果你通过 manage.pystartapp 创建了一个应用,你可以把它放在那里,或者你可以为模板库单独创建一个应用。
无论你采用何种方式,请确保把你的应用添加到 INSTALLED_APPS 中。我们稍后会解释这一点。
第二,在适当的Django应用包里创建一个 templatetags 目录。这个目录应当和 models.py 、 views.py 等处于同一层次。例如:
books/
__init__.py
models.py
templatetags/
views.py
在 templatetags 中创建两个空文件:一个 __init__.py (告诉Python这是一个包含了Python代码的包)和一个用来存放你自定义的标签/过滤器定义的py文件。第二个文件的名字稍后将用来加载标签。例如,如果你的自定义标签/过滤器在一个叫作 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.py 和 django/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>
1)首先,我们定义一个函数,通过给定的参数生成一个字典形式的结果。需要注意的是,我们只需要返回字典类型的结果就行了,它将被用做模板片断的context。 (译注:dict 的 key 作为变量名在模板中被使用)
def show_books_for_author(author):
books = author.book_set.all()
return {'books': books}
2)接下来,我们创建用于渲染标签输出的模板。在我们的例子中,模板很简单:
<ul>
{% for book in books %}
<li> {{ book }} </li>
{% endfor %}
</ul>
3)最后,我们通过对一个 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_link 和 home_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、还有你可以想到的其他数据格式。