扩展模板系统

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

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

创建一个模板库

不管是写自定义标签还是过滤器,第一件要做的事是创建模板库(Django能够导入的基本结构)。

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

第一,决定模板库应该放在哪个Django应用下。 如果你通过 manage.py startapp 创建了一个应用,你可以把它放在那里,或者你可以为模板库单独创建一个应用。 我们更推荐使用后者,因为你的filter可能在后来的工程中有用。无论你采用何种方式,请确保把你的应用添加到 INSTALLED_APPS 中。 我们稍后会解释这一点。第二,在适当的Django应用包里创建一个 templatetags 目录。 这个目录应当和 models.pyviews.py 等处于同一层次。 例如:
  1. books/
  2. __init__.py
  3. models.py
  4. templatetags/
  5. views.py
templatetags 中创建两个空文件: 一个 init.py (告诉Python这是 一个包含了Python代码的包)和一个用来存放你自定义的标签/过滤器定义的文件。 第二个文件的名字稍后将用来加载标签。 例如,如果你的自定义标签/过滤器在一个叫作 poll_extras.py 的文件中,你需要在模板中写入如下内容:
  1. {% load poll_extras %}
{% load %} 标签检查 INSTALLED_APPS 中的设置,仅允许加载已安装的Django应用程序中的模板库。 这是一个安全特性;它可以让你在一台电脑上部署很多的模板库的代码,而又不用把它们暴露给每一个Django安装。

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

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

作为合法的标签库,模块需要包含一个名为register的模块级变量。这个变量是template.Library的实例,是所有注册标签和过滤器的数据结构。 所以,请在你的模块的顶部插入如下语句:

  1. from django import template
  2. register = template.Library()

注意

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

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

自定义模板过滤器

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

  • (输入)变量的值

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

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

过滤器函数应该总有返回值。 而且不能触发异常,它们都应该静静地失败。 如果出现错误,应该返回一个原始输入或者空字符串,这会更有意义。

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

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

下面是一个可以用来去掉变量值空格的过滤器例子:

  1. {{ somevariable|cut:" " }}

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

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

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

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

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

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

  • 过滤器函数本身

如果你使用的是Python 2.4或者更新的版本,你可以使用装饰器register.filter()

  1. @register.filter(name='cut')
  2. def cut(value, arg):
  3. return value.replace(arg, '')
  4. @register.filter
  5. def lower(value):
  6. return value.lower()

如果你想第二个例子那样不使用 name 参数,那么Django会把函数名当作过滤器的名字。

下面是一个完整的模板库的例子,它包含一个 cut 过滤器:

  1. from django import template
  2. register = template.Library()
  3. @register.filter(name='cut')
  4. def cut(value, arg):
  5. return value.replace(arg, '')

自定义模板标签

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

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

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

  1. Hello, {{ person.name }}.
  2. {% ifequal name.birthday today %}
  3. Happy birthday!
  4. {% else %}
  5. Be sure to come back on your birthday
  6. for a splendid surprise message.
  7. {% endifequal %}

被编译的模板表现为节点列表的形式:

  • 文本节点: "Hello, "

  • 变量节点: person.name

  • 文本节点: ".\n\n"

  • IfEqual节点: name.birthdaytoday

当你调用一个已编译模板的 render() 方法时,模板就会用给定的context来调用每个在它的节点列表上的所有节点的 render() 方法。 这些渲染的结果合并起来,形成了模板的输出。 因此,要自定义模板标签,你需要指明原始模板标签如何转换成节点(编译函数)和节点的render()方法完成的功能 。

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

编写编译函数

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

例如,写一个显示当前日期的模板标签:{% current_time %}。该标签会根据参数指定的 strftime 格式(参见:http://www.djangoproject.com/r/python/strftime/)显示当前时间。首先确定标签的语法是个好主意。 在这个例子里,标签应该这样使用:

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

注意

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

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

  1. from django import template
  2. register = template.Library()
  3. def do_current_time(parser, token):
  4. try:
  5. # split_contents() knows not to split quoted strings.
  6. tag_name, format_string = token.split_contents()
  7. except ValueError:
  8. msg = '%r tag requires a single argument' % token.split_contents()[0]
  9. raise template.TemplateSyntaxError(msg)
  10. return CurrentTimeNode(format_string[1:-1])

这里需要说明的地方很多:

  • 每个标签编译函数有两个参数,parsertokenparser是模板解析器对象。 我们在这个例子中并不使用它。 token是正在被解析的语句。

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

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

  • 这个函数可以抛出 django.template.TemplateSyntaxError ,这个异常提供所有语法错误的有用信息。

  • 不要把标签名称硬编码在你的错误信息中,因为这样会把标签名称和你的函数耦合在一起。 token.splitcontents()[0]总是_记录标签的名字,就算标签没有任何参数。

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

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

编写模板节点

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

  1. import datetime
  2. class CurrentTimeNode(template.Node):
  3. def __init__(self, format_string):
  4. self.format_string = str(format_string)
  5. def render(self, context):
  6. now = datetime.datetime.now()
  7. return now.strftime(self.format_string)

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

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

注册标签

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

  1. register.tag('current_time', do_current_time)

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

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

  • 编译函数。

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

  1. @register.tag(name="current_time")
  2. def do_current_time(parser, token):
  3. # ...
  4. @register.tag
  5. def shout(parser, token):
  6. # ...

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

在上下文中设置变量

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

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

  1. class CurrentTimeNode2(template.Node):
  2. def __init__(self, format_string):
  3. self.format_string = str(format_string)
  4. def render(self, context):
  5. now = datetime.datetime.now()
  6. context['current_time'] = now.strftime(self.format_string)
  7. return ''

(我们把创建函数do_current_time2和注册给current_time2模板标签的工作留作读者练习。)

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

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

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

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

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

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

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

  1. import re
  2. class CurrentTimeNode3(template.Node):
  3. def __init__(self, format_string, var_name):
  4. self.format_string = str(format_string)
  5. self.var_name = var_name
  6. def render(self, context):
  7. now = datetime.datetime.now()
  8. context[self.var_name] = now.strftime(self.format_string)
  9. return ''
  10. def do_current_time(parser, token):
  11. # This version uses a regular expression to parse tag contents.
  12. try:
  13. # Splitting by None == splitting by spaces.
  14. tag_name, arg = token.contents.split(None, 1)
  15. except ValueError:
  16. msg = '%r tag requires arguments' % token.contents[0]
  17. raise template.TemplateSyntaxError(msg)
  18. m = re.search(r'(.*?) as (\w+)', arg)
  19. if m:
  20. fmt, var_name = m.groups()
  21. else:
  22. msg = '%r tag had invalid arguments' % tag_name
  23. raise template.TemplateSyntaxError(msg)
  24. if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
  25. msg = "%r tag's argument should be in quotes" % tag_name
  26. raise template.TemplateSyntaxError(msg)
  27. return CurrentTimeNode3(fmt[1:-1], var_name)

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

分析直至另一个模板标签

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

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

  1. def do_comment(parser, token):
  2. nodelist = parser.parse(('endcomment',))
  3. parser.delete_first_token()
  4. return CommentNode()
  5. class CommentNode(template.Node):
  6. def render(self, context):
  7. 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 %}之间的内容变成大写:

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

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

  1. def do_upper(parser, token):
  2. nodelist = parser.parse(('endupper',))
  3. parser.delete_first_token()
  4. return UpperNode(nodelist)
  5. class UpperNode(template.Node):
  6. def __init__(self, nodelist):
  7. self.nodelist = nodelist
  8. def render(self, context):
  9. output = self.nodelist.render(context)
  10. 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 函数于是可以写成这样:

  1. def current_time(format_string):
  2. try:
  3. return datetime.datetime.now().strftime(str(format_string))
  4. except UnicodeEncodeError:
  5. return ''
  6. register.simple_tag(current_time)

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

  1. @register.simple_tag
  2. def current_time(token):
  3. # ...

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

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

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

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

包含标签

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

这些排序标签被称为 包含标签 。如何写包含标签最好通过举例来说明。 让我们来写一个能够产生指定作者对象的书籍清单的标签。 我们将这样利用标签:

  1. {% books_for_author author %}

结果将会像下面这样:

  1. <ul>
  2. <li>The Cat In The Hat</li>
  3. <li>Hop On Pop</li>
  4. <li>Green Eggs And Ham</li>
  5. </ul>

首先,我们定义一个函数,通过给定的参数生成一个字典形式的结果。 需要注意的是,我们只需要返回字典类型的结果就行了,不需要返回更复杂的东西。 这将被用来作为模板片段的内容:

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

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

  1. <ul>
  2. {% for book in books %}
  3. <li>{{ book.title }}</li>
  4. {% endfor %}
  5. </ul>

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

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

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

Python 2.4装饰器语法也能正常工作,所以我们可以这样写:

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

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

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

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

(注意函数的第一个参数 必须context 。)

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

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

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

  1. {% jump_link %}