回到顶部

CONTENTS

django 1.8 自定义模板标签(simple_tag)和过滤器(filter)

为了解决应用中展示逻辑的需求,Django的模板语言提供了各式各样的内建标签以及过滤器。然而,你或许会发现模板内建的这些工具集合不一定能全部满足你的功能需要。在Python中,你可以通过自定义标签或过滤器的方式扩展模板引擎的功能,并使用{{ load }}标签在你的模板中进行调用。

代码布局

自定义模板标签和过滤器必须位于Django 的某个应用中。如果它们与某个已存在的应用相关,那么将其与应用绑在一起才有意义;否则,就应该创建一个新的应用来包含它。

这个应用应该包含一个templatetags 目录,和models.pyviews.py等文件处于同一级别目录下。如果目录不存在则创建它——不要忘记创建__init__.py 文件以使得该目录可以作为Python 的包。在添加这个模块以后,在模板里使用标签或过滤器之前你将需要重启服务器。

你的自定义的标签和过滤器将放在templatetags 目录下的一个模块里。这个模块的名字是你稍后将要载入标签时使用的,所以要谨慎的选择名字以防与其他应用下的自定义标签和过滤器名字冲突。

例如,你的自定义标签/过滤器在一个名为poll_extras.py的文件中,那么你的app目录结构看起来应该是这样的:

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

然后你可以在模板中像如下这样使用:

{% load poll_extras %}

为了让{{ load }} 标签工作,包含自定义标签的应用必须在INSTALLED_APPS中。这是一种安全功能︰它允许你在单个主机上Host 许多模板库的Python 代码,而不必让每个Django 都可以访问所有的模板库。

在 templatetags 包中放多少个模块没有限制。只需要记住{% load %} 声明将会载入给定模块名中的标签/过滤器,而不是应用的名称。

为了成为一个可用的标签库,这个模块必须包含一个名为 register的变量,它是template.Library 的一个实例,所有的标签和过滤器都是在其中注册的。所以把如下的内容放在你的模块的顶部:

from django import template

register = template.Library()

幕后

对于大量的示例,请阅读Django的默认过滤器和标记的源代码。它们分别位于django/template/defaultfilters.py 和django/template/defaulttags.py 中。

有关load 标签的更多信息,请阅读其文档。

编写自定义模板过滤器

自定义过滤器就是一个带有一个或两个参数的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()

注册自定义过滤器

django.template.Library.filter()

一旦你写好了你的自定义过滤器函数,你就开始需要把它注册为你的 Library实例,来让它在Django模板语言中可用:

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

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

  1. 过滤器的名称(一个字符串对象)
  2. 编译的函数 – 一个Python函数(不要把函数名写成字符串)

你还可以把register.filter()用作装饰器:

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

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

如果你像上面第二个例子一样没有声明 name 参数,Django将使用函数名作为过滤器的名字。

最后,register.filter() 还接收三个关键字参数,is_safeneeds_autoescape 和expects_localtime。这些参数将在下边过滤器和自动转义 以及过滤器和时区 章节中介绍。

期望字符串的过滤器

django.template.defaultfilters.stringfilter()

如果你正在编写一个只希望用一个字符串来作为第一个参数的模板过滤器,你应当使用stringfilter装饰器。这将在对象被传入你的函数之前把这个对象转换成它的字符串值:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()

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

用这种方式,你甚至可以给这个过滤器传递一个整数,并且不会出现AttributeError (因为整数没有 lower()方法).

过滤器和自动转义

编写一个自定义的过滤器时,请考虑一下过滤器如何与Django 的自定转义行为相互作用。请注意有三种类型的字符串可以传递给模板中的代码:

  • 原始字符串 即Python 原生的str 或unicode 类型。输出时,如果自动转义生效则进行转义,否则保持不变。

  • 安全字符串 是指在输出时已经被标记为安全而不用进一步转义的字符串。任何必要的转义已经完成。它们通常用于包含HTML 的输出,并希望在客户端解释为原始的形式。

    在内部,这些字符串是SafeBytes 或SafeText 类型。它们共享一个公共基类SafeData,所以你可以使用类似的代码测试他们︰

    if isinstance(value, SafeData):
        # Do something with the "safe" string.
        ...
    
  • 标记为“需要转义”的字符串 在输出时始终转义,无论它们是否在autoescape 块。然而,即使已经应用自动转义,这些字符也只会转义一次。

    在内部,这些字符串是EscapeBytes 或EscapeText 类型。通常你不需要担心这些;它们用于escape 过滤器的实现。

模板过滤代码最终是这两种中的一个:

  1. 你的过滤器没有引进任何HTML 不安全字符(<>'" 或&)到结果中。在这种情况下,你可以让Django 照顾你的所有的自动转义处理。你需要做的就是当你注册过滤器函数的时候将is_safe 标志设置为True,如下所示︰

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    这个标志告诉Django 如果"安全"的字符串传递到您的筛选器,结果仍将是"安全",如果一个非安全字符串传递,如果必要Django 会自动转义它。

    你可以认为这个的意思是"此过滤器是安全的 —— 它没有引入任何不安全的HTML 的可能性。

    is_safe 存在的必要原因是因为有很多正常的字符串操作会将一个SafeData 对象转换回正常的str 或unicode 对象而不是试图捕获它们,Django 在过滤器完成之后会修复这种破坏。

    例如,假设你有一个过滤器将字符串xx 添加到任何输入的末尾。因为这没有引入危险的HTML 字符(已经存在的除外)的结果,你应该使用is_safe标记你的过滤器:

    @register.filter(is_safe=True)
    def add_xx(value):
        return '%sxx' % value
    

    当这个过滤器用在模板中启用自动转义的地方时,如果输入没有标记为“安全”,Django 将对输出进行转义。

    默认情况下,is_safe 为False,你可以在不需要的任何过滤器中省略它。

    决定你的过滤器是否真的会保持安全字符串是安全的时要小心。如果你在删除字符,可能会无意中在结果留下不平衡的 HTML 标记或实体。例如,从输入删除> 可能将<a> 转变成<a,这将需要对输出进行转义,避免造成问题。同样,删除一个分号(;) 可以将&amp; 转变成&amp,不再是一个有效的实体,因此需要进一步的转义。大多数情况下不会这么棘手,但在审查你的代码时要留意任何类似的问题。

    标记过滤器位is_safe 将强制过滤器的返回值为字符串。如果你的过滤器应返回一个布尔值或其他非字符串值,则将其标记is_safe 会有意想不到的后果 (如将布尔值 False 转换为字符串 'False')。

  2. 或者,你的过滤器代码手动照顾任何必要的转义。这在你正引入新的HTML 标记到结果中时是必要的。你想标记输出为安全的而不用进一步的转义,所以你需要自己处理输入输出。

    django.utils.safestring.mark_safe() 标记输出为安全字符。

    但你要小心。你需要做的不仅仅只是标记作为安全输出。您需要确保它真的安全的,而你做什么取决于自动转义是否有效。这个想法的目的是编写的过滤器在无论模板自动转义是打开或关闭时都可以工作,这样模板作者使用起来更简单。

    为了使你的过滤器知道当前的自动转义状态,当你注册过滤器函数时需要设置needs_autoescape 标志为True。(如果不指定此标志,则默认为False)。此标志告诉Django 你的过滤器函数想要被传递一个额外的关键字参数,称为autoescape,如果启用自动转义则为True,否则为False。建议设置autoescape 参数的默认值设置为True,这样如果从Python 代码中调用该函数则会自动启用转义。

    例如,让我们编写一个强调字符串第一个字符的过滤器︰

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=True):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = '<strong>%s</strong>%s' % (esc(first), esc(other))
        return mark_safe(result)
    

    The needs_autoescape flag and the autoescape keyword argument mean that our function will know whether automatic escaping is in effect when the filter is called. We use autoescape to decide whether the input data needs to be passed through django.utils.html.conditional_escape or not. (In the latter case, we just use the identity function as the “escape” function.) The conditional_escape() function is like escape() except it only escapes input that is not a SafeData instance. If a SafeData instance is passed to conditional_escape(), the data is returned unchanged.

    Finally, in the above example, we remember to mark the result as safe so that our HTML is inserted directly into the template without further escaping.

    There’s no need to worry about the is_safe flag in this case (although including it wouldn’t hurt anything). Whenever you manually handle the auto-escaping issues and return a safe string, the is_safe flag won’t change anything either way.

警告

Avoiding XSS vulnerabilities when reusing built-in filters

Changed in Django 1.8.

Django的内置过滤器默认情况下设置autoescape=True,以便获得正确的自动转义行为并避免跨站点脚本漏洞。

在旧版本的Django中,重用Django的内置过滤器时要格外注意,因为旧版本中,autoescape默认设置成None。You’ll need to pass autoescape=True to get autoescaping.

例如,如果您想编写一个名为urlize_and_linebreaks的自定义过滤器,它结合了内置的urlizelinebreaksbr过滤器,过滤器将如下所示:

from django.template.defaultfilters import linebreaksbr, urlize

@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
    return linebreaksbr(
        urlize(text, autoescape=autoescape),
        autoescape=autoescape
    )

Then:

{{ comment|urlize_and_linebreaks }}

将等同于︰

{{ comment|urlize|linebreaksbr }}

过滤器和时区

如果你在编写一个操作datetime 对象的自定义过滤器,那么你注册这个自定义过滤器时通常需要将 expects_localtime 标志设置为True

@register.filter(expects_localtime=True)
def businesshours(value):
    try:
        return 9 <= value.hour < 17
    except AttributeError:
        return ''

当设置了此标志,如果你的过滤器的第一个参数是时区相关的日期时间值,那么在把它传递给你的过滤器之前,Django 会根据模板中的时区转换规则 将其转换为基于当前时区的日期时间值。

编写自定义的模板标签

标签比过滤器更复杂,因为标签可以做任何事情。Django 提供了大量的快捷方式,使得编写大多数类型的标签更为容易。首先我们要探讨这些快捷方式,然后再解释当快捷方式不够用时如何为这些情况从头开始编写标签。

简单的标签

django.template.Library.simple_tag()

许多模板标签接收多个参数 —— 字符串或模板变量 —— 并在基于输入的参数和一些其它外部信息进行一些处理后返回一个字符串。例如,current_time 标签可能接受一个格式字符串,并返回与之对应的格式化后的时间。

为了简化这些类型的标签的创建,Django 提供一个辅助函数simple_tag。这个函数是django.template.Library 的一个方法,接受一个任意数目的参数的函数,将其包装在一个render 函数和上面提到的其他必要位,并在模板系统中注册它。

我们的current_time 函数从而可以这样写︰

import datetime
from django import template

register = template.Library()

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

关于simple_tag 辅助函数几件值得注意的事项︰

  • 检查所需参数的数量等等,在我们的函数调用的时刻已经完成,所以我们不需要做了。
  • 参数(如果有)的引号都已经被截掉,所以我们收到的只是一个普通字符串。
  • 如果该参数是一个模板变量,传递给我们的函数是当前变量的值,不是变量本身。

如果你的模板标签需要访问当前上下文,你可以在注册标签时使用takes_context 参数︰

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

请注意,第一个参数必须称作context

takes_context 选项的工作方式的详细信息,请参阅包含标签

如果你需要重命名你的标签,你可以给它提供自定义的名称︰

register.simple_tag(lambda x: x - 1, name='minusone')

@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2

simple_tag 函数可以接受任意数量的位置参数和关键字参数。例如:

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

然后在模板中,可以将任意数量的由空格分隔的参数传递给模板标签。像在Python 中一样,关键字参数的值的设置使用等号("=") ,并且必须在位置参数之后提供。例如:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

Inclusion 标签

django.template.Library.inclusion_tag()

另一种常见类型的模板标签是通过渲染另外一个模板来显示一些数据。例如,Django 的Admin 界面使用自定义模板标签显示"添加/更改"表单页面底部的按钮。这些按钮看起来总是相同,但链接的目标根据正在编辑的对象而变化 —— 所以它们是使用小模板展示当前对象详细信息很好的例子。(在Admin 界面这种情况下,它是submit_row 标记)。

这些类型的标签被称为"Inclusion 标签"。

示例最能体现如何编写Inclusion 标签。让我们编写一个根据给定的教程中创建的Poll 对象输出一个选项列表的标签。标签的用法像这样︰

{% show_results poll %}

... 输出将像这样:

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

首先,定义接收这个参数并产生数据字典作为结果的函数。这里重要的一点是,我们只需要返回一个字典,不需要任何复杂的东西。它将用做模板片段的模板上下文。例如:

def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

接下来,创建用于渲染标签输出的模板。这个模板是标签固定的功能︰标签的编写者指定它,不是模板设计者。在我们的例子中,模板非常简单︰

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

现在,通过调用Library 对象的inclusion_tag() 方法创建并注册Inclusion 标签。在我们的示例中,如果上面的模板叫做results.html 文件,并位于模板加载程序搜索的目录,我们将这样注册标签︰

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag('results.html')
def show_results(poll):
    ...

或者可以使用django.template.Template 实例注册Inclusion标签︰

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

......当首次创建该函数时。

有时,你的Inclusion 标签可能要求一大堆参数,这让模板作者非常痛苦,因为不仅要传递这些参数还要记住它们的顺序。为了解决这个问题, Django 提供了一个takes_context 选项给Inclusion 标签。如果你在创建模板标签时指定takes_context,这个标签将不需要必选参数,当标签被调用的时候底层的Python 函数将有一个参数 —— 模板上下文。

比如说,当你想要写一个 inclusion tag总是应用在上下文中,包含 home_link 和 home_title 这两个用来返回主页的变量。 如下所示:

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

注意函数的第一个参数必须叫做context

register.inclusion_tag()这一行,我们指定了takes_context=True 和模板的名字。这里是模板link.html 看起来的样子:

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

然后,当任何时候你想调用这个自定义的标签, load 它的 library 然后不需要任何参数就是调用它,就像这样:

{% jump_link %}

注意当你使用takes_context=True,就不需要传递参数给这个模板标签。它会自己去获取上下文。

takes_context 参数默认为False。当它设置为True 时,会传递上下文对象给这个标签,如本示例所示。这是这个示例和前面的inclusion_tag 示例的唯一区别。

inclusion_tag 函数可以接受任意数量的位置参数和关键字参数。例如:

@register.inclusion_tag('my_template.html')
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

然后在模板中,可以将任意数量的由空格分隔的参数传递给模板标签。像在Python 中一样,关键字参数的值的设置使用等号("=") ,并且必须在位置参数之后提供。例子:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

赋值标签

django.template.Library.assignment_tag()

为了简单化设置上下文中变量的标签的创建,Django 提供一个辅助函数assignment_tag。这个函数方式的工作方式与simple_tag 相同,不同之处在于它将标签的结果存储在指定的上下文变量中而不是直接将其输出。

我们之前的current_time 函数从而可以这样写︰

@register.assignment_tag
def get_current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

然后你可以使用as 参数后面跟随变量的名称将结果存储在模板变量中,并将它输出到你觉得合适的地方︰

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

如果你的模板标签需要访问当前上下文,你可以在注册标签时使用takes_context 参数:

@register.assignment_tag(takes_context=True)
def get_current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

注意函数的第一个参数必须叫做context

takes_context 选项的工作方式的详细信息,请参阅包含标签

assignment_tag 函数可以接受任意数量的位置参数和关键字参数。例如:

@register.assignment_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

然后在模板中,可以将任意数量的由空格分隔的参数传递给模板标签。像在Python 中一样,关键字参数的值的设置使用等号("=") ,并且必须在位置参数之后提供。例子:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile as the_result %}

自定义模板标签进阶

有时创建自定义模板标签的基本功能是不够的。别担心,Django 给你建立模板标签所需的从底层访问完整的内部。

概述

模板系统的运行分为两步︰编译和渲染。若要定义一个自定义的模板标签,你指定编译如何工作以及渲染如何工作。

当Django 编译一个模板时,它将原始模板文本拆分成节点。每个节点是django.template.Node 的一个实例,并且有一个render() 方法。编译后的模板就是一个简单Node 对象的列表。当你在编译后的模板对象上调用render() 时,该模板将结合给定的上下文调用每个Node 的render()。结果所有串联在一起形成该模板的输出。

因此,若要定义一个自定义的模板标签,你需要指定原始模板标签如何被转换成一个Node(节点) (编译函数),以及该节点的render() 方法会进行的渲染动作

写编译函数

解析器处理每个模板标签时,会调用标签上下文对应的函数和对象本身。这个函数会会返回一个Node实例

For example, let’s write a full implementation of our simple template tag, }} current_time }}, that displays the current date/time, formatted according to a parameter given in the tag, instrftime() syntax. It’s a good idea to decide the tag syntax before anything else. In our case, let’s say the tag should be used like this:

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

此函数的解析器应抓取参数并创建节点对象:

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:
        raise template.TemplateSyntaxError(
            "%r tag requires a single argument" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode(format_string[1:-1])

笔记:

  • parser是模板解析器对象。在这个例子中我们不需要它。
  • token.contents是标记的原始内容的字符串。在我们的示例中,它是'current_time “%Y-%m-%d %I:%M %p “'”。
  • token.split_contents()方法将空格上的参数分隔开,同时将带引号的字符串保存在一起。更直接的token.contents.split()将不会那么健壮,因为它会在所有空间(包括引用字符串中的那些)上天真地分割。始终使用token.split_contents()是一个好主意。
  • 此函数负责提高django.template.TemplateSyntaxError,包含有用的消息,任何语法错误。
  • The TemplateSyntaxError exceptions use the tag_name variable. Don’t hard-code the tag’s name in your error messages, because that couples the tag’s name to your function. token.contents.split()[0] will ‘’always’’ be the name of your tag – even when the tag has no arguments.
  • The function returns a CurrentTimeNode with everything the node needs to know about this tag. In this case, it just passes the argument – "%Y-%m-%d %I:%M %p". The leading and trailing quotes from the template tag are removed in format_string[1:-1].
  • The parsing is very low-level. The Django developers have experimented with writing small frameworks on top of this parsing system, using techniques such as EBNF grammars, but those experiments made the template engine too slow. It’s low-level because that’s fastest.

Writing the renderer

The second step in writing custom tags is to define a Node subclass that has a render() method.

Continuing the above example, we need to define CurrentTimeNode:

import datetime
from django import template

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

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

Notes:

  • __init__() gets the format_string from do_current_time(). Always pass any options/parameters/arguments to a Node via its __init__().
  • The render() method is where the work actually happens.
  • render() should generally fail silently, particularly in a production environment. In some cases however, particularly if context.template.engine.debug is True, this method may raise an exception to make debugging easier. For example, several core tags raise django.template.TemplateSyntaxError if they receive the wrong number or type of arguments.

Ultimately, this decoupling of compilation and rendering results in an efficient template system, because a template can render multiple contexts without having to be parsed multiple times.

Auto-escaping considerations

The output from template tags is not automatically run through the auto-escaping filters. However, there are still a couple of things you should keep in mind when writing a template tag.

If the render() function of your template stores the result in a context variable (rather than returning the result in a string), it should take care to call mark_safe() if appropriate. When the variable is ultimately rendered, it will be affected by the auto-escape setting in effect at the time, so content that should be safe from further escaping needs to be marked as such.

Also, if your template tag creates a new context for performing some sub-rendering, set the auto-escape attribute to the current context’s value. The __init__ method for the Context class takes a parameter called autoescape that you can use for this purpose. For example:

from django.template import Context

def render(self, context):
    # ...
    new_context = Context({'var': obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

This is not a very common situation, but it’s useful if you’re rendering a template yourself. For example:

def render(self, context):
    t = context.template.engine.get_template('small_fragment.html')
    return t.render(Context({'var': obj}, autoescape=context.autoescape))

Changed in Django 1.8:

The template attribute of Context objects was added in Django 1.8. context.template.engine.get_template must be used instead of django.template.loader.get_template() because the latter now returns a wrapper whose render method doesn’t accept a Context.

If we had neglected to pass in the current context.autoescape value to our new Context in this example, the results would have always been automatically escaped, which may not be the desired behavior if the template tag is used inside a }} autoescape off }} block.

Thread-safety considerations

Once a node is parsed, its render method may be called any number of times. Since Django is sometimes run in multi-threaded environments, a single node may be simultaneously rendering with different contexts in response to two separate requests. Therefore, it’s important to make sure your template tags are thread safe.

To make sure your template tags are thread safe, you should never store state information on the node itself. For example, Django provides a builtin cycle template tag that cycles among a list of given strings each time it’s rendered:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

A naive implementation of CycleNode might look something like this:

import itertools
from django import template

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)

    def render(self, context):
        return next(self.cycle_iter)

But, suppose we have two templates rendering the template snippet from above at the same time:

  1. Thread 1 performs its first loop iteration, CycleNode.render() returns ‘row1’
  2. Thread 2 performs its first loop iteration, CycleNode.render() returns ‘row2’
  3. Thread 1 performs its second loop iteration, CycleNode.render() returns ‘row1’
  4. Thread 2 performs its second loop iteration, CycleNode.render() returns ‘row2’

The CycleNode is iterating, but it’s iterating globally. As far as Thread 1 and Thread 2 are concerned, it’s always returning the same value. This is obviously not what we want!

To address this problem, Django provides a render_context that’s associated with the context of the template that is currently being rendered. The render_context behaves like a Python dictionary, and should be used to store Node state between invocations of the render method.

Let’s refactor our CycleNode implementation to use the render_context:

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cyclevars = cyclevars

    def render(self, context):
        if self not in context.render_context:
            context.render_context[self] = itertools.cycle(self.cyclevars)
        cycle_iter = context.render_context[self]
        return next(cycle_iter)

Note that it’s perfectly safe to store global information that will not change throughout the life of the Node as an attribute. In the case of CycleNode, the cyclevars argument doesn’t change after the Node is instantiated, so we don’t need to put it in the render_context. But state information that is specific to the template that is currently being rendered, like the current iteration of the CycleNode, should be stored in the render_context.

Note

Notice how we used self to scope the CycleNode specific information within the render_context. There may be multiple CycleNodes in a given template, so we need to be careful not to clobber another node’s state information. The easiest way to do this is to always use self as the key into render_context. If you’re keeping track of several state variables, make render_context[self] a dictionary.

Registering the tag

Finally, register the tag with your module’s Library instance, as explained in writing custom template filters above. Example:

register.tag('current_time', do_current_time)

The tag() method takes two arguments:

  1. The name of the template tag – a string. If this is left out, the name of the compilation function will be used.
  2. The compilation function – a Python function (not the name of the function as a string).

As with filter registration, it is also possible to use this as a decorator:

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

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

If you leave off the name argument, as in the second example above, Django will use the function’s name as the tag name.

Passing template variables to the tag

Although you can pass any number of arguments to a template tag using token.split_contents(), the arguments are all unpacked as string literals. A little more work is required in order to pass dynamic content (a template variable) to a template tag as an argument.

While the previous examples have formatted the current time into a string and returned the string, suppose you wanted to pass in a DateTimeField from an object and have the template tag format that date-time:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

Initially, token.split_contents() will return three values:

  1. The tag name format_time.
  2. The string 'blog_entry.date_updated' (without the surrounding quotes).
  3. The formatting string '"%Y-%m-%d %I:%M %p"'. The return value from split_contents() will include the leading and trailing quotes for string literals like this.

Now your tag should begin to look like this:

from django import template

def do_format_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, date_to_be_formatted, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires exactly two arguments" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return FormatTimeNode(date_to_be_formatted, format_string[1:-1])

You also have to change the renderer to retrieve the actual contents of the date_updated property of the blog_entry object. This can be accomplished by using the Variable() class in django.template.

To use the Variable class, simply instantiate it with the name of the variable to be resolved, and then call variable.resolve(context). So, for example:

class FormatTimeNode(template.Node):
    def __init__(self, date_to_be_formatted, format_string):
        self.date_to_be_formatted = template.Variable(date_to_be_formatted)
        self.format_string = format_string

    def render(self, context):
        try:
            actual_date = self.date_to_be_formatted.resolve(context)
            return actual_date.strftime(self.format_string)
        except template.VariableDoesNotExist:
            return ''

Variable resolution will throw a VariableDoesNotExist exception if it cannot resolve the string passed to it in the current context of the page.

Setting a variable in the context

The above examples simply output a value. Generally, it’s more flexible if your template tags set template variables instead of outputting values. That way, template authors can reuse the values that your template tags create.

To set a variable in the context, just use dictionary assignment on the context object in the render() method. Here’s an updated version of CurrentTimeNode that sets a template variable current_time instead of outputting it:

import datetime
from django import template

class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string
    def render(self, context):
        context['current_time'] = datetime.datetime.now().strftime(self.format_string)
        return ''

Note that render() returns the empty string. render() should always return string output. If all the template tag does is set a variable, render() should return the empty string.

Here’s how you’d use this new version of the tag:

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

Variable scope in context

Any variable set in the context will only be available in the same block of the template in which it was assigned. This behavior is intentional; it provides a scope for variables so that they don’t conflict with context in other blocks.

But, there’s a problem with CurrentTimeNode2: The variable name current_time is hard-coded. This means you’ll need to make sure your template doesn’t use {{ current_time }} anywhere else, because the }} current_time }} will blindly overwrite that variable’s value. A cleaner solution is to make the template tag specify the name of the output variable, like so:

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

To do that, you’ll need to refactor both the compilation function and Node class, like so:

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):
        context[self.var_name] = datetime.datetime.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:
        raise template.TemplateSyntaxError(
            "%r tag requires arguments" % token.contents.split()[0]
        )
    m = re.search(r'(.*?) as (\w+)', arg)
    if not m:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
    format_string, var_name = m.groups()
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode3(format_string[1:-1], var_name)

The difference here is that do_current_time() grabs the format string and the variable name, passing both to CurrentTimeNode3.

Finally, if you only need to have a simple syntax for your custom context-updating template tag, you might want to consider using the assignment tag shortcut we introduced above.

Parsing until another block tag

Template tags can work in tandem. For instance, the standard }} comment }} tag hides everything until }} endcomment }}. To create a template tag such as this, use parser.parse() in your compilation function.

Here’s how a simplified }} comment }} tag might be implemented:

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

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

Note

The actual implementation of }} comment }} is slightly different in that it allows broken template tags to appear between }} comment }} and }} endcomment }}. It does so by callingparser.skip_past('endcomment') instead of parser.parse(('endcomment',)) followed by parser.delete_first_token(), thus avoiding the generation of a node list.

parser.parse() takes a tuple of names of block tags ‘’to parse until’‘. 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.

In "nodelist = parser.parse(('endcomment',))" in the above example, nodelist is a list of all nodes between the }} comment }} and }} endcomment }}, not counting }} comment }} and }}endcomment }} themselves.

After parser.parse() is called, the parser hasn’t yet “consumed” the }} endcomment }} tag, so the code needs to explicitly call parser.delete_first_token().

CommentNode.render() simply returns an empty string. Anything between }} comment }} and }} endcomment }} is ignored.

Parsing until another block tag, and saving contents

In the previous example, do_comment() discarded everything between }} comment }} and }} endcomment }}. Instead of doing that, it’s possible to do something with the code between block tags.

For example, here’s a custom template tag, }} upper }}, that capitalizes everything between itself and }} endupper }}.

Usage:

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

和前面的例子一样,我们使用parser.parse()。但是这次,我们将得到的节点传递给节点

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)

For more examples of complex rendering, see the source code of }} for }} in django/template/defaulttags.py and }} if }} in django/template/smartif.py.

^_^
请喝咖啡 ×

前一篇: 博客链接提交给百度和谷歌
下一篇: Fiddler 更换 User-Agent
captcha
带 * 是必填项