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

 

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

 

1.创建一个模板库

使用模板过滤器的时候,直接把过滤器写在app里,

例如:在app里新建一个templatetags的文件夹,这个目录应当和 models.py 、 views.py 等处于同一层次。例如:

books/

   __init__.py

   models.py

   templatetags/

   views.py

在 templatetags 中创建两个空文件:一个 __init__.py (告诉Python这是一个包含了Python代码的包)和一个用来存放你自定义的标签/过滤器定义的文件。第二个文件的名字稍后将用来加载标签。例如,如果你的自定义标签/过滤器在一个叫作 poll_extras.py 的文件中,你需要在模板中写入如下内容: {% load poll_extras %}

{% load %} 标签检查 INSTALLED_APPS 中的设置,仅允许加载已安装的Django应用程序中的模板库。

要成为有效的标签库,模块必须包含一个模块级的变量: register ,这是一个 template.Library 的实例。这个 template.Library 实例是包含所有已注册的标签及过滤器的数据结构。因此,在模块的顶部位置插入下述代码:

from django import template

register = template.Library()

2. 自定义模板过滤器

自定义过滤器就是有一个或两个参数的Python函数::

例如,在过滤器 {{ var|foo:”bar” }} 中,过滤器 foo 会被传入变量 var 和参数 bar 的内容。

过滤器函数应该总有返回值,而且不能触发异常,它们都应该静静的失败。如果有一个错误发生,它们要么返回原始的输入字符串,要么返回空的字符串,无论哪个都可以。

1)这里是一些定义过滤器的例子:

def cut(value, arg):

   "Removes all values of arg from the given string"

return value.replace(arg, '')

def lower(value): # Only one argument.

   "Converts a string into all lowercase"

   return value.lower()

2)这里是一些如何使用过滤器的例子:

   {{ somevariable|cut:”0″ }}

3)下面是一个完整的模板库的例子,提供了一个cut 过滤器:

from django import template

register = template.Library()

@register.filter(name='cut') (装饰器)

def cut(value, arg):

   return value.replace(arg, '')

 

@register.filter

def lower(value):

   return value.lower()

   注意:1)在定义过滤器时,需要用Library 实例来注册它,这样就能通过Django的模板语言来使用了: Python 2.4或更新,可以通过上面的装饰器   实现,如果不使用 name参数,@register.filter那么Django将会使用函数名作为过滤器的名字

2)保证templatetags在已经INSTALLED_APPS里面

 

 

编写自定义模板标签

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

快速回顾

模板系统工作有两个流程:编译和渲染。去定义一个模板标签,你需要知道如何去编译和如何去渲染。当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 singleargument" % 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 inquotes" % 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 inquotes" % 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函数我们这里可以这样写

 

ef 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|lowerprofile=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 inclusiontag 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 invalidarguments" % 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 inquotes" % 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 %}这些标签是怎么实现的

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值