雖然大多數和Django模板語言的交互都是模板作者的工作,但你可能想定制和擴展模板引擎,讓它做一些它不能做的事情,或者是以其他方式讓你的工作更輕松。
本章深入探討Django的模板系統。 如果你想擴展模板系統或者只是對它的工作原理感覺到好奇,本章涉及了你需要了解的東西。 它也包含一個自動轉意特征,如果你繼續使用django,隨著時間的推移你一定會注意這個安全考慮。
如果你想把Django的模版系統作為另外一個應用程序的一部分(就是說,僅使用Django的模板系統而不使用Django框架的其他部分),那你一定要讀一下“配置獨立模式下的模版系統”這一節。
## 模板語言回顧
首先,讓我們快速回顧一下第四章介紹的若干專業術語:
> _模板_?是一個純文本文件,或是一個用Django模板語言標記過的普通的Python字符串。 模板可以包含模板標簽和變量。
>
> _模板標簽_?是在一個模板里面起作用的的標記。 這個定義故意搞得模糊不清。 例如,一個模版標簽能夠產生作為控制結構的內容(一個?if語句或for?循環), 可以獲取數據庫內容,或者訪問其他的模板標簽。
>
> 區塊標簽被?{%?和?%}?包圍:
~~~
{% if is_logged_in %}
Thanks for logging in!
{% else %}
Please log in.
{% endif %}
~~~
> _變量_?是一個在模板里用來輸出值的標記。
>
> 變量標簽被?{{?和?}}?包圍:
~~~
My first name is {{ first_name }}. My last name is {{ last_name }}.
~~~
> _context_?是一個傳遞給模板的名稱到值的映射(類似Python字典)。
>
> 模板?_渲染_?就是是通過從context獲取值來替換模板中變量并執行所有的模板標簽。
關于這些基本概念更詳細的內容,請參考第四章。
本章的其余部分討論了擴展模板引擎的方法。 首先,我們快速的看一下第四章遺留的內容。
## RequestContext和Context處理器
你需要一段context來解析模板。 一般情況下,這是一個?django.template.Context?的實例,不過在Django中還可以用一個特殊的子類,?django.template.RequestContext?,這個用起來稍微有些不同。?RequestContext默認地在模板context中加入了一些變量,如?HttpRequest?對象或當前登錄用戶的相關信息。
當你不想在一系例模板中都明確指定一些相同的變量時,你應該使用?RequestContext?。 例如,考慮這兩個視圖:
~~~
from django.template import loader, Context
def view_1(request):
# ...
t = loader.get_template('template1.html')
c = Context({
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR'],
'message': 'I am view 1.'
})
return t.render(c)
def view_2(request):
# ...
t = loader.get_template('template2.html')
c = Context({
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR'],
'message': 'I am the second view.'
})
return t.render(c)
~~~
(注意,在這些例子中,我們故意?_不_?使用?render_to_response()?這個快捷方法,而選擇手動載入模板,手動構造context對象然后渲染模板。 是為了能夠清晰的說明所有步驟。)
每個視圖都給模板傳入了三個相同的變量:app、user和ip_address。 如果我們把這些冗余去掉會不會更好?
創建?RequestContext?和?**context處理器**?就是為了解決這個問題。 Context處理器允許你設置一些變量,它們會在每個context中自動被設置好,而不必每次調用?render_to_response()?時都指定。 要點就是,當你渲染模板時,你要用?RequestContext?而不是?Context?。
最直接的做法是用context處理器來創建一些處理器并傳遞給?RequestContext?。上面的例子可以用context processors改寫如下:
~~~
from django.template import loader, RequestContext
def custom_proc(request):
"A context processor that provides 'app', 'user' and 'ip_address'."
return {
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR']
}
def view_1(request):
# ...
t = loader.get_template('template1.html')
c = RequestContext(request, {'message': 'I am view 1.'},
processors=[custom_proc])
return t.render(c)
def view_2(request):
# ...
t = loader.get_template('template2.html')
c = RequestContext(request, {'message': 'I am the second view.'},
processors=[custom_proc])
return t.render(c)
~~~
我們來通讀一下代碼:
* 首先,我們定義一個函數?custom_proc?。這是一個context處理器,它接收一個?HttpRequest?對象,然后返回一個字典,這個字典中包含了可以在模板context中使用的變量。 它就做了這么多。
* 我們在這兩個視圖函數中用?RequestContext?代替了?Context?。在context對象的構建上有兩個不同點。 一,?RequestContext?的第一個參數需要傳遞一個?HttpRequest?對象,就是傳遞給視圖函數的第一個參數(?request?)。二,?RequestContext?有一個可選的參數?processors?,這是一個包含context處理器函數的列表或者元組。 在這里,我們傳遞了我們之前定義的處理器函數?curstom_proc?。
* 每個視圖的context結構里不再包含?app?、?user?、?ip_address?等變量,因為這些由?custom_proc?函數提供了。
* 每個視圖?_仍然_?具有很大的靈活性,可以引入我們需要的任何模板變量。 在這個例子中,?message?模板變量在每個視圖中都不一樣。
在第四章,我們介紹了?render_to_response()?這個快捷方式,它可以簡化調用?loader.get_template()?,然后創建一個?Context?對象,最后再調用模板對象的?render()過程。 為了講解context處理器底層是如何工作的,在上面的例子中我們沒有使用?render_to_response()?。但是建議選擇?render_to_response()?作為context的處理器。這就需要用到context_instance參數:
~~~
from django.shortcuts import render_to_response
from django.template import RequestContext
def custom_proc(request):
"A context processor that provides 'app', 'user' and 'ip_address'."
return {
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR']
}
def view_1(request):
# ...
return render_to_response('template1.html',
{'message': 'I am view 1.'},
context_instance=RequestContext(request, processors=[custom_proc]))
def view_2(request):
# ...
return render_to_response('template2.html',
{'message': 'I am the second view.'},
context_instance=RequestContext(request, processors=[custom_proc]))
~~~
在這,我們將每個視圖的模板渲染代碼寫成了一個單行。
雖然這是一種改進,但是,請考慮一下這段代碼的簡潔性,我們現在不得不承認的是在?_另外_?一方面有些過分了。 我們以代碼冗余(在?processors?調用中)的代價消除了數據上的冗余(我們的模板變量)。 由于你不得不一直鍵入?processors?,所以使用context處理器并沒有減少太多的輸入量。
Django因此提供對?_全局_?context處理器的支持。?TEMPLATE_CONTEXT_PROCESSORS?指定了哪些context?processors_總是_默認被使用。這樣就省去了每次使用?RequestContext?都指定?processors?的麻煩。
默認情況下,?TEMPLATE_CONTEXT_PROCESSORS?設置如下:
~~~
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
)
~~~
這個設置項是一個可調用函數的元組,其中的每個函數使用了和上文中我們的?custom_proc?相同的接口,它們以request對象作為參數,返回一個會被合并傳給context的字典: 接收一個request對象作為參數,返回一個包含了將被合并到context中的項的字典。
每個處理器將會按照順序應用。 也就是說如果你在第一個處理器里面向context添加了一個變量,而第二個處理器添加了同樣名字的變量,那么第二個將會覆蓋第一個。
Django提供了幾個簡單的context處理器,有些在默認情況下被啟用的。
### django.core.context_processors.auth
如果?TEMPLATE_CONTEXT_PROCESSORS?包含了這個處理器,那么每個?RequestContext?將包含這些變量:
* user?:一個?django.contrib.auth.models.User?實例,描述了當前登錄用戶(或者一個?AnonymousUser?實例,如果客戶端沒有登錄)。
* messages?:一個當前登錄用戶的消息列表(字符串)。 在后臺,對每一個請求,這個變量都調用request.user.get_and_delete_messages()?方法。 這個方法收集用戶的消息然后把它們從數據庫中刪除。
* perms?:?django.core.context_processors.PermWrapper?的一個實例,包含了當前登錄用戶有哪些權限。
關于users、permissions和messages的更多內容請參考第14章。
### django.core.context_processors.debug
這個處理器把調試信息發送到模板層。 如果TEMPLATE_CONTEXT_PROCESSORS包含這個處理器,每一個RequestContext將包含這些變量:
* debug?:你設置的?DEBUG?的值(?True?或?False?)。你可以在模板里面用這個變量測試是否處在debug模式下。
* sql_queries?:包含類似于?[``](http://docs.30c.org/djangobook2/chapter09/index.html#id3){‘sql’: …, ‘time’: `` 的字典的一個列表, 記錄了這個請求期間的每個SQL查詢以及查詢所耗費的時間。 這個列表是按照請求順序進行排列的。
由于調試信息比較敏感,所以這個context處理器只有當同時滿足下面兩個條件的時候才有效:
* DEBUG?參數設置為?True?。
* 請求的ip應該包含在?INTERNAL_IPS?的設置里面。
細心的讀者可能會注意到debug模板變量的值永遠不可能為False,因為如果DEBUG是False,那么debug模板變量一開始就不會被RequestContext所包含。
### django.core.context_processors.i18n
如果這個處理器啟用,每個?RequestContext?將包含下面的變量:
* LANGUAGES?:?LANGUAGES?選項的值。
* LANGUAGE_CODE?:如果?request.LANGUAGE_CODE?存在,就等于它;否則,等同于?LANGUAGE_CODE?設置。
附錄E提供了有關這兩個設置的更多的信息。
### django.core.context_processors.request
如果啟用這個處理器,每個?RequestContext?將包含變量?request?, 也就是當前的?HttpRequest?對象。 注意這個處理器默認是不啟用的,你需要激活它。
如果你發現你的模板需要訪問當前的HttpRequest你就需要使用它:
~~~
{{ request.REMOTE_ADDR }}
~~~
### 寫Context處理器的一些建議
編寫處理器的一些建議:
* 使每個context處理器完成盡可能小的功能。 使用多個處理器是很容易的,所以你可以根據邏輯塊來分解功能以便將來復用。
* 要注意?TEMPLATE_CONTEXT_PROCESSORS?里的context processor 將會在基于這個settings.py的_每個_?模板中有效,所以變量的命名不要和模板的變量沖突。 變量名是大小寫敏感的,所以processor的變量全用大寫是個不錯的主意。
* 不論它們存放在哪個物理路徑下,只要在你的Python搜索路徑中,你就可以在TEMPLATE_CONTEXT_PROCESSORS?設置里指向它們。 建議你把它們放在應用或者工程目錄下名為context_processors.py?的文件里。
## html自動轉義
從模板生成html的時候,總是有一個風險——變量包了含會影響結果html的字符。 例如,考慮這個模板片段:
~~~
Hello, {{ name }}.
~~~
一開始,這看起來是顯示用戶名的一個無害的途徑,但是考慮如果用戶輸入如下的名字將會發生什么:
~~~
<script>alert('hello')</script>
~~~
用這個用戶名,模板將被渲染成:
~~~
Hello, <script>alert('hello')</script>
~~~
這意味著瀏覽器將彈出JavaScript警告框!
類似的,如果用戶名包含小于符號,就像這樣:
~~~
<b>username
~~~
那樣的話模板結果被翻譯成這樣:
~~~
Hello, <b>username
~~~
頁面的剩余部分變成了粗體!
顯然,用戶提交的數據不應該被盲目信任,直接插入到你的頁面中。因為一個潛在的惡意的用戶能夠利用這類漏洞做壞事。 這類漏洞稱為被跨域腳本 (XSS) 攻擊。 關于安全的更多內容,請看20章
為了避免這個問題,你有兩個選擇:
* 一是你可以確保每一個不被信任的變量都被escape過濾器處理一遍,把潛在有害的html字符轉換為無害的。 這是最初幾年Django的默認方案,但是這樣做的問題是它把責任推給_你_(開發者、模版作者)自己,來確保把所有東西轉意。 很容易就忘記轉意數據。
* 二是,你可以利用Django的自動html轉意。 這一章的剩余部分描述自動轉意是如何工作的。
在django里默認情況下,每一個模板自動轉意每一個變量標簽的輸出。 尤其是這五個字符。
* < 自動轉換為 `<`;
* > 自動轉換為 `>`;
* ' (單引號) 自動轉換為 `'`;
* " (雙引號) 自動轉換為 `"`;
* & 自動轉換為 `&`;
另外,我強調一下這個行為默認是開啟的。 如果你正在使用django的模板系統,那么你是被保護的。
### 如何關閉它
如果你不想數據被自動轉意,在每一站點級別、每一模板級別或者每一變量級別你都有幾種方法來關閉它。
為什么要關閉它? 因為有時候模板變量包含了一些原始html數據,在這種情況下我們不想它們的內容被轉意。 例如,你可能在數據庫里存儲了一段被信任的html代碼,并且你想直接把它嵌入到你的模板里。 或者,你可能正在使用Django的模板系統生成非html文本,比如一封e-mail。
#### 對于單獨的變量
用safe過濾器為單獨的變量關閉自動轉意:
~~~
This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}
~~~
你可以把_safe_當做_safe from further escaping_的簡寫,或者_當做可以被直接譯成HTML的內容_。在這個例子里,如果數據包含'',那么輸出會變成:
~~~
This will be escaped: <b>
This will not be escaped:
~~~
#### 對于模板塊
為了控制模板的自動轉意,用標簽autoescape來包裝整個模板(或者模板中常用的部分),就像這樣:
~~~
{% autoescape off %}
Hello {{ name }}
{% endautoescape %}
~~~
autoescape 標簽有兩個參數on和off 有時,你可能想阻止一部分自動轉意,對另一部分自動轉意。 這是一個模板的例子:
~~~
Auto-escaping is on by default. Hello {{ name }}
{% autoescape off %}
This will not be auto-escaped: {{ data }}.
Nor this: {{ other_data }}
{% autoescape on %}
Auto-escaping applies again: {{ name }}
{% endautoescape %}
{% endautoescape %}
~~~
auto-escaping 標簽的作用域不僅可以影響到當前模板還可以通過include標簽作用到其他標簽,就像block標簽一樣。 例如:
~~~
# base.html
{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
{% endautoescape %}
# child.html
{% extends "base.html" %}
{% block title %}This & that{% endblock %}
{% block content %}{{ greeting }}{% endblock %}
~~~
由于在base模板中自動轉意被關閉,所以在child模板中自動轉意也會關閉.因此,在下面一段HTML被提交時,變量greeting的值就為字符串Hello!
~~~
<h1>This & that</h1>
<b>Hello!</b>
~~~
### 備注
通常,模板作者沒必要為自動轉意擔心. 基于Pyhton的開發者(編寫VIEWS視圖和自定義過濾器)只需要考慮哪些數據不需要被轉意,適時的標記數據,就可以讓它們在模板中工作。
如果你正在編寫一個模板而不知道是否要關閉自動轉意,那就為所有需要轉意的變量添加一個escape過濾器。 當自動轉意開啟時,使用escape過濾器似乎會兩次轉意數據,但其實沒有任何危險。因為escape過濾器不作用于被轉意過的變量。
### 過濾器參數里的字符串常量的自動轉義
就像我們前面提到的,過濾器也可以是字符串.
~~~
{{ data|default:"This is a string literal." }}
~~~
所有字符常量沒有經過轉義就被插入模板,就如同它們都經過了safe過濾。 這是由于字符常量完全由模板作者決定,因此編寫模板的時候他們會確保文本的正確性。
這意味著你必須這樣寫
~~~
{{ data|default:"3 < 2" }}
~~~
而不是這樣
~~~
{{ data|default:"3 < 2" }} <-- Bad! Don't do this.
~~~
這點對來自變量本身的數據不起作用。 如果必要,變量內容會自動轉義,因為它們不在模板作者的控制下。
## 模板加載的內幕
一般說來,你會把模板以文件的方式存儲在文件系統中,但是你也可以使用自定義的?_template loaders_?從其他來源加載模板。
Django有兩種方法加載模板
* django.template.loader.get_template(template_name)?:?get_template?根據給定的模板名稱返回一個已編譯的模板(一個?Template?對象)。 如果模板不存在,就觸發?TemplateDoesNotExist?的異常。
* django.template.loader.select_template(template_name_list)?:?select_template?很像get_template?,不過它是以模板名稱的列表作為參數的。 它會返回列表中存在的第一個模板。 如果模板都不存在,將會觸發TemplateDoesNotExist異常。
正如在第四章中所提到的,默認情況下這些函數使用?TEMPLATE_DIRS?的設置來載入模板。 但是,在內部這些函數可以指定一個模板加載器來完成這些繁重的任務。
一些加載器默認被禁用,但是你可以通過編輯?TEMPLATE_LOADERS?設置來激活它們。?TEMPLATE_LOADERS?應當是一個字符串的元組,其中每個字符串都表示一個模板加載器。 這些模板加載器隨Django一起發布。
> django.template.loaders.filesystem.load_template_source?: 這個加載器根據?TEMPLATE_DIRS?的設置從文件系統加載模板。它默認是可用的。
>
> django.template.loaders.app_directories.load_template_source?: 這個加 載器從文件系統上的Django應用中加載模板。 對?INSTALLED_APPS?中的每個應用,這個加載器會查找templates?子目錄。 如果這個目錄存在,Django就在那里尋找模板。
>
> 這意味著你可以把模板和你的應用一起保存,從而使得Django應用更容易和默認模板一起發布。 例如,如果?INSTALLED_APPS?包含?('myproject.polls','myproject.music')?,那么?get_template('foo.html')?會按這個順序查找模板:
>
> * /path/to/myproject/polls/templates/foo.html
>
>
>
> * /path/to/myproject/music/templates/foo.html
>
>
>
> 請注意加載器在首次被導入的時候會執行一個優化: 它會緩存一個列表,這個列表包含了?INSTALLED_APPS中帶有?templates?子目錄的包。
>
> 這個加載器默認啟用。
>
> django.template.loaders.eggs.load_template_source?: 這個加載器類似?app_directories?,只不過它從Python eggs而不是文件系統中加載模板。 這個加載器默認被禁用;如果你使用eggs來發布你的應用,那么你就需要啟用它。 Python eggs可以將Python代碼壓縮到一個文件中。
Django按照?TEMPLATE_LOADERS?設置中的順序使用模板加載器。 它逐個使用每個加載器直至找到一個匹配的模板。
## 擴展模板系統
既然你已經對模板系統的內幕多了一些了解,讓我們來看看如何使用自定義的代碼來擴展這個系統吧。
絕大部分的模板定制是以自定義標簽/過濾器的方式來完成的。 盡管Django模板語言自帶了許多內建標簽和過濾器,但是你可能還是需要組建你自己的標簽和過濾器庫來滿足你的需要。 幸運的是,定義你自己的功能非常容易。
### 創建一個模板庫
不管是寫自定義標簽還是過濾器,第一件要做的事是創建**模板庫**(Django能夠導入的基本結構)。
創建一個模板庫分兩步走:
> 第一,決定模板庫應該放在哪個Django應用下。 如果你通過?manage.py?startapp?創建了一個應用,你可以把它放在那里,或者你可以為模板庫單獨創建一個應用。 我們更推薦使用后者,因為你的filter可能在后來的工程中有用。
>
> 無論你采用何種方式,請確保把你的應用添加到?INSTALLED_APPS?中。 我們稍后會解釋這一點。
>
> 第二,在適當的Django應用包里創建一個?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應用程序中的模板庫。 這是一個安全特性;它可以讓你在一臺電腦上部署很多的模板庫的代碼,而又不用把它們暴露給每一個Django安裝。
如果你寫了一個不和任何特定模型/視圖關聯的模板庫,那么得到一個僅包含?templatetags?包的Django應用程序包是完全正常的。 對于在?templatetags?包中放置多少個模塊沒有做任何的限制。 需要了解的是:{%load%}語句是通過指定的Python模塊名而不是應用名來加載標簽/過濾器的。
一旦創建了Python模塊,你只需根據是要編寫過濾器還是標簽來相應的編寫一些Python代碼。
作為合法的標簽庫,模塊需要包含一個名為register的模塊級變量。這個變量是template.Library的實例,是所有注冊標簽和過濾器的數據結構。 所以,請在你的模塊的頂部插入如下語句:
~~~
from django import template
register = template.Library()
~~~
注意
請閱讀Django默認的過濾器和標簽的源碼,那里有大量的例子。 他們分別為:django/template/defaultfilters.py?和 django/template/defaulttags.py 。django.contrib中的某些應用程序也包含模板庫。
創建?register?變量后,你就可以使用它來創建模板的過濾器和標簽了。
### 自定義模板過濾器
自定義過濾器就是有一個或兩個參數的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:" " }}
~~~
大多數過濾器并不需要參數。 下面的例子把參數從你的函數中拿掉了:
~~~
def lower(value): # Only one argument.
"Converts a string into all lowercase"
return value.lower()
~~~
當你定義完過濾器后,你需要用?Library?實例來注冊它,這樣就能通過Django的模板語言來使用了:
~~~
register.filter('cut', cut)
register.filter('lower', lower)
~~~
Library.filter()?方法需要兩個參數:
* 過濾器的名稱(一個字串)
* 過濾器函數本身
如果你使用的是Python 2.4或者更新的版本,你可以使用裝飾器register.filter():
~~~
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter
def lower(value):
return value.lower()
~~~
如果你想第二個例子那樣不使用?name?參數,那么Django會把函數名當作過濾器的名字。
下面是一個完整的模板庫的例子,它包含一個?cut?過濾器:
~~~
from django import template
register = template.Library()
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
~~~
### 自定義模板標簽
標簽要比過濾器復雜些,因為標簽幾乎能做任何事情。
第四章描述了模板系統的兩步處理過程: 編譯和呈現。 為了自定義一個模板標簽,你需要告訴Django當遇到你的標簽時怎樣進行這個過程。
當Django編譯一個模板時,它將原始模板分成一個個?_節點_?。每個節點都是?django.template.Node?的一個實例,并且具備?render()?方法。 于是,一個已編譯的模板就是?節點?對象的一個列表。 例如,看看這個模板:
~~~
Hello, {{ person.name }}.
{% ifequal name.birthday today %}
Happy birthday!
{% else %}
Be sure to come back on your birthday
for a splendid surprise message.
{% endifequal %}
~~~
被編譯的模板表現為節點列表的形式:
* 文本節點:?"Hello,?"
* 變量節點:?person.name
* 文本節點:?".\n\n"
* IfEqual節點:?name.birthday和today
當你調用一個已編譯模板的?render()?方法時,模板就會用給定的context來調用每個在它的節點列表上的所有節點的?render()?方法。 這些渲染的結果合并起來,形成了模板的輸出。 因此,要自定義模板標簽,你需要指明原始模板標簽如何轉換成節點(編譯函數)和節點的render()方法完成的功能 。
在下面的章節中,我們將詳細解說寫一個自定義標簽時的所有步驟。
### 編寫編譯函數
當遇到一個模板標簽(template tag)時,模板解析器就會把標簽包含的內容,以及模板解析器自己作為參數調用一個python函數。 這個函數負責返回一個和當前模板標簽內容相對應的節點(Node)的實例。
例如,寫一個顯示當前日期的模板標簽:{% current_time %}。該標簽會根據參數指定的 strftime 格式(參見:http://www.djangoproject.com/r/python/strftime/)顯示當前時間。首先確定標簽的語法是個好主意。 在這個例子里,標簽應該這樣使用:
~~~
The time is {% current_time "%Y-%m-%d %I:%M %p" %}.
~~~
注意
沒錯, 這個模板標簽是多余的,Django默認的?{%?now?%}?用更簡單的語法完成了同樣的工作。 這個模板標簽在這里只是作為一個例子。
這個函數的分析器會獲取參數并創建一個?Node?對象:
~~~
from django import template
register = template.Library()
def do_current_time(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, format_string = token.split_contents()
except ValueError:
msg = '%r tag requires a single argument' % token.split_contents()[0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[1:-1])
~~~
這里需要說明的地方很多:
* 每個標簽編譯函數有兩個參數,parser和token。parser是模板解析器對象。 我們在這個例子中并不使用它。?token是正在被解析的語句。
* token.contents?是包含有標簽原始內容的字符串。 在我們的例子中,它是'current_time?"%Y-%m-%d?%I:%M?%p"'?。
* token.split_contents()?方法按空格拆分參數同時保證引號中的字符串不拆分。 應該避免使用token.contents.split()?(僅使用Python的標準字符串拆分)。 它不夠健壯,因為它只是簡單的按照所有空格進行拆分,包括那些引號引起來的字符串中的空格。
* 這個函數可以拋出?django.template.TemplateSyntaxError?,這個異常提供所有語法錯誤的有用信息。
* 不要把標簽名稱硬編碼在你的錯誤信息中,因為這樣會把標簽名稱和你的函數耦合在一起。token.split_contents()[0]_總是_記錄標簽的名字,就算標簽沒有任何參數。
* 這個函數返回一個?CurrentTimeNode?(稍后我們將創建它),它包含了節點需要知道的關于這個標簽的全部信息。 在這個例子中,它只是傳遞了參數?"%Y-%m-%d?%I:%M?%p"?。模板標簽開頭和結尾的引號使用format_string[1:-1]?除去。
* 模板標簽編譯函數?_必須_?返回一個?Node?子類,返回其它值都是錯的。
### 編寫模板節點
編寫自定義標簽的第二步就是定義一個擁有?render()?方法的?Node?子類。 繼續前面的例子,我們需要定義CurrentTimeNode?:
~~~
import datetime
class CurrentTimeNode(template.Node):
def __init__(self, format_string):
self.format_string = str(format_string)
def render(self, context):
now = datetime.datetime.now()
return now.strftime(self.format_string)
~~~
這兩個函數(?__init__()?和?render()?)與模板處理中的兩步(編譯與渲染)直接對應。 這樣,初始化函數僅僅需要存儲后面要用到的格式字符串,而?render()?函數才做真正的工作。
與模板過濾器一樣,這些渲染函數應該靜靜地捕獲錯誤,而不是拋出錯誤。 模板標簽只允許在編譯的時候拋出錯誤。
### 注冊標簽
最后,你需要用你模塊的Library?實例注冊這個標簽。 注冊自定義標簽與注冊自定義過濾器非常類似(如前文所述)。 只需實例化一個?template.Library?實例然后調用它的?tag()?方法。 例如:
~~~
register.tag('current_time', do_current_time)
~~~
tag()?方法需要兩個參數:
* 模板標簽的名字(字符串)。
* 編譯函數。
和注冊過濾器類似,也可以在Python2.4及其以上版本中使用?register.tag裝飾器:
~~~
@register.tag(name="current_time")
def do_current_time(parser, token):
# ...
@register.tag
def shout(parser, token):
# ...
~~~
如果你像在第二個例子中那樣忽略?name?參數的話,Django會使用函數名稱作為標簽名稱。
### 在上下文中設置變量
前一節的例子只是簡單的返回一個值。 很多時候設置一個模板變量而非返回值也很有用。 那樣,模板作者就只能使用你的模板標簽所設置的變量。
要在上下文中設置變量,在?render()?函數的context對象上使用字典賦值。 這里是一個修改過的CurrentTimeNode?,其中設定了一個模板變量?current_time?,并沒有返回它:
~~~
class CurrentTimeNode2(template.Node):
def __init__(self, format_string):
self.format_string = str(format_string)
def render(self, context):
now = datetime.datetime.now()
context['current_time'] = now.strftime(self.format_string)
return ''
~~~
(我們把創建函數do_current_time2和注冊給current_time2模板標簽的工作留作讀者練習。)
注意?render()?返回了一個空字符串。?render()?應當總是返回一個字符串,所以如果模板標簽只是要設置變量,?render()?就應該返回一個空字符串。
你應該這樣使用這個新版本的標簽:
~~~
{% current_time2 "%Y-%M-%d %I:%M %p" %}
The time is {{ current_time }}.
~~~
但是?CurrentTimeNode2?有一個問題: 變量名?current_time?是硬編碼的。 這意味著你必須確定你的模板在其它任何地方都不使用?{{?current_time?}}?,因為?{%?current_time2?%}?會盲目的覆蓋該變量的值。
一種更簡潔的方案是由模板標簽來指定需要設定的變量的名稱,就像這樣:
~~~
{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
The current time is {{ my_current_time }}.
~~~
為此,你需要重構編譯函數和?Node?類,如下所示:
~~~
import re
class CurrentTimeNode3(template.Node):
def __init__(self, format_string, var_name):
self.format_string = str(format_string)
self.var_name = var_name
def render(self, context):
now = datetime.datetime.now()
context[self.var_name] = now.strftime(self.format_string)
return ''
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:
msg = '%r tag requires arguments' % token.contents[0]
raise template.TemplateSyntaxError(msg)
m = re.search(r'(.*?) as (\w+)', arg)
if m:
fmt, var_name = m.groups()
else:
msg = '%r tag had invalid arguments' % tag_name
raise template.TemplateSyntaxError(msg)
if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
msg = "%r tag's argument should be in quotes" % tag_name
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode3(fmt[1:-1], var_name)
~~~
現在?do_current_time()?把格式字符串和變量名傳遞給?CurrentTimeNode3?。
### 分析直至另一個模板標簽
模板標簽可以像包含其它標簽的塊一樣工作(想想?{%?if?%}?、?{%?for?%}?等)。 要創建一個這樣的模板標簽,在你的編譯函數中使用?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_對象的列表,這些對象是解析器在解析到任一元組中指定的標簽之前遇到的內容.
因此在前面的例子中,?nodelist?是在?{%?comment?%}?和?{%?endcomment?%}?之間所有節點的列表,不包括{%?comment?%}?和?{%?endcomment?%}?自身。
在?parser.parse()?被調用之后,分析器還沒有清除?{%?endcomment?%}?標簽,因此代碼需要顯式地調用parser.delete_first_token()?來防止該標簽被處理兩次。
之后?CommentNode.render()?只是簡單地返回一個空字符串。 在?{%?comment?%}?和?{%?endcomment?%}?之間的所有內容都被忽略。
### 分析直至另外一個模板標簽并保存內容
在前一個例子中,?do_comment()?拋棄了{%?comment?%}?和?{%?endcomment?%}?之間的所有內容。當然也可以修改和利用下標簽之間的這些內容。
例如,這個自定義模板標簽{%?upper?%},它會把它自己和{%?endupper?%}之間的內容變成大寫:
~~~
{% upper %}
This will appear in uppercase, {{ user_name }}.
{% endupper %}
~~~
就像前面的例子一樣,我們將使用?parser.parse()?。這次,我們將產生的?nodelist?傳遞給?Node?:
~~~
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()
~~~
這里唯一的一個新概念是?UpperNode.render()?中的?self.nodelist.render(context)?。它對節點列表中的每個Node?簡單的調用?render()?。
更多的復雜渲染示例請查看?django/template/defaulttags.py?中的?{%?if?%}?、?{%?for?%}?、?{%?ifequal?%}和?{%?ifchanged?%}?的代碼。
### 簡單標簽的快捷方式
許多模板標簽接收單一的字符串參數或者一個模板變量引用,然后獨立地根據輸入變量和一些其它外部信息進行處理并返回一個字符串。 例如,我們先前寫的current_time標簽就是這樣一個例子。 我們給定了一個格式化字符串,然后它返回一個字符串形式的時間。
為了簡化這類標簽,Django提供了一個幫助函數simple_tag。這個函數是django.template.Library的一個方法,它接受一個只有一個參數的函數作參數,把它包裝在render函數和之前提及過的其他的必要單位中,然后通過模板系統注冊標簽。
我們之前的的?current_time?函數于是可以寫成這樣:
~~~
def current_time(format_string):
try:
return datetime.datetime.now().strftime(str(format_string))
except UnicodeEncodeError:
return ''
register.simple_tag(current_time)
~~~
在Python 2.4中,也可以使用裝飾器語法:
~~~
@register.simple_tag
def current_time(token):
# ...
~~~
有關?simple_tag?輔助函數,需要注意下面一些事情:
* 傳遞給我們的函數的只有(單個)參數。
* 在我們的函數被調用的時候,檢查必需參數個數的工作已經完成了,所以我們不需要再做這個工作。
* 參數兩邊的引號(如果有的話)已經被截掉了,所以我們會接收到一個普通Unicode字符串。
### 包含標簽
另外一類常用的模板標簽是通過渲染?_其他_?模板顯示數據的。 比如說,Django的后臺管理界面,它使用了自定義的模板標簽來顯示新增/編輯表單頁面下部的按鈕。 那些按鈕看起來總是一樣的,但是鏈接卻隨著所編輯的對象的不同而改變。 這就是一個使用小模板很好的例子,這些小模板就是當前對象的詳細信息。
這些排序標簽被稱為?_包含標簽_?。如何寫包含標簽最好通過舉例來說明。 讓我們來寫一個能夠產生指定作者對象的書籍清單的標簽。 我們將這樣利用標簽:
~~~
{% books_for_author author %}
~~~
結果將會像下面這樣:
~~~
<ul>
<li>The Cat In The Hat</li>
<li>Hop On Pop</li>
<li>Green Eggs And Ham</li>
</ul>
~~~
首先,我們定義一個函數,通過給定的參數生成一個字典形式的結果。 需要注意的是,我們只需要返回字典類型的結果就行了,不需要返回更復雜的東西。 這將被用來作為模板片段的內容:
~~~
def books_for_author(author):
books = Book.objects.filter(authors__id=author.id)
return {'books': books}
~~~
接下來,我們創建用于渲染標簽輸出的模板。 在我們的例子中,模板很簡單:
~~~
<ul>
{% for book in books %}
<li>{{ book.title }}</li>
{% endfor %}
</ul>
~~~
最后,我們通過對一個?Library?對象使用?inclusion_tag()?方法來創建并注冊這個包含標簽。
在我們的例子中,如果先前的模板在?polls/result_snippet.html?文件中,那么我們這樣注冊標簽:
~~~
register.inclusion_tag('book_snippet.html')(books_for_author)
~~~
Python 2.4裝飾器語法也能正常工作,所以我們可以這樣寫:
~~~
@register.inclusion_tag('book_snippet.html')
def books_for_author(author):
# ...
~~~
有時候,你的包含標簽需要訪問父模板的context。 為了解決這個問題,Django為包含標簽提供了一個takes_context?選項。 如果你在創建模板標簽時,指明了這個選項,這個標簽就不需要參數,并且下面的Python函數會帶一個參數: 就是當這個標簽被調用時的模板context。
例如,你正在寫一個包含標簽,該標簽包含有指向主頁的?home_link?和?home_title?變量。 Python函數會像這樣:
~~~
@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
return {
'link': context['home_link'],
'title': context['home_title'],
}
~~~
(注意函數的第一個參數?_必須_?是?context?。)
模板?link.html?可能包含下面的東西:
~~~
Jump directly to <a href="{{ link }}">{{ title }}</a>.
~~~
然后您想使用自定義標簽時,就可以加載它的庫,然后不帶參數地調用它,就像這樣:
~~~
{% jump_link %}
~~~
## 編寫自定義模板加載器
Djangos 內置的模板加載器(在先前的模板加載內幕章節有敘述)通常會滿足你的所有的模板加載需求,但是如果你有特殊的加載需求的話,編寫自己的模板加載器也會相當簡單。 比如:你可以從數據庫中,或者利用Python的綁定直接從Subversion庫中,更或者從一個ZIP文檔中加載模板。
模板加載器,也就是?TEMPLATE_LOADERS?中的每一項,都要能被下面這個接口調用:
~~~
load_template_source(template_name, template_dirs=None)
~~~
參數?template_name?是所加載模板的名稱 (和傳遞給?loader.get_template()?或者?loader.select_template()一樣), 而?template_dirs?是一個可選的代替TEMPLATE_DIRS的搜索目錄列表。
如果加載器能夠成功加載一個模板, 它應當返回一個元組:?(template_source,?template_path)?。在這里的template_source?就是將被模板引擎編譯的的模板字符串,而?template_path?是被加載的模板的路徑。 由于那個路徑可能會出于調試目的顯示給用戶,因此它應當很快的指明模板從哪里加載。
如果加載器加載模板失敗,那么就會觸發?django.template.TemplateDoesNotExist?異常。
每個加載函數都應該有一個名為?is_usable?的函數屬性。 這個屬性是一個布爾值,用于告知模板引擎這個加載器是否在當前安裝的Python中可用。 例如,如果?pkg_resources?模塊沒有安裝的話,eggs加載器(它能夠從python eggs中加載模板)就應該把?is_usable?設為?False?,因為必須通過?pkg_resources?才能從eggs中讀取數據。
一個例子可以清晰地闡明一切。 這兒是一個模板加載函數,它可以從ZIP文件中加載模板。 它使用了自定義的設置?TEMPLATE_ZIP_FILES?來取代了?TEMPLATE_DIRS?用作查找路徑,并且它假設在此路徑上的每一個文件都是包含模板的ZIP文件:
~~~
from django.conf import settings
from django.template import TemplateDoesNotExist
import zipfile
def load_template_source(template_name, template_dirs=None):
"Template loader that loads templates from a ZIP file."
template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])
# Try each ZIP file in TEMPLATE_ZIP_FILES.
for fname in template_zipfiles:
try:
z = zipfile.ZipFile(fname)
source = z.read(template_name)
except (IOError, KeyError):
continue
z.close()
# We found a template, so return the source.
template_path = "%s:%s" % (fname, template_name)
return (source, template_path)
# If we reach here, the template couldn't be loaded
raise TemplateDoesNotExist(template_name)
# This loader is always usable (since zipfile is included with Python)
load_template_source.is_usable = True
~~~
我們要想使用它,還差最后一步,就是把它加入到?TEMPLATE_LOADERS?。 如果我們將這個代碼放入一個叫mysite.zip_loader的包中,那么我們要把mysite.zip_loader.load_template_source加到TEMPLATE_LOADERS中。
## 配置獨立模式下的模板系統
注意:
這部分只針對于對在其他應用中使用模版系統作為輸出組件感興趣的人。 如果你是在Django應用中使用模版系統,請略過此部分。
通常,Django會從它的默認配置文件和由?DJANGO_SETTINGS_MODULE?環境變量所指定的模塊中加載它需要的所有配置信息。 (這點在第四章的”特殊的Python命令提示行”一節解釋過。)但是當你想在非Django應用中使用模版系統的時候,采用環境變量并不方便,因為你可能更想同其余的應用一起配置你的模板系統,而不是處理配置文件并通過環境變量指向他們。
為了解決這個問題,你需要使用附錄D中所描述的手動配置選項。概括的說,你需要導入正確的模板中的片段,然后在你訪問任一個模板函數之前,首先用你想指定的配置訪問Django.conf.settings.configure()。
你可能會考慮至少要設置?TEMPLATE_DIRS?(如果你打算使用模板加載器),?DEFAULT_CHARSET?(盡管默認的utf-8?編碼相當好用),以及?TEMPLATE_DEBUG?。所有可用的選項在附錄D中都有詳細描述,所有以?TEMPLATE_開頭的選項都可能使你感興趣。
## 接下來做什么?
延續本章的高級話題,[下一章](http://docs.30c.org/djangobook2/chapter10/index.html)?會繼續討論Django模型的高級用法。