首页 > 代码库 > django “如何”系列4:如何编写自定义模板标签和过滤器
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:"0" }}
如果过滤器不接受参数,只需要这样写
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.filterdef lower(value): return value.lower()
stringfilter
如果你的模板过滤器只希望接受字符串作为第一个参数,那么你可以是用stringfilter装饰器,这样的话,在传参进你的函数之前,该参数的值会被转换成对应字符串值
from django import templatefrom django.template.defaultfilters import stringfilterregister = template.Library()@register.filter@stringfilterdef 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_escapefrom 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 templatedef 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 templateimport datetimeclass 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.tagdef 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 templatedef 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_tagdef 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_tagdef 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)
#第一个参数必须为contextdef 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 redef 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 %}这些标签是怎么实现的
django “如何”系列4:如何编写自定义模板标签和过滤器