如何编写自定义的模板标签和过滤器

您所在的位置:网站首页 标签模板怎么制作方法图片 如何编写自定义的模板标签和过滤器

如何编写自定义的模板标签和过滤器

2024-05-27 01:54| 来源: 网络整理| 查看: 265

编写自定义模板标签¶

标签比过滤器更复杂,因为标签啥都能做。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 %} The time is {{ the_time }}. 包含标签¶ django.template.Library.inclusion_tag()¶

另一种常见的模板标签会为 另一个 模板渲染数据。例如, Django 的后台利用自定义模板标签在表单页的底部展示按钮。这些按钮看起来一样,但是连接目标根据被编辑的对象不同而不同——所以,这是一个极好的例子,展示如何用当前对象的细节填充小模板。(在后台例子中,即 submit_row 标签。)

这种标签被称为“包含标签”。

编写包含标签可能最好通过实例来展示。让我们编写一个标签,它会将指定 Poll 对象(就像 教程 中创建的那样)的选项以列表输出。我们像这样使用标签:

{% show_results poll %}

输出看起来像这样:

First choice Second choice Third choice

首先,定义一个函数,接受参数,并返回一个字典。此处的要点是我们只需返回一个字典,不是任何其它复杂的东西。这将作为一个模板上下文被模板碎片使用。例子:

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

随后,创建用于渲染标签输出的模板。该模板是标签的一个固有特性:标签作者指定它,而不是模板设计者。跟随我们的例子,模板非常简短:

{% for choice in choices %} {{ choice }} {% endfor %}

现在,在 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 为包含标签提供了一个 take_context 选项。如果在创建模板标签时指定了 takes_context,该标签将没有必要的参数,底层 Python 函数将只有一个参数——标签创建时的模板上下文。

举个例子,假设你编写了一个包含标签,总会在一个包含指向首页的 home_link 和 home_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 {{ title }}.

后面,当你想用该自定义标签时,加载它的库,并不带任何参数的调用它,像这样:

{% jump_link %}

注意,只要使用了 takes_context=True,就无需为模板标签传递参数。它自动从上下文获取。

takes_context 参数默认为 False。当其为 True,标签会被传入上下文对象,像本例展示的那样。这是本例和之前的 包含标签 实例的唯一不同之处。

包含标签 函数能接受任意个数的位置或关键字参数。例子:

@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() 方法。编译完的模板就是一个包含 节点 对象的列表。当你在已编译的模板上调用 render(),该模板会为节点列表中的每个 节点 携带指定上下文调用 render() 方法。结果会自动连接,形成模板的输出。

因此,要定义一个自定义模板标签,你要指定如何将原始模板标签转换为 节点 (编译函数),还要指定 render() 方法的操作。

编写编译函数¶

模板解析器遇到的每个模板标签,解析器都会调用一个 Python 函数,参数是标签内容和解析器对象本身。该函数需要基于标签的内容返回一个 节点 实例。

举个例子,让我们完整地实现模板标签 {% current_time %},该标签根据标签中指定的参数,以 strftime() 语法格式化当前时间或日期。先决定标签语法是个不错的主意。在本例中,我们要这样使用标签:

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

这个函数的解析器应用获取参数,并创建一个 节点 对象:

from django import template def do_current_time(parser, token): try: # split_contents() knows not to split quoted strings. tag_name, format_string = token.split_contents() except ValueError: raise template.TemplateSyntaxError( "%r tag requires a single argument" % token.contents.split()[0] ) if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): raise template.TemplateSyntaxError( "%r tag's argument should be in quotes" % tag_name ) return CurrentTimeNode(format_string[1:-1])

注意:

parser 是模板解析器对象。本例中不需要。 token.contents 是包含标签原始内容的字符串。本例中是 'current_time "%Y-%m-%d %I:%M %p"'。 token.split_contents() 方法按空格分隔字符串,但不会分隔引号包裹的部分。二愣子 token.contents.split() 就没那么健壮了,它直接在空格处分割字符串,不论它们是否被引号包裹。推荐总是使用 token.split_contents()。 该方法要在语法错误发生时抛出包含有用信息的 django.template.TemplateSyntaxError。 TemplateSyntaxError 异常使用了 tag_name 变量。不要在错误消息中硬编码标签名,因为这会使的标签名与函数耦合。 token.contents.split()[0] 总会返回标签名——即便标签没有参数。 该函数返回一个 CurrentTimeNode,内含节点需要了解的标签的一切信息。在本例中,传递了参数—— "%Y-%m-%d %I:%M %p"。开头和结尾的引号由 format_string[1:-1] 删除。 这种解析是很低级的。Django 开发者已试着在该解析系统之上编写小型解析框架,使用类似 EBNF 语法,但这些尝试使得模板引擎运行的很慢。低级意味着快。 编写渲染器¶

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

承接上述例子,我们需要定义 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__() 方法为其传入 options/parameters/arguments。 render() 方法是实际干活的地方。 render() 应该无声失败,尤其是在生产环境。不过,某些场景下,尤其是 context.template.engine.debug 为 True 时,该方法可能抛出一个异常,简化调式流程。例如,某些核心标签在接受个数不对的参数时抛出 django.template.TemplateSyntaxError。

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

自动转义的注意事项¶

模板标签的输出 不会 自动通过自动转义过滤器(除了上述的 simple_tag() 之外)。不过,在编写模板标签时,你仍需牢记几点。

若模板标签的 render() 方法在上下文变量中存储结果(而不是以字符串返回结果),它要小心地在合适的时机调用 mark_safe()。当变量最后一次被渲染时,它会在这个时候受到自动转义配置的影响,所以为了避免变量被进一步转义,需要如此配置。

同理,如果你的模板标签为某些子渲染进程创建了新的上下文,那么就需要将当前上下文对应的自动转义属性传入。 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 %} ... {% 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 执行其第一次迭代, CycleNode.render() 返回 'row1' 线程 2 执行其第一次迭代, CycleNode.render() 返回 'row2' 线程 1 执行其第二次迭代, CycleNode.render() 返回 'row1' 线程 2 执行其第二次迭代, CycleNode.render() 返回 'row2'

CycleNode 正在被迭代,却是全局范围的。就像线程 1 和线程 2 担心的那样,它们总是返回同样的值。这不是我们想要的。

为了定位此问题,Django 提供了一个 render_context,关联至当前正在渲染的模板的 context。 render_context 表现的像一个 Python 字典,应该在其中保存多次同时调用 render 方法时的 Node 状态。

让我们用 render_context 重构我们的 CycleNode 实现:

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 将 CycleNode 的特定参数装入 render_context 的。一个模板中可能有多个 CycleNodes,所以我们要十分小心,不要破坏其它节点的状态信息。最简单的方式就是一直将 self 作为键存入 render_context。如果你同时追踪好几个状态变量,将 render_context[self] 做成一个字典。

注册该标签¶

最后,用你的模块的 Library 实例注册该标签,像上文 编写自定义模板标签 1 介绍的那样。举个例子:

register.tag("current_time", do_current_time)

tag 方法接收两个参数:

模板标签的名字——一个字符串。若为空,将会使用编译函数的名字。 编辑函数——一个 Python 函数(不是函数名的字符串)。

就像过滤器注册一样,这里也能用装饰器:

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

若未输入 name 参数,像上述第二个例子一样,Django 会将函数名作为标签名。

传递模板变量给标签¶

虽然你能利用 token.split_contents() 将任意数量的变量传递给一个模板标签,但是解包出来的参数均是字符串文本。要将一个动态内容(一个模板变量)作为参数传递给模板标签需要额外工作。

前文的例子已经成功将当前时间转为字符串并将之返回,假设你想传入一个 DateTimeField 对象,并想用该模板标签格式化这个对象:

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

首先,token.split_contents() 会返回 3 个值:

标签名 format_time。 字符串 'blog_entry.date_updated' (不包含引号)。 格式化字符串 '"%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])

你也需要修改 renderer,让其获取 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" %}The time is {{ current_time }}.

上下文中的变量作用域

上下文内变量仅在模板中相同 block 内生效。这是故意的;提供有作用域的变量不会与其它区块中的上下文发生冲突。

但是, CurrentTimeNode2 有个问题:变量名 current_time 是硬编码的。这意味着你需要确认模板未在其它地方使用 {{ current_time }},因为 {% current_time %} 会绑定兵重写该变量的值。一个简洁的方法是让模板标签指定输出变量的值,像这样:

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

为此,你需要重构编译函数和 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:{% 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()

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

要获取更多复杂渲染的示例,请查看 django/template/defaulttags.py 中的 {% for %} 和 django/template/smartif.py 中的 {% if %} 的源代码。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3