Django搭建个人博客:自定义模板过滤器和标签

现在我们已经很熟悉Django的MTV模式了。模板(template)负责如何去展示数据,而视图(view)负责筛选出正确的数据。因此通常来说逻辑都是放到视图中的,但模板也需要一些和表示相关的逻辑:比如循环展示(如{% for ... %})、或者以某种特定格式输出(如{{ ...|date:'Y-m-d' }})等,这些功能都是靠模板的过滤器(filters)标签(tags)实现的。

Django的模板语言包含了很多内置的过滤器和标签,设计目的是满足应用需要占位逻辑需求。但有的时候这些通用的功能满足不了你的某些需求,这时候就需要自定义过滤器和标签来实现了。

前置条件

要在Django中使用模板过滤器或标签,就首先得注册它们。

注册方法如下:

  • 在APP中新建名为templatetags的目录(方便起见,教程选择了article这个APP)
  • 在此目录中新建名为__init__.py的空文件,使得此目录被视作一个Python的包
  • 在此目录中新建python文件(比如my_filters_and_tags.py),就可以在里面愉快的写代码啦

完成后的目录结构如下:

article/
    __init__.py
    views.py
    models.py
    # 新增目录
    templatetags/
        __init__.py  # 空文件
        my_filters_and_tags.py  # 即将写代码的地方
    ...

请注意

  • 目录必须位于已注册的APP中,这是出于安全性的考虑
  • 新建目录后,必须手动重启服务器,里面的过滤器和标签才能生效

前置条件就完成了,接下来我们看看如何写一个模板过滤器。

模板过滤器

过滤器filter的表现形式为紧跟在上下文后面的管道符|,管道符后面是filter的名称:{{ ...|filter_name }}。有的filter还可以带有参数:{{ ...|filter_name:var }}

注意过滤器名称的冒号后面不能有空格。

filter这个名字可能会让你误认为它只是用来筛选某些特定数据的,但实际上它远不止这点功能。它可以改变上下文的最终展示效果,也可以将上下文通过运算输出为特定的值。

小试牛刀

要成为一个可用的filter,文件中必须包含一个名为 register 的模块级变量,它是一个 template.Library 实例,所有的filters均在其中注册。所以在my_filter_and_tags.py文件中输入以下内容:

article/templatetags/my_filter_and_tags.py

from django import template
register = template.Library()

接下来就可以像写普通的Python函数一样写过滤器了:

article/templatetags/my_filter_and_tags.py

from django import template
register = template.Library()

@register.filter(name='transfer')
def transfer(value, arg):
    """将输出强制转换为字符串 arg """
    return arg

@register.filter()
def lower(value):
    """将字符串转换为小写字符"""
    return value.lower()
  • filter可以通过装饰器进行注册。若注册装饰器中携带了name参数,则其值为此filter的名称;若未携带,则函数名就是filter的名称。
  • filter必须是有一到两个参数的Python函数。第一个参数是上下文本身,第二个参数则由filter提供。举个栗子,在过滤器 {{ var|foo:"bar" }} 中,变量 var 为第一个参数,变量 bar 则作为第二个参数。

调用这些filter的方法是在模板文件中用{% load ... %}将filter文件的名称加载进去,像这样:

# 任意模板文件中

{% load my_filters_and_tags %}

{{ 'ABC'|transfer:'cool' }}  # 输出:'cool'
{{ 'ABC'|lower }}  # 输出: 'abc'

更人性化的时间

了解完filter的使用方法后,下面来写点更实用的功能。

对人类这种生物来说,相对时间通常比绝对时间要更容易阅读。发表于 3天前可以轻易得知此文章刚发表不久;而发表于 2019年8月10日你还得想想今天到底几号来着。

因此写一个显示相对日期的time_since_zh过滤器:

article/templatetags/my_filter_and_tags.py

...
from django.utils import timezone
import math

# 获取相对时间
@register.filter(name='timesince_zh')
def time_since_zh(value):
    now = timezone.now()
    diff = now - value

    if diff.days == 0 and diff.seconds >= 0 and diff.seconds < 60:
        return '刚刚'

    if diff.days == 0 and diff.seconds >= 60 and diff.seconds < 3600:
        return str(math.floor(diff.seconds / 60)) + "分钟前"

    if diff.days == 0 and diff.seconds >= 3600 and diff.seconds < 86400:
        return str(math.floor(diff.seconds / 3600)) + "小时前"

    if diff.days >= 1 and diff.days < 30:
        return str(diff.days) + "天前"

    if diff.days >= 30 and diff.days < 365:
        return str(math.floor(diff.days / 30)) + "个月前"

    if diff.days >= 365:
        return str(math.floor(diff.days / 365)) + "年前"

代码功能很简单,就是将文章发布时间和当前时间作比较,然后返回适当的字符串。

修改文章列表模板文件中与发布时间相关的代码,把刚写的filter用上:

templates/article/list.html

...
{% load my_filters_and_tags %}

...

<!-- 旧代码
{{ article.created|date:'Y-m-d' }}
-->

<!-- 新代码 -->
{{ article.created|timesince_zh }}

...

效果如下:

image

实际上Django内置了一个timesince过滤器,只不过显示日期是英文的,不够友好。

模板标签

模板标签(tag)比过滤器更复杂,功能也更强大。

标签tag的表现形式为{% tag_name ... %},比如我们非常熟悉的内置标签{% url ... %}{% static ... %}等。如果内置标签满足不了你的需求,Django 提供了很多快捷方式,简化了编写绝大多数类型的标签过程。

简单标签

simple_tag就是最重要的标签类型。标签的注册方法跟过滤器非常类似:

@register.simple_tag
def change_http_to_https(url):
    new_url = url.replace('http://', 'https://')
    return new_url

调用时同样记得在模板文件中用{% load... %}引入。用法你应该猜得到:{% change_http_to_https ... %},这个标签的作用是将http链接替换为https链接。

用 Django-allauth 进行微博登录,默认返回的用户头像是 http 链接(虽然微博有 https 版本的头像)。如果你的站点已经升级为 https 了,又不想花时间去研究微博的接口,那么这个标签就可以派上用场了。

顺带一说, Django-allauth 第三方登录的头像 url 保存在 User.socialaccount_set.all.0.get_avatar_url 中。

下面这个例子可以返回指定格式的时间字符串:

import datetime

@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

调用时你可以将其保存为模板变量,以便你在期望的位置多次调用:

{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>
<p>Again, the time is {{ the_time }}.</p>

模板标签也可以访问当前的上下文,只需要在注册标签时传入takes_context参数:

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

注意,第一个参数必须是context

与过滤器不同的是,标签可以接受任意数量的位置或关键字参数。例如:

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return ...

在模板中调用时,任意数量的、以空格分隔的参数会被传递给模板标签。与 Python 中类似,关键字参数的赋值使用等号("="),且必须在位置参数后提供:

{% my_tag 123 "abcd" book.title warning=message profile=user.profile %}

包含标签

包含标签可以让另一个模板为当前模板渲染数据。听起来比较拗口,还是通过例子来理解。

假设现在有一个需求,是要在文章详情页面中,显示所有相关评论的发布时间。因此在my_filter_and_tags.py中写入:

my_filter_and_tags.py

...

@register.inclusion_tag('article/tag_list.html')
def show_comments_pub_time(article):
    """显示文章评论的发布时间"""
    comments = article.comments.all()
    return {'comments': comments}

函数传入的参数可以是模板中的上下文变量。函数体内部取得了所有相关评论的查询集,然后把结果comments返回。注意返回结果是进入到tag_list.html这个模板中去了,因此新建它并写入:

templates/article/tag_list.html

<ul>
{% for comment in comments %}
    <li> {{ comment.created }} </li>
{% endfor %}
</ul>

然后在文章详情页面的模板中,随便找一个位置写入:

templates/article/detail.html

...
{% load my_filters_and_tags %}
...
{% show_comments_pub_time article %}

刷新详情页面,顺利的话就能看到所有评论的发表时间都展示出来了。

包含标签的另一个应用场景就是各种按钮了。有的按钮看上去长得都差不多,但是根据页面不同会有不同的功能,这时候也可以用包含标签来实现。

总之,包含标签可以将常用的模板代码打包成小组件,方便重复利用。

目前的博客项目中暂时还用不到包含标签,所以放心的删除上面的代码吧。

总结

模板过滤器和标签,可以完成和表示相关的逻辑,从而使视图专注于业务核心逻辑,有利于组件化和逻辑分离。随着学习的深入,会逐渐体会到它的重要性。

本章对它们做了入门介绍,以便读者对其有初步的概念。更详细的解释请进一步阅读Django官方文档


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容