自定义模板标签和过滤器

Django的模板语言带有各种各样的built-in tags and filters,旨在解决您的应用程序的表示逻辑需求。 不过,您可能会发现自己需要的功能不在核心模板原语集范围内。 您可以通过使用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()

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

在幕后

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

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

编写自定义模板过滤器

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

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

For example, in the filter {{ var|foo:"bar" }}, the filter foo would be passed the variable var and the argument "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。过滤 T0>()¶ T1>

一旦你编写了你的​​过滤器定义,你需要将它注册到你的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 这些参数在下面的filters and auto-escapingfilters and time zones中进行了描述。

期望字符串的模板过滤器

django.template.defaultfilters。 stringfilter T0>()¶ T1>

如果你正在编写一个模板过滤器,只需要一个字符串作为第一个参数,你应该使用装饰器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 are the native Python strings. 在输出时,如果自动转义生效并且保持不变,则会被转义。

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

    在内部,这些字符串的类型是SafeText 你可以使用如下代码来测试它们:

    from django.utils.safestring import SafeText
    
    if isinstance(value, SafeText):
        # Do something with the "safe" string.
        ...
    

模板过滤器代码有两种情况:

  1. Your filter does not introduce any HTML-unsafe characters (<, >, ', " or &) into the result that were not already present. 在这种情况下,您可以让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,您可以从任何不需要的过滤器中将其省略。

    在决定你的过滤器是否真的离开安全的字符串时要小心。 If you’re removing characters, you might inadvertently leave unbalanced HTML tags or entities in the result. 例如,从输入中移除>可能会将<a>转换为<a,这需要在输出上转义避免造成问题。 同样,删除分号(;)可以将&amp;转换为&amp,这不再是一个有效的实体,进一步逃脱。 大多数情况下几乎不会这么棘手,但在查看代码时请留意这类问题。

    标记过滤器is_safe会强制过滤器的返回值为一个字符串。 If your filter should return a boolean or other non-string value, marking it is_safe will probably have unintended consequences (such as converting a boolean False to the string ‘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传递。 (在后一种情况下,我们只是使用身份函数作为“转义”函数。) The conditional_escape() function is like escape() except it only escapes input that is not a SafeData instance. 如果将一个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 ''

设置此标志时,如果过滤器的第一个参数是可识别时区的日期时间,则根据rules for time zones conversions in templates

编写自定义模板标签

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

简单的标签

django.template.Library。 simple_tag T0>()¶ T1>

许多模板标签需要一些参数(字符串或模板变量),并在完成一些基于输入参数和外部信息的处理后返回结果。 例如,一个current_time标签可能会接受一个格式字符串,并将其返回格式为相应的字符串。

To ease the creation of these types of tags, Django provides a helper function, 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辅助函数的一些注意事项:

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

Unlike other tag utilities, simple_tag passes its output through conditional_escape() if the template context is in autoescape mode, to ensure correct HTML and protect you from XSS vulnerabilities.

如果不需要额外的转义,如果您确信您的代码不包含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选项如何工作的更多信息,请参阅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 functions may accept any number of positional or keyword arguments. 例如:

@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 T0>()¶ T1>

另一种常见的模板标签类型是通过呈现另一个模板来显示一些数据的类型。 例如,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函数将有一个参数 - 模板上下文作为标签被调用的时间。

For example, say you’re writing an inclusion tag that will always be used in a context that contains home_link and home_title variables that point back to the main page. 以下是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 functions may accept any number of positional or keyword arguments. 例如:

@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对象的列表。 When you call render() on a compiled template object, the template calls render() on each Node in its node list, with the given context. 结果全部连接在一起形成模板的输出。

因此,要定义一个自定义模板标签,可以指定原始模板标签如何转换为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是标签原始内容的字符串。 In our example, it’s 'current_time "%Y-%m-%d %I:%M %p"'.
  • token.split_contents()方法将空格上的参数分开,同时将引用的字符串保留在一起。 更简单的token.contents.split()不会像在t2之前所有的空格(包括引用字符串中的空格)那样天真地分割。 始终使用token.split_contents()是一个好主意。
  • 这个函数负责为任何语法错误提示django.template.TemplateSyntaxError,其中包含有用的消息。
  • TemplateSyntaxError异常使用tag_name变量。 不要在错误信息中对标签名称进行硬编码,因为这会将标签的名称与您的功能相结合。 token.contents.split()[0]将始终作为您的标记的名称 - 即使标记没有参数。
  • 该函数返回一个CurrentTimeNode节点需要知道的关于这个标签的所有信息。 In this case, it just passes the argument – "%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__() gets the format_string from do_current_time(). 总是通过它的__init__()传递任何选项/参数/参数给Node
  • The render() method is where the work actually happens.
  • render() should generally fail silently, particularly in a production environment. 然而,在某些情况下,特别是如果context.template.engine.debugTrue,则此方法可能会引发异常,使调试更容易。 For example, several core tags raise django.template.TemplateSyntaxError if they receive the wrong number or type of arguments.

最终,编译和渲染的解耦会产生一个高效的模板系统,因为模板可以渲染多个上下文而不必多次分析。

自动转义注意事项

The output from template tags is not automatically run through the auto-escaping filters (with the exception of simple_tag() as described above). 但是,在编写模板标签时,还是应该记住一些事情。

如果模板的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,结果总是会自动转义,如果在{% 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. 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’

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

为了解决这个问题,Django提供了一个与当前正在呈现的模板的context关联的render_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的情况下,cyclevars参数在Node实例化后不会改变,所以我们不需要把它放入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 ''

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.

以下是如何使用这个新版本的标签:

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

The difference here is that do_current_time() grabs the format string and the variable name, passing both to 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 ''

注意

The actual implementation of {% comment %} is slightly different in that it allows broken template tags to appear between {% comment %} and {% endcomment %}. 它通过调用parser.skip_past('endcomment')来代替parser.parse(('endcomment',)),然后是parser.delete_first_token(),从而避免生成节点列表。

parser.parse() takes a tuple of names of block tags ‘’to parse until’‘. 它返回一个django.template.NodeList的实例,该实例是解析器遇到的所有Node对象的列表。元组

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. {% comment %}{% endcomment %}被忽略。

解析到另一个块标记,并保存内容

在前面的例子中,do_comment()丢弃了{% 注释 %} t2 >和{% endcomment %} 而不是这样做,有可能做一些块标签之间的代码。

For example, here’s a custom template tag, {% upper %}, that capitalizes everything between itself and {% 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)

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