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

  • 相关阅读:
    jsp 内置对象二
    jsp 内置对象(一)
    jsp04状态管理
    jsp03( javabeans)
    jsp05 指令与动作
    Maven搭建SpringMVC + SpringJDBC项目详解
    java 面向对象
    java 面向对象 2
    javaScript 进阶篇
    NSSet、NSMutableSet、NSOrderedSet、NSMutableOrderedSet
  • 原文地址:https://www.cnblogs.com/qwj-sysu/p/4246605.html
Copyright © 2020-2023  润新知