Python 学习笔记

高级自定义模板标签

2018-02-08  本文已影响5人  大爷的二舅

有时,自定义模板标签创建的基本功能是不够的。 别担心,Django可以让您完全访问从底层构建模板标签所需的内部组件。

快速概览

模板系统分两个步骤进行:编译和渲染。 要定义自定义模板标签,您需要指定编译的工作方式以及渲染的工作方式。 当Django编译模板时,它将原始模板文本分割成“节点”。 每个节点是django.template.Node的一个实例,并有一个render()方法。 编译好的模板就是一个Node对象列表。

当您在编译模板对象上调用render()时,模板将使用给定的上下文在其节点列表中的每个节点上调用render()。 结果全部连接在一起形成模板的输出。 因此,要定义一个自定义模板标签,可以指定原始模板标签如何转换为一个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:
  
      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])

笔记:

编写渲染器

编写自定义标签的第二步是定义一个具有render()方法的Node子类。 继续上面的例子,我们需要定义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)
自动转义注意事项

模板标签的输出不会通过自动转义过滤器自动运行。 但是,在编写模板标签时,还是应该记住一些事情。 如果模板的render()函数将结果存储在上下文变量中(而不是以字符串形式返回结果),则应该小心地在适当的时候调用mark_safe()。 当变量最终呈现时,它将受到当时有效的自动转义设置的影响,所以应该安全避免进一步转义的内容需要被标记为这样。

另外,如果您的模板标签为执行某个子呈现创建了一个新的上下文,请将auto-escape属性设置为当前上下文的值。 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% }块。

线程安全考虑

一旦节点被解析,其渲染方法可以被调用任意次数。 由于Django有时在多线程环境中运行,单个节点可能会同时呈现不同的上下文以响应两个单独的请求。

因此,确保您的模板标签是线程安全的是非常重要的。 为了确保你的模板标签是线程安全的,你不应该在节点上存储状态信息。 例如,
Django提供了一个内置的循环模板标签,它在每次渲染时在给定字符串列表中循环显示:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}>
        ...
    </tr>
{% 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. 线程1执行第一次循环迭代,CycleNode.render()返回'row1'
  2. 线程2执行第一次循环迭代,CycleNode.render()返回'row2'
  3. 线程1执行第二次循环迭代,CycleNode.render()返回'row1'
  4. 线程2执行第二次循环迭代,CycleNode.render()返回'row2'

CycleNode正在迭代,但它在全局迭代。 就线程1和线程2而言,它总是返回相同的值。 这显然不是我们想要的!

为了解决这个问题,Django提供了一个render_context,它与当前正在呈现的模板的上下文相关联。 render_context的行为就像一个Python字典,应该用来在render方法的调用之间存储Node状态。 让我们重构我们的CycleNode实现来使用render_context:

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)

请注意,将在整个生命周期中不会改变的全局信息作为属性存储是非常安全的。

在CycleNode的情况下,在实例化Node之后,cyclevars参数不会改变,所以我们不需要把它放在render_context中。 但是,当前正在呈现的模板(如CycleNode的当前迭代)所特有的状态信息应存储在render_context中。

注册标签

最后,按照上面“编写自定义模板过滤器”的说明,将模块的库实例注册到标签。 例:

register.tag('current_time', do_current_time)

tag()方法有两个参数:

  1. 模板标签的名称 - 一个字符串。 如果省略,编译函数的名称将被使用。

  2. 编译函数 - 一个Python函数(不是作为字符串的函数的名字)。

与过滤器注册一样,也可以将其用作装饰器:

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

@register.tag
def shout(parser, token):
    ...

如果你忽略名称参数,就像上面的第二个例子一样,Django将使用该函数的名字作为标签名称。

将模板变量传递给标签

虽然可以使用token.split_contents()将任意数量的参数传递给模板标记,但参数都将解压缩为字符串文本。 为了将动态内容(模板变量)作为参数传递给模板标签,需要做更多的工作。

虽然前面的例子已经把当前时间格式化为一个字符串并且返回了字符串,但是假设你想从一个对象传递一个DateTimeField并且使用date-time的模板标签格式:

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

最初,token.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])

您还必须更改渲染器以检索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" %}
<p>The time is {{ current_time }}.</p>
上下文中的变量范围

在上下文中设置的任何变量只能在分配模板的相同块中使用。 这种行为是故意的; 它为变量提供了一个范围,以便它们不会与其他块中的上下文发生冲突。

但是,CurrentTimeNode2存在问题:变量名称current_time是硬编码的。 这意味着您需要确保您的模板不会在其他任何地方使用{{current_time}},因为{%current_time%}将盲目地覆盖该变量的值。

一个更清洁的解决方案是使模板标签指定输出变量的名称,如下所示:

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

为此,您需要重构编译函数和节点
类,如下所示:

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。 最后,如果您只需要为自定义上下文更新模板标签使用简单的语法,则可能需要考虑使用上面介绍的分配标签快捷方式。

解析到另一个块标记

模板标签可以协同工作。 例如,标准的{%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()。 但是这一次,我们将生成的节点列表传递给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%}。

下一步是什么

继续本节的高级主题,下一章将介绍Django模型的高级用法。

上一篇下一篇

猜你喜欢

热点阅读