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