django “如何”系列4:如何编写自定义模板标签和过滤器

时间:2023-03-08 15:53:42
django “如何”系列4:如何编写自定义模板标签和过滤器

django的模板系统自带了一系列的内建标签和过滤器,一般情况下可以满足你的要求,如果觉得需更精准的模板标签或者过滤器,你可以自己编写模板标签和过滤器,然后使用{% load %}标签使用他们。

代码布局

自定义标签和过滤器必须依赖于一个django app,也就是说,自定义标签和过滤器是绑定app的。该app应该包含一个templatetags目录,这个目录一个和model.py,views.py在同一个层级,记得在该目录下建立一个__init__.py文件一遍django知道这是一个python包。在该目录下,你可以新建一个python模块文件,文件名不要和其他app中的冲突就好。例如:

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

然后在你的模板文件中你可以这样使用你的自定义标签和过滤器:

{% load poll_extras %}

注意事项:

  • 包含templatetags目录的app一定要在INSTALLED_APPS列表里面
  • {% load %}load的是模块名,而不是app名
  • 记得使用 from django import template ,register=template.Library()注册

编写自定义模板过滤器

自定义过滤器就是接受一个或者连个参数的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:"" }}

如果过滤器不接受参数,只需要这样写

def lower(value): # 只有一个参数
return value.lower()

注册自定义的过滤器

一旦定义好你的过滤器,你需要注册这个过滤器,有两种方式,一种是上面提到的template.Library(),另一种是装饰器

#第一种方法
register.filter('cut', cut)
register.filter('lower', lower)
#第二种方法
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter
def lower(value):
return value.lower()

stringfilter

如果你的模板过滤器只希望接受字符串作为第一个参数,那么你可以是用stringfilter装饰器,这样的话,在传参进你的函数之前,该参数的值会被转换成对应字符串值

from django import template
from django.template.defaultfilters import stringfilter register = template.Library() @register.filter
@stringfilter
def lower(value):
return value.lower()

过滤器和自动转义

当你编写一个过滤器的时候,考虑一下该过滤器如何和django的自动转义行为“协作”。注意到三种类型的字符串可以被传进模板代码中。

  • 原始字符串(raw strings):本地的str或者unicode。在输出的时候,如果可以自动转义的话会被转义的,否则就会保持不变
  • 安全字符串(safe strings):在输出的时候已经被标识为安全的。任何可能的转义都已经被转义了。
  • 被标记为需要转义的字符串:在输出的时候总是要被转义

模板过滤器代码分为下面两种情况:

  • 你的过滤器没有任何的HTML不安全字符(<>,"&),在这种情况下,你可以是用is_safe=True来装饰你的过滤器函数,is_safe默认为False
@register.filter(is_safe=True)
def myfilter(value):
return value
  • 同样的,你的过滤器代码可以人为的注意 任何必须的转义。为了标识一个输出时安全的,我们可以使用django.utils.safestring.mark_safe()函数。如果你需要知道你的过滤器目前的自动转义状态,在你注册过滤器函数的时候设置needs_autoescape标识为True(默认为False),这个标识告诉django,你的过滤器函数想要一个额外的关键字参数autoescape,如果auto-escape有效则返回真,否则返回False
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe @register.filter(needs_autoescape=True)
def initial_letter_filter(text, autoescape=None):
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去决定我们是否要使用condition_escape,也因此,在最后我们使用mark_safe告诉我们的模板系统这个已经不需要进一步的转义了

过滤器和时区

如果哦你编写一个自定义的过滤器去操作一个datetime对象,你可以使用expects_localtime,并将其设置为真

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

如果这个标识为真,那么如果哦你的第一个参数是一个datetime类型数据,那么django会在将value的值传参进去之前将其转成当前时区的值

编写自定义模板标签

标签比过滤器复杂的多,因为标签可以做任何事情。

快速回顾

模板系统工作有两个流程:编译和渲染。去定义一个模板标签,你需要知道如何去编译和如何去渲染。当django编译一个模板的时候,它会把原始的模板文本分割成一个个节点,每个节点都是django.template.Node实例并且有一个render()方法。一个编译好的模板是一个Node对象的列表。当你在一个已经编译好的模板对象调用render方法时,模板会对node列表中的每一个Node调用render方法(使用给定的上下文),结果会被级联在一起去组成模板的输出。因此,定义一个模板标签,你需要知道一个原始的模板标签是如何被转换成一个Node,以及这个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])

tips:

  • parser是模板解析器对象
  • token.contents是标签的原始内容,在我们的例子中时'current_time "%Y-%m-%d %I:%M %p"'
  • token.split_contents()方法把参数按空格分开,同时保留引号之间的内容,如果使用token.contents.split()的话,这个函数会将所有空格都分开,所以建议还是使用token.split_contents()
  • 这个函数会引发django.template.TemplateSymtaxError,并附有有用的信息
  • TemplateSyntaxError异常使用tag_name变量,所以不要在你的错误信息里面硬编码标签名,因为token.contents.spilt()[0]永远是你的标签名
  • 这个函数返回一个包括所有有关这个标签的内容的CurrentTimeNode对象,所以你只需把参数穿进去就可以了
  • 这个解析过程是非常底层的,所以直接用就好了,因为底层所以快速。

编写渲染器

编写自定义标签的第二步是定义一个Node的子类并且定义一个render方法

from django import template
import datetime
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)

tips:

  • __init__()从上面的do_current_time()中获取format_string,记得只通过__init__()函数传参
  • render()方法才是真正做事情的
  • render函数不会抛出任何异常,只会默默的失败(如果发生异常的话)

最终,编译和渲染的非耦合组成了一个有效的模板系统,因为一个模板可以渲染多个上下文而不用多次解析。

自动转义注意事项

模板标签的输出并不会自动的执行自动转义过滤器的,所以当你编写一个模板标签的时候你需要注意这些事情:

如果render函数在一个上下文变量里面存储结果(而不是一个字符串),你需要注意正确的使用mark_safe(),当该变量已经是最终渲染了,你需要给它打上标识,以防会受到自动转义的影响。

并且,你的模板标签新建一个用于进一步渲染的上下文,记得把自动转义属性设置为当前上下文的值。Context的 __init__()方法接受一个autoescape的参数

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

这不是一个很常用的情景,但是当你自己渲染一个模板的时候会很有用

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

如果我们不传这个参数的时候,结果可能是永远都是自动转义的,即使这个标签实在{% autoescape off %}块里面。

线程安全考虑

一旦一个节点被解析,render方法会被调用任意次,由于django有时运行在多线程的环境,单个节点可能会被两个独立的请求的不同上下文同时渲染,因此,保证你的模板标签线程安全是很重要的

为了保证你的模板标签是线程安全的,你应该永远不要存储信息在节点本身。举个例子,django提供一个内建的cycle模板标签,这个标签每次渲染的时候都会循环一个给定字符串的列表

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

一个朴素的CycleNode的实现可能想这样:

class CycleNode(Node):
def __init__(self, cyclevars):
self.cycle_iter = itertools.cycle(cyclevars)
def render(self, context):
return self.cycle_iter.next()

但,假设我们有两个模板,同时渲染上面那个小模板:

  • 线程1执行第一次循环迭代,CycleNode.render()返回row1
  • 线程2执行第一次循环迭代,CycleNode.render()返回row2
  • 线程1执行第二次循环迭代,CycleNode.render()返回row1
  • 线程2执行第二次循环迭代,CycleNode.render()返回row2

CycleNode是可以迭代的,但却是全局迭代,由于线程1和线程2是关联的,所以它们总是返回相同的值,显然这不是我们想要的结果。

解决这个问题,django提供了一个正在被渲染的模板的上下文关联的render_context,这个render_context就像一个python字典一样,并且应该在render方法被调用之间保存Node状态

让我们使用render_context重新实现CycleNode吧

class CycleNode(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 cycle_iter.next()

注册标签

和过滤器注册差不多

register.tag('current_time', do_current_time)

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

给标签传模板变量

尽管你可以是用token.split_contents()传入任意个参数,但考虑一个参数是一个模板变量的情况(这是一个动态的情况)

假如我们有一个这样的标签,接受一个给定的日期和指定格式,返回用指定格式格式化的日期,像这样:

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

现在你的解析器大概是这样的

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

然后FormatTimeNode大概就要这样子了

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

简单的标签

很多的标签接受很多的参数-字符串或者模板变量-返回一个字符串或者空串,为了减轻这类简单的标签的创建,django提供了一个简单有效的函数simple_tag。这个函数,是django.template.Library的一个方法,接受一个 可以接受任意个参数的函数 ,然后把这个函数包装成一个render函数,以及其他必要的注册等步奏。

比如之前的current_time函数我们这里可以这样写

def current_time(format_string):
return datetime.datetime.now().strftime(format_string) register.simple_tag(current_time)
#或者这样
@register.simple_tag
def current_time(format_string):
...

如果你的模板标签需要访问当前上下文的话,你可以使用takes_context参数,像下面这样:

# The first argument *must* be called "context" here.
def current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string) register.simple_tag(takes_context=True)(current_time)
#或者这样
@register.simple_tag(takes_context=True)
def current_time(context, format_string):
timezone = context['timezone']
return your_get_current_time_method(timezone, format_string)

或者你想重命名你的标签,你可以这样来指定

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 ...
{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

包含标签

另外一类标签是通过渲染其他的模板来展示内容的,这类标签的用途在于一些相似的内容的展示,并且返回的内容是渲染其他模板得到的内容,这类标签称为“包含标签”。最好我们通过一个例子来阐述。

我们即将写一个标签,这个标签将输出给定Poll对象的选择的列表,我们可以这样使用这个标签

{% show_results poll %}

输出大概是这样的

<ul>
<li>First choice</li>
<li>Second choice</li>
<li>Third choice</li>
</ul>

下面我们看看怎么实现吧。首先定义一个接受一个poll参数的函数,这个函数返回该poll对象的choices

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

然后我们创建一个要被渲染的模板用于输出

<ul>
{% for choice in choices %}
<li> {{ choice }} </li>
{% endfor %}
</ul>

最后是使用inclusion_tag函数注册

register.inclusion_tag('results.html')(show_results)
#或者这样
@register.inclusion_tag('results.html')
def show_results(poll):
...

如果你要使用上下文的话,可以使用takes_context参数,如果你使用了takes_context,这个标签是没有必须参数,不过底层的python函数需要接受一个context的首参(第一个参数必须为context)

#第一个参数必须为context
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
# Register the custom tag as an inclusion tag with takes_context=True.
register.inclusion_tag('link.html', takes_context=True)(jump_link)

link.html可以是这样的

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

那么你可以这样来使用这个标签,不需要带任何的参数

{% jump_link %}

和simple_tag一样,inclusion_tag可以接受关键字参数

在上下文中设置变量

到现在为止,所有的模板标签只是输出一个值,现在我们考虑一下给标签设置变量吧,这样,模板的作者可以重用这些你的标签产生的值。

想要在上下文中设置变量,只需要在render方法中给context对象像字典那样复制,这里有一个升级版的CurrentTimeNode,设置了一个模板变量current_time而不是直接输出该值

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

注意到这个标签是返回一个空串,使用如下:

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

注意事项:

作用范围:上下文中的模板变量仅仅在当前块代码中生效(如果有多个层次的块的话),这是为了预防块之间的变量冲突

覆盖问题:由于变量名是硬编码的,所有同名的变量都会被覆盖,所以强烈建议使用别名as,但是要使用as的话,编译函数和结点类都要重新定义如下

{% current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p> 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 '' import re
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)

赋值标签

上面的设置一个变量是不是有点麻烦呢?于是django提供了一个有用的函数assignment_tag,这个函数和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 ''

parser.parse()接受一个元组的块标签,返回一个django.template.NodeList的实例,这是实例是一个Node对象的列表,包含 解析器在碰到 元组里任何一个块标签之前 碰到的所有的Node对象。比如

nodelist = parser.parse(('endcomment',))会返回{% comment %}和{% endcomment %}标签之间的所有节点,不包含{% comment %}和{% endcomment %}

在parser.parse()被调用之后,解析器还没有解析{% endcomment %},所以需要调用parser.delete_first_token()

由于comment成对标签不必返回任何内容,所以CommentNode.render()仅仅返回一个空串

如果你的成对标签需要返回内容,可以参考下面这个例子,我们以{% upper %}为例子:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}
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()

如果还想了解更多的复杂的例子,你可以去看一下djang/template/defaulttags.py里面的内容,看看{% if %}{% endif %}这些标签是怎么实现的