自定义模板标签和过滤器 — Django 文档

来自菜鸟教程
Django/docs/3.1.x/howto/custom-template-tags
跳转至:导航、​搜索

自定义模板标签和过滤器

Django 的模板语言带有各种各样的 内置标签和过滤器 ,旨在满足应用程序的表示逻辑需求。 尽管如此,您可能会发现自己需要的功能未包含在核心模板原语集内。 您可以通过使用 Python 定义自定义标签和过滤器来扩展模板引擎,然后使用 :ttag:`{% 加载 %} ` 标签。

代码布局

指定自定义模板标签和过滤器的最常见位置是在 Django 应用程序中。 如果它们与现有应用程序相关,则将它们捆绑在那里是有意义的; 否则,它们可以添加到新应用程序中。 当 Django 应用程序被添加到 :setting:`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` 为了使 :ttag:`{% 加载 %} ` 标记工作。 这是一项安全功能:它允许您在单个主机上托管许多模板库的 Python 代码,而无需为每个 Django 安装启用对所有模板库的访问。

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

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

from django import template

register = template.Library()

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

幕后花絮

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

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


编写自定义模板过滤器

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

  • 变量(输入)的值——不一定是字符串。
  • 参数的值——这可以有一个默认值,或者完全省略。

例如,在过滤器 模板:Var 中,过滤器 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 的自动转义行为交互。 请注意,可以在模板代码中传递两种类型的字符串:

  • Raw strings 是原生 Python 字符串。 在输出时,如果自动转义有效并保持不变,则它们将被转义,否则。

  • 安全字符串 是在输出时被标记为安全的字符串。 任何必要的转义都已经完成。 它们通常用于包含旨在在客户端按原样解释的原始 HTML 的输出。

    在内部,这些字符串的类型为 SafeString。 您可以使用以下代码测试它们:

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # 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 对象变回正常的 str 对象,而不是试图将它们全部捕获,这将是非常困难的,Django 在过滤器完成后修复损坏。

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

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

    当在启用自动转义的模板中使用此过滤器时,只要输入尚未标记为“安全”,Django 就会对输出进行转义。

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

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

    标记过滤器 is_safe 会将过滤器的返回值强制转换为字符串。 如果您的过滤器应返回布尔值或其他非字符串值,将其标记为 is_safe 可能会产生意想不到的后果(例如将布尔值 False 转换为字符串 'False')。

  2. 或者,您的过滤器代码可以手动处理任何必要的转义。 当您在结果中引入新的 HTML 标记时,这是必要的。 您希望将输出标记为安全,以免进一步转义,以便您的 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)

    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 的自定义过滤器,它结合了 :tfilter:`urlize`:tfilter:`linebreaksbr` 过滤器,过滤器看起来像:

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 对象进行操作的自定义过滤器,您通常会将 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 辅助函数的一些注意事项:

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

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

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

如果您的模板标签需要访问当前上下文,您可以在注册标签时使用 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 %}

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

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

包含标签

django.template.Library.inclusion_tag()

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

这些类型的标签称为“包含标签”。

编写包含标签可能最好通过示例来演示。 让我们编写一个标签,输出给定 Poll 对象的选项列表,例如在 tutorials 中创建的对象。 我们将像这样使用标签:

{% 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'],
    }

请注意,函数 的第一个参数必须 称为 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 让您可以完全访问从头开始构建模板标签所需的内部结构。


快速概览

模板系统分两步工作:编译和渲染。 要定义自定义模板标记,您需要指定编译的工作方式和渲染的工作方式。

当 Django 编译模板时,它会将原始模板文本拆分为“节点”。 每个节点都是 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() 不会那么健壮,因为它会天真地拆分 所有 空间,包括带引号的字符串中的空间。 始终使用 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

最终,这种编译和渲染的解耦产生了一个高效的模板系统,因为模板可以渲染多个上下文而无需多次解析。


自动转义注意事项

模板标签的输出是 not 自动通过自动转义过滤器(除了如上所述的 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在这个例子中,结果将有总是被自动转义,如果模板标签用于内部,这可能不是所需的行为 :ttag:`{% 自动转义关闭 %} ` 堵塞。


线程安全注意事项

一旦一个节点被解析,它的 render 方法可以被调用任意次数。 由于 Django 有时运行在多线程环境中,因此单个节点可能会同时使用不同的上下文来响应两个单独的请求。 因此,确保您的模板标签是线程安全的非常重要。

为了确保你的模板标签是线程安全的,你永远不应该在节点本身上存储状态信息。 例如,Django 提供了一个内置的 :ttag:`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 提供了一个 render_context,它与当前正在渲染的模板的 context 相关联。 render_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 实例,如上面的 编写自定义模板标签 中所述。 例子:

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 并具有日期时间的模板标记格式:

<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() 快捷方式,它支持将标签结果分配给模板变量。


解析直到另一个块标记

模板标签可以协同工作。 例如,标准 :ttag:`{% 评论 %} ` 标签隐藏一切,直到{% 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 ''

笔记

实际执行 :ttag:`{% 评论 %} ` 略有不同,因为它允许损坏的模板标签出现在{% comment %}{% endcomment %} . 它通过调用 parser.skip_past('endcomment') 而不是 parser.parse(('endcomment',)) 后跟 parser.delete_first_token() 来实现,从而避免生成节点列表。


parser.parse() 接受一个块标签名称的元组“解析直到”。 它返回一个 django.template.NodeList 的实例,它是解析器在遇到元组中命名的任何标记之前遇到的所有 Node 对象的列表。

在上例中的"nodelist = parser.parse(('endcomment',))"中,nodelist{% comment %}{% endcomment %}之间所有节点的列表,不包括{% comment %}和[ X132X] 自己。

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)

更多复杂渲染的例子,请查看源代码 :ttag:`{% 为 %} `django/template/defaulttags.py:ttag:`{% 如果 %} `django/template/smartif.py .