第九章 模板高級進階

第九章 模板高級進階

雖然大多數和Django模板語言的交互都是模板做者的工做,但你可能想定製和擴展模板引擎,讓它作一些它不能作的事情,或者是以其餘方式讓你的工做更輕鬆。html

本章深刻探討Django的模板系統。 若是你想擴展模板系統或者只是對它的工做原理感受到好奇,本章涉及了你須要瞭解的東西。 它也包含一個自動轉意特徵,若是你繼續使用django,隨着時間的推移你必定會注意這個安全考慮。node

若是你想把Django的模版系統做爲另一個應用程序的一部分(就是說,僅使用Django的模板系統而不使用Django框架的其餘部分),那你必定要讀一下「配置獨立模式下的模版系統」這一節。python

模板語言回顧

首先,讓咱們快速回顧一下第四章介紹的若干專業術語:sql

模板 是一個純文本文件,或是一個用Django模板語言標記過的普通的Python字符串。 模板能夠包含模板標籤和變量。數據庫

模板標籤 是在一個模板裏面起做用的的標記。 這個定義故意搞得模糊不清。 例如,一個模版標籤可以產生做爲控制結構的內容(一個 if語句或for 循環), 能夠獲取數據庫內容,或者訪問其餘的模板標籤。express

區塊標籤被 {% 和 %} 包圍:django

{% if is_logged_in %}
    Thanks for logging in!
{% else %}
    Please log in.
{% endif %}

變量 是一個在模板裏用來輸出值的標記。api

變量標籤被 {{ 和 }} 包圍:跨域

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對象而後渲染模板。 是爲了可以清晰的說明全部步驟。)

每一個視圖都給模板傳入了三個相同的變量:appuserip_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 :包含相似於 ``{‘sql’: …, ‘time’: `` 的字典的一個列表, 記錄了這個請求期間的每一個SQL查詢以及查詢所耗費的時間。 這個列表是按照請求順序進行排列的。

    System Message: WARNING/2 (<string>, line 315); backlink

    Inline literal start-string without end-string.

因爲調試信息比較敏感,因此這個context處理器只有當同時知足下面兩個條件的時候纔有效:

  • DEBUG 參數設置爲 True 。

  • 請求的ip應該包含在 INTERNAL_IPS 的設置裏面。

細心的讀者可能會注意到debug模板變量的值永遠不可能爲False,由於若是DEBUGFalse,那麼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警告框!

相似的,若是用戶名包含小於符號,就像這樣:

用戶名

那樣的話模板結果被翻譯成這樣:

Hello, <b>username

頁面的剩餘部分變成了粗體!

顯然,用戶提交的數據不該該被盲目信任,直接插入到你的頁面中。由於一個潛在的惡意的用戶可以利用這類漏洞作壞事。 這類漏洞稱爲被跨域腳本 (XSS) 攻擊。 關於安全的更多內容,請看20章

爲了不這個問題,你有兩個選擇:

  • 一是你能夠確保每個不被信任的變量都被escape過濾器處理一遍,把潛在有害的html字符轉換爲無害的。 這是最初幾年Django的默認方案,可是這樣作的問題是它把責任推給(開發者、模版做者)本身,來確保把全部東西轉意。 很容易就忘記轉意數據。

  • 二是,你能夠利用Django的自動html轉意。 這一章的剩餘部分描述自動轉意是如何工做的。

在django裏默認狀況下,每個模板自動轉意每個變量標籤的輸出。 尤爲是這五個字符。

  • ``\ ``

    System Message: WARNING/2 (<string>, line 491); backlink

    Inline literal start-string without end-string.

  • > 被轉換爲>

  • '(單引號)被轉換爲'

  • "(雙引號)被轉換爲"

  • & is converted to &

另外,我強調一下這個行爲默認是開啓的。 若是你正在使用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: &lt;b&gt;
This will not be escaped: <b>

對於模板塊

爲了控制模板的自動轉意,用標籤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 &lt; 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.birthdaytoday

當你調用一個已編譯模板的 render() 方法時,模板就會用給定的context來調用每一個在它的節點列表上的全部節點的 render() 方法。 這些渲染的結果合併起來,造成了模板的輸出。 所以,要自定義模板標籤,你須要指明原始模板標籤如何轉換成節點(編譯函數)和節點的render()方法完成的功能 。

在下面的章節中,咱們將詳細解說寫一個自定義標籤時的全部步驟。

編寫編譯函數

當遇到一個模板標籤(template tag)時,模板解析器就會把標籤包含的內容,以及模板解析器本身做爲參數調用一個python函數。 這個函數負責返回一個和當前模板標籤內容相對應的節點(Node)的實例。

例如,寫一個顯示當前日期的模板標籤:{% current_time %}。該標籤會根據參數指定的 strftime 格式(參見:http://www.djangoproject.com/r/python/strftime/)顯示當前時間。首先肯定標籤的語法是個好主意。 在這個例子裏,標籤應該這樣使用:

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</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])

這裏須要說明的地方不少:

  • 每一個標籤編譯函數有兩個參數,parsertokenparser是模板解析器對象。 咱們在這個例子中並不使用它。 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" %}
<p>The time is {{ current_time }}.</p>

可是 CurrentTimeNode2 有一個問題: 變量名 current_time 是硬編碼的。 這意味着你必須肯定你的模板在其它任何地方都不使用 {{ current_time }} ,由於 {% current_time2 %} 會盲目的覆蓋該變量的值。

一種更簡潔的方案是由模板標籤來指定須要設定的變量的名稱,就像這樣:

{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

爲此,你須要重構編譯函數和 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_ 開頭的選項均可能使你感興趣。

相關文章
相關標籤/搜索