Skip to main content

自定义模板代码和过滤器

Django的模板语言配有各种各样的 内置标签和过滤器,旨在解决应用程序的表示逻辑需求。但是,您可能会发现自己需要的功能不包含在核心模板基元集中。您可以通过使用Python定义自定义标记和过滤器来扩展模板引擎,然后使用 {% load %} 标记将它们提供给您的模板。

代码布局

指定自定义模板标记和过滤器的最常见位置是在Django应用程序中。如果他们涉及到一个现有的应用程序,它是有意义的捆绑在那里;否则,他们可以添加到一个新的应用程序。当将一个Django应用程序添加到 INSTALLED_APPS 时,它在下面描述的常规位置中定义的任何标记都会自动在模板中加载。

应用程序应该包含一个 templatetags 目录,与 models.pyviews.py 等级相同。如果这不存在,创建它 - 不要忘记 __init__.py 文件,以确保该目录被视为一个Python包。

开发服务器不会自动重启

添加 templatetags 模块后,您需要重新启动服务器,然后才能在模板中使用标记或过滤器。

您的自定义标签和过滤器将位于 templatetags 目录中的模块中。模块文件的名称是稍后用于加载代码的名称,因此请谨慎选择一个不会与其他应用中的自定义代码和过滤器冲突的名称。

例如,如果您的自定义代码/过滤器位于名为 poll_extras.py 的文件中,则您的应用布局可能如下所示:

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

在您的模板中,您将使用以下内容:

{% load poll_extras %}

包含自定义标记的应用程序必须位于 INSTALLED_APPS 中,以便 {% load %} 标记可以正常工作。这是一个安全功能:它允许您在单个主机上为许多模板库托管Python代码,而无需为每个Django安装访问所有模板库。

您在 templatetags 包中放置的模块数量没有限制。请记住,{% load %} 语句将为给定的Python模块名称加载标签/过滤器,而不是应用程序的名称。

要成为有效的标记库,模块必须包含一个名为 register 的模块级变量,它是一个 template.Library 实例,其中注册了所有标记和过滤器。因此,在模块顶部附近,放置以下内容:

from django import template

register = template.Library()
New in Django 1.9.

或者,模板标签模块可以通过 'libraries' 参数注册到 DjangoTemplates。如果要在加载模板标记时使用模板标记模块名称中的不同标签,这将非常有用。它还使您能够注册标签,而无需安装应用程序。

幕后

对于大量的示例,请阅读Django的默认过滤器和标记的源代码。他们分别在 django/template/defaultfilters.pydjango/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_autoescapeexpects_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 strunicode 类型。在输出中,如果自动转义有效并且呈现不变,则它们被转义,否则。

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

    在内部,这些字符串是 SafeBytesSafeText 类型。它们共享 SafeData 的公共基类,因此您可以使用代码来测试它们:

    if isinstance(value, SafeData):
        # Do something with the "safe" string.
        ...
    

模板过滤器代码属于以下两种情况之一:

  1. 您的过滤器不会在结果中引入任何不存在的HTML不安全字符(<>'"&)。在这种情况下,您可以让Django为您处理所有自动转义处理。所有你需要做的是,当你注册你的过滤器功能,像这样设置 is_safe 标志为 True:

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

    这个标志告诉Django,如果一个“安全”的字符串被传递到你的过滤器,结果仍然是“安全的”,如果传递一个不安全的字符串,Django将自动转义它,如果必要。

    你可以认为这意味着“这个过滤器是安全的 - 它不会引入任何不安全的HTML的可能性。

    is_safe 是必要的原因是因为有很多正常的字符串操作,这将使一个 SafeData 对象回到一个正常的 strunicode 对象,而不是试图抓住所有,这将是非常困难的,Django修复损坏后过滤器已完成。

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

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

    当在启用自动转义的模板中使用此过滤器时,每当输入尚未标记为“安全”时,Django将转义输出。

    默认情况下,is_safeFalse,您可以从任何不需要的过滤器中忽略它。

    在确定您的过滤器是否确实将安全字符串安全时,请小心。如果您是 removing 字符,则可能会在结果中无意间留下不平衡的HTML标记或实体。例如,从输入中移除 > 可能会将 <a> 转换为 <a,这需要在输出上转义以避免导致问题。类似地,删除分号(;)可以将 &amp; 转换为 &amp&amp 不再是有效实体,因此需要进一步转义。大多数情况下不会有这个棘手的问题,但在检查代码时,请留意任何问题。

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

  2. 或者,您的过滤器代码可以手动处理任何必要的转义。当您在结果中引入新的HTML标记时,这是必要的。您希望将输出标记为避免进一步转义,以便您的HTML标记不会进一步转义,因此您需要自己处理输入。

    要将输出标记为安全字符串,请使用 django.utils.safestring.mark_safe()

    但要小心。你需要做的不仅仅是将输出标记为安全。你需要确保它真的 is 安全,你做什么取决于是否自动转义是有效的。这个想法是编写可以在模板中操作的过滤器,其中自动转义是打开还是关闭,以使模板作者更容易。

    为了使您的过滤器知道当前的自动转义状态,在注册过滤器功能时将 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)
    

    needs_autoescape 标志和 autoescape 关键字参数意味着我们的函数将知道当调用过滤器时自动转义是否有效。我们使用 autoescape 来决定输入数据是否需要通过 django.utils.html.conditional_escape 传递。 (在后一种情况下,我们只使用身份函数作为“转义”函数。) conditional_escape() 函数类似于 escape(),除了它只转义输入是 a SafeData 实例。如果 SafeData 实例被传递给 conditional_escape(),则数据不被改变地返回。

    最后,在上面的例子中,我们记得将结果标记为安全的,这样我们的HTML直接插入到模板中,而不需要进一步转义。

    在这种情况下没有必要担心 is_safe 标志(虽然包括它不会伤害任何东西)。每当你手动处理自动转义问题并返回一个安全的字符串,is_safe 标志不会改变任何方式。

警告

在重用内置过滤器时避免XSS漏洞

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

在旧版本的Django中,在重新使用Django的内置过滤器时要小心,因为 autoescape 默认为 None。您需要通过 autoescape=True 才能获得自动转义。

例如,如果您想编写一个称为 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
    )

然后:

{{ comment|urlize_and_linebreaks }}

将等效于:

{{ comment|urlize|linebreaksbr }}

过滤器和时区

如果您编写了一个对 datetime 对象进行操作的自定义过滤器,通常会使用设置为 Trueexpects_localtime 标志来注册它:

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

设置此标志时,如果过滤器的第一个参数是一个时区感知datetime,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 帮助函数的一些注意事项:

  • 检查所需的参数数量等,在我们的函数被调用时已经完成了,所以我们不需要这样做。

  • 参数(如果有的话)的引号已经被删除,所以我们只接收一个纯字符串。

  • 如果参数是模板变量,则我们的函数将传递变量的当前值,而不是变量本身。

与其他标记实用程序不同,如果模板上下文处于自动转义模式,则 simple_tag 将其输出传递给 conditional_escape(),以确保正确的HTML并保护您免受XSS漏洞。

如果不需要额外的转义,您将需要使用 mark_safe(),如果你绝对确定您的代码不包含XSS漏洞。对于构建小型HTML代码段,强烈建议使用 format_html() 而不是 mark_safe()

Changed in Django 1.9:

增加了前两段所述的 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)

注意第一个参数 must 被称为 context

有关 takes_context 选项如何工作的更多信息,请参阅 inclusion tags 部分。

如果您需要重命名代码,可以为其提供自定义名称:

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 %}
New in Django 1.9.

可以将标签结果存储在模板变量中,而不是直接输出。这通过使用 as 参数后跟变量名称来完成。这样,您可以在合适的位置自己输出内容:

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

包含标记

django.template.Library.inclusion_tag()

另一种常见类型的模板标签是通过渲染 another 模板显示一些数据的类型。例如,Django的管理界面使用自定义模板标签来显示“添加/更改”表单页面底部的按钮。这些按钮总是看起来相同,但是链接目标根据正在编辑的对象而改变 - 因此,它们是使用填充有当前对象的细节的小模板的理想情况。 (在管理员的情况下,这是 submit_row 标签。)

这些标签称为“包含标签”。

写入包含标签可能是最好的例子。让我们写一个标签,输出给定 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() 方法来创建和注册包含标记。按照我们的示例,如果上述模板位于模板加载器搜索的目录中的一个名为 results.html 的文件中,我们将注册该标记:

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

或者,可以使用 django.template.Template 实例来注册包含标签:

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

...第一次创建函数时。

有时,您的包含标签可能需要大量的参数,这使得模板作者传递所有参数并记住它们的顺序很痛苦。为了解决这个问题,Django为包含标签提供了一个 takes_context 选项。如果在创建模板标记时指定 takes_context,那么标记将没有必需的参数,底层Python函数将有一个参数 - 调用标记时的模板上下文。

例如,假设您正在编写一个包含标记,该标记将始终用于包含指向主页的 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'],
    }

注意,函数 must 的第一个参数被称为 context

register.inclusion_tag() 行中,我们指定了 takes_context=True 和模板的名称。以下是模板 link.html 的外观:

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

然后,任何时候你想使用该自定义标签,加载它的库并调用它没有任何参数,如:

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

1.9 版后已移除: simple_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>

高级自定义模板标记

有时,自定义模板标记创建的基本功能是不够的。不要担心,Django让您完全访问从头开始构建模板标记所需的内部。

快速概述

模板系统在两个步骤中工作:编译和渲染。要定义自定义模板标记,请指定编译的工作原理以及渲染的工作原理。

当Django编译模板时,它将原始模板文本拆分为“nodes”。每个节点是 django.template.Node 的实例,并具有 render() 方法。编译模板简单地是 Node 对象的列表。当您在编译的模板对象上调用 render() 时,模板将使用给定的上下文在其节点列表中的每个 Node 上调用 render()。结果都被连接在一起以形成模板的输出。

因此,要定义自定义模板标签,您需要指定原始模板标签如何转换为 Node (编译函数)以及节点的 render() 方法。

编写编译函数

对于模板解析器遇到的每个模板标签,它调用带有标签内容的Python函数和解析器对象本身。此函数负责基于标记的内容返回 Node 实例。

例如,让我们编写一个完全实现的简单模板标签 {% current_time %},显示当前日期/时间,根据 strftime() 语法中的标签中给定的参数格式化。在其他任何事情之前决定标记语法是一个好主意。在我们的例子中,我们假设标签应该像这样使用:

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

该函数的解析器应该抓取参数并创建一个 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:
        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() 不会那么健壮,因为它会原样分割在 all 空间上,包括在引用的字符串中。这是一个好主意,总是使用 token.split_contents()

  • 这个函数负责提高 django.template.TemplateSyntaxError,有帮助的消息,任何语法错误。

  • TemplateSyntaxError 异常使用 tag_name 变量。不要在错误消息中硬编码标记的名称,因为它会将标记的名称与您的函数相关联。 token.contents.split()[0] 将“始终”是您的标记的名称 - 即使标记没有参数。

  • 该函数返回 CurrentTimeNode,其中包含节点需要知道的关于此标记的所有内容。在这种情况下,它只传递参数 - "%Y-%m-%d %I:%M %p"。模板标记的前导和尾部引号在 format_string[1:-1] 中被删除。

  • 解析是非常低级的。 Django开发人员已经尝试在这个解析系统之上编写小框架,使用诸如EBNF语法的技术,但是这些实验使得模板引擎太慢。它是低级的,因为这是最快的。

写入渲染器

编写自定义标签的第二步是定义一个具有 render() 方法的 Node 子类。

继续上面的例子,我们需要定义 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)

笔记:

  • __init__()do_current_time() 获取 format_string。始终通过其 __init__() 将任何选项/参数/参数传递给 Node

  • render() 方法是工作实际发生的地方。

  • render() 通常应该默默失败,特别是在生产环境中。在某些情况下,特别是如果 context.template.engine.debugTrue,这个方法可能会引发异常,使调试更容易。例如,如果几个核心标签接收到错误的数量或类型的参数,则会产生 django.template.TemplateSyntaxError

最终,编译和渲染的这种解耦导致了高效的模板系统,因为模板可以呈现多个上下文,而不必被多次解析。

自动转义注意事项

模板标签的输出是 自动运行通过自动转义过滤器(除了如上所述的 simple_tag())。但是,在编写模板标记时,您仍然需要记住一些事情。

如果您的模板的 render() 函数将结果存储在上下文变量中(而不是返回字符串中的结果),则应注意在适当时调用 mark_safe()。当变量最终呈现时,它会受到当时有效的自动转义设置的影响,因此应该避免进一步转义的内容需要标记为这样。

此外,如果您的模板标记为执行某些子呈现创建了一个新的上下文,请将auto-escape属性设置为当前上下文的值。 Context 类的 __init__ 方法使用一个名为 autoescape 的参数,您可以将其用于此目的。例如:

from django.template import Context

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

这不是一个很常见的情况,但它是有用的,如果你自己渲染模板。例如:

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

如果我们在这个例子中忽略了将当前 context.autoescape 值传递给我们的新 Context,那么结果将自动转义 always,如果在 {% autoescape off %} 块中使用模板标签,则可能不是所需的行为。

线程安全考虑

一旦节点被解析,其 render 方法可被调用任何次数。由于Django有时在多线程环境中运行,因此单个节点可以响应于两个单独的请求而用不同的上下文同时呈现。因此,确保模板标记是线程安全的很重要。

为了确保你的模板标签是线程安全的,你不应该在节点本身存储状态信息。例如,Django提供了一个内置的 cycle 模板标签,它在每次呈现的给定字符串列表之间循环:

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

CycleNode 的一个天真的实现可能看起来像这样:

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)

但是,假设我们有两个模板同时从上面呈现模板片段:

  1. 线程1执行其第一次循环迭代,CycleNode.render() 返回’row1’

  2. 线程2执行其第一次循环迭代,CycleNode.render() 返回’row2’

  3. 线程1执行其第二循环迭代,CycleNode.render() 返回’row1’

  4. 线程2执行其第二循环迭代,CycleNode.render() 返回’row2’

CycleNode是迭代,但它是全局迭代。就线程1和线程2而言,它总是返回相同的值。这显然不是我们想要的!

为了解决这个问题,Django提供了一个与当前正在渲染的模板的 context 相关联的 render_contextrender_context 表现得像一个Python字典,并且应该用于存储 render 方法的调用之间的 Node 状态。

让我们重构我们的 CycleNode 实现以使用 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)

注意,存储在 Node 的整个生命周期中不会作为属性改变的全局信息是完全安全的。在 CycleNode 的情况下,在 Node 被实例化之后,cyclevars 自变量不改变,所以我们不需要把它放在 render_context 中。但是对于当前正在渲染的模板特定的状态信息,如 CycleNode 的当前迭代,应该存储在 render_context 中。

注解

注意我们如何使用 self 来定义 render_context 中的 CycleNode 特定信息。在给定模板中可能有多个 CycleNodes,因此我们需要小心,不要破坏另一个节点的状态信息。最简单的方法是总是使用 self 作为 render_context 的密钥。如果你跟踪几个状态变量,使 render_context[self] 字典。

注册标签

最后,使用模块的 Library 实例注册标签,如上文 writing custom template filters 中所述。例:

register.tag('current_time', do_current_time)

tag() 方法有两个参数:

  1. 模板标记的名称 - 字符串。如果省略,将使用编译函数的名称。

  2. 编译函数 - 一个Python函数(不是作为字符串的函数的名称)。

与过滤器注册一样,也可以将其用作装饰器:

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

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

如果你离开 name 参数,如上面的第二个例子,Django将使用函数的名称作为标签名称。

将模板变量传递给标记

虽然可以使用 token.split_contents() 向模板标记传递任意数量的参数,但是这些参数都是作为字符串文字解压缩的。为了将动态内容(模板变量)传递给模板标签作为参数,还需要进行更多的工作。

虽然前面的示例已将当前时间格式化为字符串并返回了字符串,但是假设您希望从对象传递 DateTimeField 并具有date-time的模板标记格式:

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

最初,token.split_contents() 将返回三个值:

  1. 标记名称 format_time

  2. 字符串 'blog_entry.date_updated' (不带周围的引号)。

  3. 格式化字符串 '"%Y-%m-%d %I:%M %p"'split_contents() 的返回值将包括字符串文字的前导和尾部引号,如下所示。

现在您的标记应该开始看起来像这样:

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

您还必须更改渲染器以检索 blog_entry 对象的 date_updated 属性的实际内容。这可以通过使用 django.template 中的 Variable() 类来实现。

要使用 Variable 类,只需使用要解析的变量的名称实例化它,然后调用 variable.resolve(context)。所以,例如:

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 ''

如果变量分辨率无法解析在页面的当前上下文中传递给它的字符串,则会抛出 VariableDoesNotExist 异常。

在上下文中设置变量

上面的例子只是输出一个值。一般来说,如果您的模板标记设置模板变量而不是输出值,则更灵活。这样,模板作者可以重用您的模板标签创建的值。

要在上下文中设置变量,只需在 render() 方法的上下文对象上使用字典分配。这里是 CurrentTimeNode 的更新版本,设置模板变量 current_time,而不是输出:

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 ''

请注意,render() 返回空字符串。 render() 应始终返回字符串输出。如果所有模板标签都设置了一个变量,render() 应该返回空字符串。

以下说明如何使用这个新版本的标记:

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

上下文中的变量范围

上下文中的任何变量集都只能在分配它的模板的相同 block 中使用。这种行为是故意的;它为变量提供了一个范围,使它们不会与其他块中的上下文冲突。

但是,CurrentTimeNode2 有一个问题:变量名 current_time 是硬编码的。这意味着你需要确保你的模板不在别的地方使用 {{ current_time }},因为 {% current_time %} 会盲目地覆盖该变量的值。一个更清洁的解决方案是使模板标签指定输出变量的名称,如下所示:

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

这里的区别是,do_current_time() 抓取格式字符串和变量名,将两者传递给 CurrentTimeNode3

最后,如果您只需要为自定义上下文更新模板标记提供一个简单的语法,请考虑使用 simple_tag() 快捷方式,它支持将标记结果分配给模板变量。

解析直到另一个块标记

模板标签可以协同工作。例如,标准 {% comment %} 标签隐藏所有内容,直到 {% endcomment %}。要创建这样的模板标签,请在编译函数中使用 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 ''

注解

{% comment %} 的实际实现略有不同,它允许破碎的模板标签出现在 {% comment %}{% endcomment %} 之间。它通过调用 parser.skip_past('endcomment') 而不是 parser.parse(('endcomment',)),然后是 parser.delete_first_token() 来这样做,从而避免生成节点列表。

parser.parse() 使用块标签的名称的元组来解析,直到“”。它返回一个 django.template.NodeList 的实例,它是一个所有 Node 对象的列表,解析器遇到’‘before’‘遇到元组中命名的任何标签。

在上述示例中的 "nodelist = parser.parse(('endcomment',))" 中,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:

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)

有关复杂渲染的更多示例,请参阅 django/template/defaulttags.py 中的 {% for %}django/template/smartif.py 中的 {% if %} 的源代码。