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 %}這些標簽是怎么實現的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM