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.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 %}这些标签是怎么实现的

时间: 2024-11-07 08:43:17

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

Django 自定义模板标签和过滤器

1.创建一个模板库 使用模板过滤器的时候,直接把过滤器写在app里,例如:在app里新建一个templatetags的文件夹,这个目录应当和 models.py . views.py 等处于同一层次.例如: books/     __init__.py     models.py     templatetags/     views.py 在 templatetags 中创建两个空文件:一个 __init__.py (告诉Python这是一个包含了Python代码的包)和一个用来存放你自定义的

Django自定义模板标签和过滤器

inclusion_tag() 在app中新建一个templatetags包(名字固定,不能变,只能是这个), 和views.py.models.py等文件处于同一级别目录下. 这是一个包!不要忘记创建__init__.py文件以使得该目录可以作为Python的包 1.首先,编写Python函数,templatetags/my_tags.py from django import template register = template.Library() register = Library(

自定义模板标签和过滤器

1.在settings的INSTALLED_APPS下配置app,否则找不到自定义的simple_tag 2.在app中创建名为tamplatetags的模块,注意:模块名只能是templatetags 3.在模块下创建任意.py文件,如my_tags.py from django import template from django.utils.safestring import mark_safe register = template.Library() #register的名字是固定的

Django1.3 模板标签和过滤器

内建标签 autoescape 控制HTML转义,参数是:on 或 off.效果和使用safe或escape过滤器相同. {% autoescape on %} {{ body }} {% endautoescape %} block 定义一个能被子模板覆盖的区块. comment 模板引擎会忽略掉 {% comment %} 和 {% endcomment %} 之间的所有内容 csrf_token 防止跨站请求伪造. <form action="." method="

Django之博客系统:自定义模板标签

Django提供了很多内置的模板标签比如{% if %}或者{% block %}Django也允许你创建自己的模板标签(template tags)来执行自定义的动作.当你需要在你的模板中添加功能而Django模板标签(template tags)的核心设置无法提供此功能的时候,自定义模板标签会非常方便 Django提供了以下帮助函数(functions)来允许你以一种简单的方式创建自己的模板标签(template tags): simple_tag:处理数据并返回一个字符串(string)

Django 自定义模版标签和过滤器

实现自定义过滤器 1. 创建register变量 在你的模块文件中,你必须首先创建一个全局register变量,它是用来注册你自定义标签和过滤器的, 你需要在你的python文件的开始处,插入几下代码: from django import templateregister = template.Library() 2. 定义过滤器函数 自定义的过滤器就是一个带1,2个参数的python函数,一个参数放变量值,一个用来放选项值. 比如{{ var|remove:"bar" }}, va

Django 内置模板标签和过滤器

一.内置模板标签 语法:{%  %} autoescape : 是否转义,on或off作为参数,并确定自动转义是否在块内有效.该块以endautoescape结束 {% autoescape on %} {{ body }} {% endautoescape %} block : 定义 可以被子模块覆盖的块,具体的参阅Django模板与继承 comment : 忽略comment之间的所有内容 查看网页源码也不会存在 csrf_token : 此标记用于csrf保护,具体的参阅Django跨站请

Django内建模版标签和过滤器

第四章列出了许多的常用内建模板标签和过滤器.然而,Django自带了更多的内建模板标签及过滤器.这章附录列出了截止到编写本书时,Django所包含的各个内建模板标签和过滤器,但是,新的标签是会被定期地加入的. 对于提供的标签和过滤器,最好的参考就是直接进入你的管理界面.Django的管理界面包含了一份针对当前站点的所有标签和过滤器的完整参考.想看到它的话,进入你的管理界面,单击右上角的Documentation(文档)链接. 内建文档中的“标签和过滤器”小节阐述了所有内建标签(事实上,本附录中的

django特殊的标签和过滤器

国际化标签和过滤器 Django还提供了一些模板标签和过滤器,用以控制模板中国际化的每个方面.它们允许对翻译,格式化和时区转换进行粒度控制. 1. i18n 此标签允许在模板中指定可翻译文本.要启用它,请将USE_I18N设置为True,然后加载{% load i18n %}. 2. l10n 此标签提供对模板的本地化控制,只需要使用{% load l10n %}.通常将USE_L10N设置为True,以便本地化默认处于活动状态. 3. tz 此标签对模板中的时区进行控制. 像l10n,只需要使