如何创建自定义模板标签和过滤器

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

或者,可以通过DjangoTemplates'libraries' 参数来注册模板标签模块。如果您希望在加载模板标签时使用与模板标签模块名称不同的标签,这将非常有用。它还允许您在不安装应用程序的情况下注册标签。

幕后

要查看大量示例,请阅读 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 字符串。在输出时,如果启用了自动转义,则会对其进行转义,否则保持不变。

  • 安全字符串是已标记为在输出时免于进一步转义的字符串。任何必要的转义都已完成。它们通常用于包含原始 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(),不同之处在于它只转义不是 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 对象进行操作的自定义过滤器,您通常会将其注册为 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 对象输出一个选项列表,例如在 教程 中创建的。我们将像这样使用该标签:

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

最终,这种编译和渲染的分离导致了一个高效的模板系统,因为模板可以在无需多次解析的情况下渲染多个上下文。

自动转义注意事项

模板标签的输出不会自动通过自动转义过滤器运行(simple_tag() 如上所述除外)。但是,在编写模板标签时,您仍然需要注意几件事。

如果您的模板标签将结果存储在上下文变量中(而不是以字符串形式返回结果),则 render() 方法应在适当情况下调用 mark_safe()。最终渲染变量时,它将受到当时有效的自动转义设置的影响,因此应将不应进一步转义的内容标记为安全内容。

此外,如果您的模板标签创建新的上下文以执行一些子渲染,请将自动转义属性设置为当前上下文的 value。Context 类的 __init__ 方法带有一个名为 autoescape 的参数,您可以为此 purpose 使用它。例如

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,则结果将始终自动转义,如果模板标签用于 {% 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 字典,应用于存储 Noderender 方法调用之间的状态。

让我们重构我们的 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 的情况下,cyclevars 参数在 Node 实例化后不会更改,因此我们不需要将其放入 render_context 中。但是,特定于当前正在渲染的模板的状态信息(例如 CycleNode 的当前迭代)应存储在 render_context 中。

注意

请注意我们如何使用 selfrender_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()快捷方式,它支持将标签结果赋值给模板变量。

解析到另一个块标签

模板标签可以协同工作。例如,标准的{% 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对象的列表。

在上例中的"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()

这里唯一的新概念是self.nodelist.render(context)UpperNode.render()中。

有关复杂渲染的更多示例,请参见{% for %}的源代码(位于django/template/defaulttags.py)和{% if %}的源代码(位于django/template/smartif.py)。

返回顶部