Django 的模板语言自带了丰富的标签和过滤器,能满足常见的表现逻辑需求。
但有时候这些内建的标签和过滤 器可能缺少我们需要的功能,这时候我们可以扩展模板引擎,使用 Python 自定义标签和过滤器,然后使用 {% load %} 标签加载,让自定义的标签和过滤器可在模板中使用。
自定义的模板标签和过滤器必须放在一个 Django 应用中。如果与现有应用有关,可以放在现有应用中;否则,应该专门创建一个应用存放。应用中应该有个 templatetags 目录,与 models.py、views.py 等文件放在 同一级。
在 templatetags 目录中创建两个空文件:__init__.py(告诉 Python 这是包含 Python 代码的包)和存放自定 义标签(过滤器)的文件。后者的名称是加载标签所用的名称。
例如,自定义的标签(过滤器)保存在 my_tags.py 文件中,那么在模板中要这么加载:
{% load my_tags %}
然后整个项目的目录结构应该如下:
{% load %} 标签在 INSTALLED_APPS 设置中查找包含自定义标签(过滤器)的应用,而且只从应用中加载模板库。这是一个安全特性,以便在同一台电脑中存贮多个模板库的 Python 代码,而不让每个 Django 实例都能访问。
如果模板库不与任何模型或视图绑定,Django 应用中可以只有 templatetags 包。事实上,这也相当常见。 templatetags 包中的模块数量不限。记住,{% load %} 语句加载的是指定的 Python 模块,而不是应用。
创建存放标签(过滤器)的 Python 模块之后,剩下的就是编写 Python 代码了。代码怎么写,取决于定义的是过滤器还是标签。一个有效的标签库必须有一个名为 register 的模块层变量,其值是 template.Library 的 实例。
自定义的过滤器其实就是普通的 Python 函数,接受一个或多个参数:
1). 变量的值(输入),不一定是字符串。
2). 参数的值,可以有默认值,也可以留空。 例如,对 {{ var|foo:"bar" }} 来说,传给 foo 过滤器的变量是 var,参数是 "bar"。
注意:因为模板语言没有提供异常处理功能,所以模板过滤器抛出的异常会以服务器错误体现出来。鉴于此,模板过滤器应该避免抛出异常,而是回落到其他合理的值。如果输入明显有问题,最好还是抛出异常,以免深埋缺陷。
定义一个过滤器的格式如下:
def cut(value, arg):
"""注释内容"""
一些代码
return value.replace(arg, '')
定义好过滤器之后,要使用 Library 实例注册,让 Django 的模板语言知道它的存在:
register.filter('cut', cut)
Library.filter() 有两个参数: 1. 过滤器的名称,一个字符串。 2. 负责处理过滤器的函数,一个 Python 函数(不是函数名称的字符串形式)。
register.filter() 也可以作为装饰器使用:
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
我们下面做一个自定义过滤器,用来过滤学生的分数,低于某个值就显示不及格。
先在标签里面把逻辑写好,其实就是写一个函数jige
from django import template
register = template.Library()
@register.filter(name='jige') #可以装饰器形式调用,也可以register.filter('jige',jige)
def jige(value,arg):
if value
然后在class3.html中引用这个标签
{% extends "base.html" %}
{% block title %}武侠三班的小窝{% endblock %}
{% load my_tags %}
{% block main %}
通知
本次中期比武,{{ student_list.0.name }}获得了第一名
获得优秀的同学还有{{student_list.1.name}}、{{student_list.2.name}}、{{student_list.3.name}}等
希望其他同学以他们为榜样,苦练武功,争取下次比武取得好成绩
{% for s in student_list %}
{% if s.sex == 'male' %}
// 设定80分以下为不及格
- {{ s.name }}大侠的成绩为{{s.score|jige:90}}
{% else %}
- {{ s.name }}女侠的成绩为{{s.score|jige:80}}
{% endif %}
{% endfor %}
落款:你们尊敬的老师
{{ teacher }}
日期: {{ now|date:"D d M Y" }}
{% endblock %}
为了更好地显示效果,我们把之前views里面的 addstudent 函数改造一下,初始化一些学生的数据:
def addstudent(request):
Student.objects.all().delete()
s1=Student(id=1,name='杨过',age=18,sex='male',score=98)
s1.save()
s2=Student(id=2,name='小龙女',age=20,sex='female',score=86)
s2.save()
s3=Student(id=3,name='黄药师',age=18,sex='male',score=95)
s3.save()
s4=Student(id=4,name='洪七公',age=20,sex='male',score=86)
s4.save()
s5=Student(id=5,name='郭靖',age=18,sex='male',score=92)
s5.save()
s6=Student(id=6,name='黄蓉',age=20,sex='female',score=86)
s6.save()
s7=Student(id=7,name='周芷若',age=17,sex='female',score=76)
s7.save()
s8=Student(id=8,name='欧阳锋',age=20,sex='male',score=96)
s8.save()
s9=Student(id=9,name='丘处机',age=18,sex='male',score=75)
s9.save()
s10=Student(id=10,name='郭襄',age=20,sex='female',score=86)
s10.save()
s11=Student.objects.create(id=11,name='胡一刀',age=16,sex='male',score=99)
return HttpResponse('{}、{}、{}、{}、{}、{}、{}、{}、{}、{}、{}等同学已经添加成功'.format(s1.name,s2.name,s3.name,s4.name,s5.name,s6.name,s7.name,s8.name,s9.name,s10.name,s11.name))
再把classnotice函数改造一下:
def classnotice(request):
now=datetime.datetime.now()
context={}
context['student_list']=Student.objects.all().order_by('-score')
context['teacher']='王重阳'
context['now']=now
return render(request,'class3.html',context=context,status=200)
好了,我们访问网站,先访问 http://127.0.0.1:8000/add/,更新学生数据
然后访问 http://127.0.0.1:8000/notice,得到如下结果:
可以看到分数低于80的会显示为“不及格”。
如果模板过滤器期望第一个参数是字符串,应该使用 stringfilter 装饰器。这样,对象在传给过滤器之前会 先转换成字符串值。
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
@register.filter
@stringfilter
def lower(value):
return value.lower()
这里,可以把整数传给过滤器,不会抛出 AttributeError(因为整数没有 lower() 方法)。
自定义处理 datetime 对象的过滤器时,注册时通常要把 expects_localtime 旗标设为 True:
@register.filter(expects_localtime=True)
def businesshours(value):
try:
return 9 <= value.hour < 17
except AttributeError:
return ''
这样设定之后,如果过滤器的第一个参数是涉及时区的日期时间,根据模板中的时区转换规则,必要时 Django 会先把它转换成当前时区,然后再传给过滤器。
标签要比过滤器复杂一些,Django 提供了一些快捷方式,简化了多数标签类型的编写。
很多模板标签接受几个参数(字符串或模板变量),对输入参数和一些外部信息做些处理之后返回一个结果。Django 提供了一个辅助函数,simple_tag。它是 django.template.Library 中的一个方法,其参数是一个接受任意个参数的函数,把它包装在 render 函数中之后,再做些前述的必要处理,最后注册到模板系统中。
比如说我们做一个实现除法的标签(Django其实没有默认的除法标签),在my_tags.py中写一个标签函数chufa:
@register.simple_tag
def chufa(a,b):
c=a/b
return c
然后去class3.html中调用这个标签,实现把成绩从百分制换算成10分制:
{% extends "base.html" %}
{% block title %}武侠三班的小窝{% endblock %}
{% load my_tags %}
{% block main %}
通知
本次中期比武,{{ student_list.0.name }}获得了第一名
获得优秀的同学还有{{student_list.1.name}}、{{student_list.2.name}}、{{student_list.3.name}}等
希望其他同学以他们为榜样,苦练武功,争取下次比武取得好成绩
{% for s in student_list %}
{% if s.sex == 'male' %}
- {{ s.name }}大侠的成绩为 {% chufa s.score 10 %}
{% else %}
- {{ s.name }}女侠的成绩为 {% chufa s.score 10 %}
{% endif %}
{% endfor %}
落款:你们尊敬的老师
{{ teacher }}
日期: {{ now|date:"D d M Y" }}
{% endblock %}
然后访问 http://127.0.0.1:8000/notice/ , 看看效果:
有时候我们希望 simple_tag() 的运算结果储存起来,以供别的地方使用,而不直接输出。
只要在标签中用 as 参数把结果储存为一个变量即可
比如前面所举的 chufa 函数可以这样引用:
{% for s in student_list %}
{% if s.sex == 'male' %}
- {{ s.name }}大侠的成绩为 {% chufa s.score 10 %}
{% else %}
{% chufa s.score 10 as jg %}
- {{ s.name }}女侠的成绩为 {{jg}}
{% endif %}
{% endfor %}
这样 jg 这个变量就储存了chufa 标签计算的结果,可以在网页任何地方用{{jg}}把它引用出来。
模板系统的运作分为两步:编译和渲染。因此,自定义模板要指定如何编译和渲染。
Django 编译模板时,把原始模板分解为一个个“节点”。节点是 django.template.Node 实例,有一个 render() 方法。编译好的模板其实就是一些 Node 对象。 在编译好的模板对象上调用 render() 方法时,模板在节点列表中的各个 Node 对象上调用 render() 方法,并传入指定的上下文。最后,把各个节点的输出拼接在一起,组成模板的输出。因此,自定义模板标签时,要 指定如何把原始模板转换成 Node 对象(编译),并且指定节点的 render() 方法要做什么(渲染)。
我们这次从头做一个乘法的标签。
模板解析器遇到模板标签时,调用一个 Python 函数,并且传入标签的内容和解析器对象自身。这个函数负责 根据标签的内容返回一个 Node 实例。
我们先写一个实现乘法的函数,假设我们希望在HTML中这样使用它
{% chengfa numA numB %}
我们写一个编译函数,用于编译原始模板文件:
def chengfa(parser, token):
try:
tag_name,numA,numB = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires 2 argument" % token.contents.split()[0])
return chengfaNode(numA,numB)
这里最终返回三个值,分别是tag标签名称,参数1 和 参数2
这里有一些注意事项:
• parser 是模板解析器对象。这里用不到。
• token.contents 是标签的原始内容字符串。这里是 'chengfa s.score 10'。
• token.split_contents() 在空格处把标签的名称和参数分开,不过放在引号内的字符串保持不动。token.contents.split() 简单一些,但是不够可靠,它会在所有空格处拆分,包括引号内的空格。最好始终使用 token.split_contents()。
• 遇到句法错误时,这个函数会抛出 django.template.TemplateSyntaxError,并且提供有用的消息。
• TemplateSyntaxError 异常用到了 tag_name 变量。别在错误消息中硬编码标签的名称,避免标签的名称与函数耦合。token.contents.split()[0] 的值始终是标签的名称,即使标签没有参数。
• 这个函数返回一个 chengfaNode 对象,它具有节点需要知道的关于这个标签的一切信息。这里是返回两个乘数 numA 和 numB 作为chengfaNode的参数
自定义标签的第二步是定义一个具有 render() 方法的 Node 子类。
我们继续编写一个chengfaNode类:
class chengfaNode(template.Node):
def __init__(self,a,b):
self.a=a
self.b=b
def render(self, context):
numa = int(a)
numb = int(b)
return numa*numb
注意事项:
• __init__() 从chengfa() 中获取 numA 和 numB。节点的选项或参数都通过 __init__() 传递。
• 具体的工作在 render() 方法中做。
• 一般来说,render() 应该静默问题,尤其是在 DEBUG 和 TEMPLATE_DEBUG 设为 False 的生产环境。然而,有些情况下,尤其是 TEMPLATE_DEBUG 设为 True 时,render() 方法可以抛出异常,以便调试。例如,有几个内置的标签在收到错误的参数数量或类型时抛出 django.template.TemplateSyntaxError。 这种编译和渲染的解耦得到的是高效的模板系统,因为无需多次解析一个模板就可以渲染多个上下文。
但是上面这个类有个问题,因为token.contents解析得到的是字符串,而实际上‘s.score’是一个变量,如果让 它 直接乘以10 肯定会报错,那么怎么把模板变量传给标签呢?
django.template 包中有一个 Variable() 类可以实现这个功能。
Variable() 类的用法很简单,先使用变量的名称实例化,然后调用 variable.resolve(context)。
我们把上面chengfaNode的类改写一下:
class chengfaNode(template.Node):
def __init__(self,a,b):
self.a=template.Variable(a)
self.b=template.Variable(b)
def render(self, context):
numa = int(self.a.resolve(context))
numb = int(self.b.resolve(context))
return numa*numb
这样的话不管输入的 a,b 是变量还是具体的数值(字符串形式)都可以正确获取到值了。
上述示例只是输出值。一般来说,设定模板变量的标签比输出值的标签更灵活。这样,模板编写人便可以复用编码标签创建的值。若想在上下文中设定变量,在 render() 方法中像字典那样为上下文中的变量赋值。
下 面是 chengfaNode 的更新版本,设定 chengfa_result 模板变量,而不输出:
class chengfaNode(template.Node):
def __init__(self,a,b):
self.a=template.Variable(a)
self.b=template.Variable(b)
def render(self, context):
numa = int(self.a.resolve(context))
numb = int(self.b.resolve(context))
context['chengfa_result'] = numa * numb
return ''
注意,render() 方法返回了一个空字符串。render() 始终应该返回一个字符串。如果模板标签只设定变量, render() 应该返回一个空字符串。这个新版本的用法如下:
{% chengfa s.score 10 %}
成绩换算成千分制为 {{ chengfa_result }}
最后别忘了像之前那样注册标签,同样可以用两种方法:
A) register.tag('chengfa', chengfa)
tag() 方法有两个参数:
1. 模板标签的名称,一个字符串。如果留空,使用编译函数的名称。
2. 编译函数,一个 Python 函数(不是函数名称的字符串形式)。
B) 作为装饰器使用:
@register.tag(name="chengfa")
def chengfa(parser, token):
......
......
最后贴一下完整my_tags.py 代码 和访问效果
from django import template
register = template.Library()
@register.filter(name='jige')
def jige(value,arg):
if value
class3.html
{% extends "base.html" %}
{% block title %}武侠三班的小窝{% endblock %}
{% load my_tags %}
{% block main %}
通知
本次中期比武,{{ student_list.0.name }}获得了第一名
获得优秀的同学还有{{student_list.1.name}}、{{student_list.2.name}}、{{student_list.3.name}}等
希望其他同学以他们为榜样,苦练武功,争取下次比武取得好成绩
{% for s in student_list %}
{% if s.sex == 'male' %}
- {{ s.name }}大侠的成绩为 {% chengfa s.score 10 %}
{% else %}
{% chufa s.score 10 as jg %}
- {{ s.name }}女侠的成绩为 {{jg}}
{% endif %}
{% endfor %}
落款:你们尊敬的老师
{{ teacher }}
日期: {{ now|date:"D d M Y" }}
{% endblock %}
访问 http://127.0.0.1:8000/notice 效果: