テンプレートタグやフィルタを自作する

revision-up-to:17812 (1.4)

Django テンプレートシステムには、いろいろな 組み込みタグとフィルタ が付属していて、アプリケーションのプレゼンテーショ ンロジックにまつわる問題を解決できます。とはいえ、コアのテンプレートタグプリミ ティブだけでは、要求を満たせない場合もあります。そういう場合のために、テンプ レートエンジンを拡張できます。Python で自作のタグやフィルタを書き、テンプレー ト上で {% load %} タグを使って使えるのです。

コードの配置

カスタムのテンプレートタグやフィルタは Django のアプリケーション内に置きま す。既存のアプリケーションに関連があるのなら、そのアプリケーションにバンド ルすればよいでしょう。そうでなければ、コードを入れておくための新たなアプリ ケーションを作成します。

アプリケーション内には、 templatetags ディレクトリを置かねばなりません。 このディレクトリは、 models.pyviews.py などと同じ階層に置きます。 templatetags がなければ、作成してください。ディレクトリ内に __init__.py を置いて、パッケージ化するのを忘れないでください。

カスタムのタグやフィルタは、 templatetags ディレクトリの下に置きます。 モジュールファイルの名前は、あとでタグをロードするときに使う名前にします。 ですから、他のアプリケーションのカスタムタグやフィルタと名前が衝突しないよ う、よく注意して名前を決めましょう。

poll_extras.py という名前のファイルに自作のタグ/フィルタを入れているの なら、アプリケーションのレイアウトは以下のようになるでしょう:

polls/
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

そして、テンプレートでは以下のようにしてロードします:

{% load poll_extras %}

あるアプリケーションのカスタムタグを {% load %} で呼び出せるよう にするには、そのアプリケーションを INSTALLED_APPS にいれておかねば なりません。これは、一つのホストに複数の Django アプリケーションを置いたとき に、 Django アプリケーション同士が勝手に他のテンプレートライブラリにアクセスし ないようにするためのセキュリティ機能です。

templatetags パッケージには、いくつでもモジュールを入れられます。 {% load %} 文はアプリケーションの名前ではなく、 Python モジュー ル名に対応したタグ/フィルタをロードするということを心に留めておいてください。 有効なタグライブラリを作るには、 register という名前のモジュールレベル変数 に template.Library のインスタンスを入れておかねばなりません。このインスタ ンスには、全てのタグとフィルタが登録されます。そこで、モジュールの一番上の方 で、以下のコードを実行しておいてください:

from django import template

register = template.Library()

舞台裏

Django のデフォルトのフィルタやタグのソースコードには大量のサンプルが 収まっています。ソースコードはそれぞれ django/template/defaultfilters.pydjango/template/defaulttags.py にあります。

load タグについてもっと知りたければ、そちらのドキュメントを読んで ください。

カスタムのテンプレートフィルタ

カスタムフィルタは単なる Python の関数で、一つまたは二つの引数:

  • テンプレート変数 (入力) の値。文字列とは限りません。
  • 引数の値。デフォルトを持たせたり、無視させたりできます。

を取ります。例えば、フィルタ {{ var|foo:"bar" }} では、フィルタ foo はテンプレート変数 var と引数 "bar" を受け取ります。

フィルタ関数は常に何かを返さねばなりません。フィルタ関数は例外を送出しては ならず、失敗するときは暗黙のうちに失敗せねばなりません。エラーが生じた場合、 フィルタ関数は元の入力そのままか空文字列のうち、分かりやすい方を返すように せねばなりません。

フィルタの定義の例を以下に示します:

def cut(value, arg):
    """入力から arg の値を全て取り去る"""
    return value.replace(arg, '')

このフィルタは以下のように使います:

{{ somevariable|cut:"0" }}

ほとんどのフィルタは引数を取りません。引数を取らない場合には関数から引数を なくして下さい。例えば:

def lower(value): # Only one argument.
    """入力をすべて小文字にする"""
    return value.lower()

カスタムフィルタを登録する

フィルタ定義を書き終えたら、 Django テンプレート言語で使えるようにするため に Library インスタンスに登録する必要があります:

register.filter('cut', cut)
register.filter('lower', lower)

Library.filter() は二つの引数を取ります:

  1. フィルタの名前。文字列です。
  2. コンパイル関数。(関数名の文字列ではなく) Python 関数です。

register.filter() をデコレータとして使うこともできます:

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()

上の例の下の定義のようにして name 引数を省略すると、 Django は関数名をその ままフィルタ名として使います。

最後に register.filter()is_safeneeds_autoescape の 2 つの キーワード引数をとることもできます。下の フィルタと自動エスケープ で説明しています。

テンプレートフィルタに文字列だけを処理させる

第一引数に文字列しかとらないテンプレートフィルタを書きたければ、 stringfilter デコレータを使ってください。このフィルタは、フィルタ処理の 関数を呼び出す前に、第一引数のオブジェクトを文字列に変換します:

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()

@register.filter
@stringfilter
def lower(value):
    return value.lower()

こうすれば、例えばフィルタに整数型を渡したとしても、(整数型に lower() がないために) AttributeError が送出されるようなことはありません。

フィルタと自動エスケープ

Django 1.0 で新たに登場しました: リリースノートを参照してください

カスタムフィルタを書く場合、フィルタが Django の自動エスケープとどう関わっ ているかに少し配慮しておく必要があります。まず、テンプレートのコード内では、 3 種類の文字列が受け渡しされています:

  • 生の文字列 は、 Python ネイティブの strunicode 型の オブジェクトです。出力時、自動エスケープが有効であればエスケープされ、 そうでなければ変更されません。

  • safe 文字列 は、出力時にさらなるエスケープ処理を行わなくてもよ い文字列です。safe 文字列は、必要なエスケープが済んでいる文字列です。 クライアント側でそのまま解釈されるべき生の HTML を含んだ文字列を表現 するのに使われます。

    内部的には、これらの文字列は SafeStringSafeUnicode 型で表 現されています。 SafeStringSafeUnicodeSafeData を 共通の基底クラスに持つので、以下のようにすれば判別できます:

    if isinstance(value, SafeData):
        # "safe" 文字列を扱う処理をここに
    
  • “エスケープが必要” であるとマークされている文字列 は、 autoescape ブロックの指定に関係なく、 必ず エスケープされます。 マークされている文字列は、自動エスケープの設定に関係なく、一度だけエ スケープ処理を受けます。

    内部的には、エスケープ必要文字列は、 EscapeStringEscapeUnicode 型で表現されています。通常、これらの型のことを気に する必要はありません。 EscapeStringEscapeUnicode は、 escape フィルタの実装のために用意されています。

テンプレートフィルタのコードは、以下の二つのうちどちらかに落ち着きます:

  1. フィルタコードの出力が、 HTML 出力として安全でない文字 (<, >, ', " or &) を含まない場合。この場合、自動エスケー プの処理は全て Django に任せられます。必要なのは、フィルタ関数の is_safe 属性に True を設定しておくことだけです:

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    この属性を設定しておくと、このフィルタは「安全な」文字列を渡したとき には出力は「安全な」ままであり、「安全でない」文字列を渡した時には、 必要に応じて自動的にエスケープします。

    is_safe は、「このフィルタは安全 (safe) であり、安全でない HTML を生成するおそれがない」という意味だと考えてください。

    is_safe が必要なのは、通常の文字列操作で、 SafeData オブジェ クトを strunicode オブジェクトに変換するものが数多くあ るからです。全てを try と catch で拾うのは難しいので、 Django はフィ ルタ処理が完了した時点でダメージを回復する戦略を取っています。

    例えば、入力の末尾に xx を追加するようなフィルタを書くとします。 このフィルタは危険な HTML 文字を出力しないので、フィルタ関数を is_safe でマークしておきます:

    @register.filter(is_safe=True)
    def add_xx(value):
        return '%sxx' % value
    

    このフィルタをテンプレート上の自動エスケープが有効な部分で使うと、 Django は “safe” にマークされていない入力の時にフィルタの出力をエス ケープします。

    デフォルトでは、 is_safeFalse なので、 is_safe が 必要でなければ無視してかまいません。

    フィルタを作成するときには、フィルタが本当に安全な文字を安全なままに 出力するのかをよく考えてください。例えば、フィルタが文字を 削除 す る場合、出力結果に、対応の取れていないタグや不完全なエンティティ表現 が意図せず混じる可能性があります。例えば、 > を入力から削ってし まうと、 <a> の出力は <a になってしまい、問題を起こさないよ うにするためには、出力をエスケープせねばならなくなってしまいます。同 様に、セミコロン (;) を除去すると、 &amp;&amp になっ てしまい、有効なエンティティ表現でないために、エスケープが必要になっ てしまいます。たいていのケースでは、このようなトリッキーなことは起こ りませんが、コードをレビューする際にはこの手のことが起こらないように よく確認してください。

    フィルタを is_safe にマークするとフィルタに文字列を返すように強制させる ことになります。真偽値や非文字列型の値を返すフィルタを is_safe にマーク すると、おそらく意図しない結果を起こすことになります (真偽値の False が文字 列の ‘False’ に変換されるなど) 。

  2. もう一つは、フィルタのコードで必要なエスケープを行うというものです。 出力に新たな HTML マークアップを含めたい場合に必要です。この場合、 出力する HTML マークアップがそれ以上エスケープされないよう、出力を safe にする必要があるので、入力は自分で扱わねばなりません。

    出力を safe 文字列としてマークするには、 django.utils.safestring.mark_safe() を使います。

    ただし、注意してください。出力を safe であるとマークするだけでは十分 ではありません。出力が本当に safe である ことを保証せねばならず、 それは自動エスケープが有効か無効かで変わります。自動エスケープがオン でもオフでも使え、テンプレートの作者が簡単に扱えるフィルタを書くのが 理想です。

    現在の自動エスケープ状態をフィルタに教えるには、関数の needs_autoescape 属性を True に設定します (この属性を設定 しない場合のデフォルト値は False です)。 needs_autoescapeTrue にすると、フィルタ関数には autoescape という追加の引 数が渡されます。自動エスケープが有効な状況でフィルタが呼び出されると、 autoescape には True が渡され、そうでない場合には 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 を使って、入力デー タを django.utils.html.conditional_escape で処理するべきかどうか 判定しています。 (conditional_escape が不要の場合、 esc を アイデンティティ関数にしています) conditional_escape()escape() に似ていますが、入力が SafeData インスタンスで ない 場合にのみエスケープを適用します。 conditional_escape()SafeData を渡すと、データは変更されません。

    最後に、上の例では、フィルタ結果を mark_safe で safe にマークし て、出力結果の HTML をこれ以上エスケープせず、最終的なテンプレート出 力に挿入させています。

    このケースでは、 is_safe 属性について触れていません (実際には、 設定しても何の影響もありません)。自動エスケープの問題を手動で解決し て、 safe 文字列を返している場合、 is_safe 属性は何ら影響をもた ないのです。

Django 1.4 で変更されました: リリースノートを参照してください

is_safeneeds_autoescape はフィルタ関数の属性として使われていまし た。この構文は廃止されています。

@register.filter
def myfilter(value):
    return value
myfilter.is_safe = True
@register.filter
def initial_letter_filter(text, autoescape=None):
    # ...
    return mark_safe(result)
initial_letter_filter.needs_autoescape = True

フィルタとタイムゾーン

Django 1.4 で新たに登場しました: リリースノートを参照してください

datetime オブジェクトを操作するカスタムフィルタを書く場合、 普通は expects_localtime フラグを True にセットして登録します:

@register.filter(expects_localtime=True)
def businesshours(value):
    try:
        return 9 <= value.hour < 17
    except AttributeError:
        return ''

このフラグがセットされると、もし最初の引数がタイムゾーン aware な datetime な ら Django はそれをフィルタに渡す前に テンプレートでのタイムゾーン変換ルール に従っ て、適切なカレントタイムゾーンに変換します。

カスタムテンプレートタグを書く

タグはフィルタよりもやや複雑です。というのも、タグを使えば何でもできるから です。

テンプレートタグの概要

前に、このドキュメントで、テンプレートシステムはコンパイルとレンダリングと いう二段階のプロセスで動作すると説明しました。カスタムテンプレートタグを定 義するには、コンパイルがどのように行われ、レンダリングがどのように行われる かを指定する必要があります。

テンプレートをコンパイルする時、Django は元のテンプレートテキストを「ノード (node)」に分割します。各ノードは django.template.Node のインスタンスで あり、 render() メソッドを持ちます。コンパイル済みのテンプレートは単な る Node オブジェクトの集まりに過ぎません。コンパイル済みのテンプレート オブジェクトに対して render() を呼び出すと、テンプレートは指定されたコ ンテキストを使ってノードリストの各 Node に対して render() を呼び出 します。戻り値は全て連結され、テンプレートのレンダリング結果になります。

従って、カスタムテンプレートタグを定義するには、元のテンプレートタグをどう やって Node (コンパイル関数: compilation function) に変換し、個々のノー ドの 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])

注意:

  • parser はテンプレートパーザオブジェクトです。上の例では特に必要で はありません。
  • token.contents はタグの中身を表す文字列です。上の例では 'current_time "%Y-%m-%d %I:%M %p"' です。
  • token.split_contents() メソッドは、クオートされた文字列はそのまま にして、引数をスペースで分割します。 token.contents.split() の方 が直接的なように思えますが、クオート中の空白も含め、 全ての スペー スを区切りとみなすので、これは頑健な方法とはいえません。常に token.split_contents() を使った方がよいでしょう。
  • この関数は、何らかの構文エラーが生じた場合、分かりやすいメッセージ付 きで django.template.TemplateSyntaxError を送出せねばなりません。
  • TemplateSyntaxError 例外は tag_name 変数を使っています。 エラーメッセージにタグの名前をハードコードしてはなりません。なぜなら 関数とタグの名前がカップリングしてしまうからです。タグに引数があって もなくても、 token.contents.split()[0] は常にタグの名前と同じです。
  • この関数は、タグの処理に必要な全ての情報を持った CurrentTimeNode を返します。この例の場合、タグには引数 "%Y-%m-%d %I:%M %p" が渡さ れただけです。テンプレートタグの先頭や末尾のクオートは format_string[1:-1] ではぎ取られています。
  • パージング処理は極めて低水準で実現されています。 Django の開発者達は、 EBNF 文法のようなテクニックを使って、このパージングシステムの上に小さ なフレームワークを書く実験を重ねて来ましたが、その結果はテンプレート エンジンをひどく低速にしてしまいました。というわけで、低水準にしてい るのは、それが最も高速だからです。

レンダラを書く

カスタムタグを書く二つめのステップは、 render() メソッドを持つ Node クラスの定義です。

上の例の続きとして話を勧めると、 CurrentTimeNode を定義する必要がありま す:

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)

注意:

  • __init__()do_current_time() から format_string を受け 取ります。 Node へのオプション/パラメタ/引数は、常に __init__() を介して渡すようにしてください。
  • 実際の処理を実現するのは render() メソッドです。
  • render()TemplateSyntaxError やその他の例外を送出してはな りません。フィルタと同様、失敗は暗黙のうちに処理されるようにせねばな りません。

一つのテンプレートは、繰り返しパージングされることなく複数のコンテキストを レンダリングすることがあるので、このようなコンパイルとレンダリングの脱カッ プリングは究極的には効率的なテンプレートシステムを実現します。

自動エスケープの注意点

テンプレートタグの出力は、自動エスケープフィルタで自動的に処理 されません 。とはいえ、テンプレートタグを書くときには、二つのことを心に留 めて置く必要があります。

まず、テンプレートタグの render() が結果をコンテキスト変数に保存する場 合 (文字列でそのまま返さない場合) には、必要なら mark_safe() を呼び出し ておく必要があります。その変数を最終的にレンダした時点で、変数は自動エスケー プの設定の影響を受けるので、自動エスケープから保護したい変数はそのようにマー クしておく必要があるのです。

また、テンプレートタグがサブレンダリングのために新たなコンテキストを生成す る場合、そのコンテキスト変数に autoescape 属性を設定してください。 Context クラスの __init__ メソッドは、この目的のために、 autoescape というパラメタをとれるようになっています。例えば:

def render(self, context):
    # ...
    new_context = Context({'var': obj}, autoescape=context.autoescape)
    # ... 新しいコンテキストで何かする ...

このような状況はあまりありませんが、テンプレートタグ内で別のテンプレートを レンダする場合には便利です。例えば:

def render(self, context):
    t = template.loader.get_template('small_fragment.html')
    return t.render(Context({'var': obj}, autoescape=context.autoescape))

上の例で、 context.autoescape の値を渡し忘れると、出力の変数は 全て 自動エスケープされてしまい、その結果、テンプレートタグを {% autoescape off %} ブロック内で使った場合の出力が期待 とは違うものになってしまいます。

スレッドセーフ性を考慮する

Django 1.2 で新たに登場しました: リリースノートを参照してください

ノードがパージングされると、そのノードの render メソッドは何度も呼び出され る可能性があります。 Django はマルチスレッド環境で実行されることがあるので、 2 つの別のリクエストに対して 1 つのノードが異なるレスポンスコンテキストで同時に レンダリングされることがありえます。それ故、テンプレートタグをスレッドセーフに しておくことが大切です。

テンプレートタグがスレッドセーフかどうかを確認するには、ノード自身に決して状態 情報を保存してはいけません。例えば、 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()

しかし上のスニペットが同時に 2 つのテンプレートレンダリングを行うと:

  1. スレッド 1 がループの 1 回目を実行し、 CycleNode.render() が ‘row1’ を 返します。
  2. スレッド 2 がループの 1 回目を実行し、 CycleNode.render() が ‘row2’ を 返します。
  3. スレッド 1 がループの 2 回目を実行し、 CycleNode.render() が ‘row1’ を 返します。
  4. スレッド 2 がループの 2 回目を実行し、 CycleNode.render() が ‘row2’ を 返します。

CycleNode は繰り返し実行されますが、その繰り返しはグローバルなものです。スレッ ド 1 とスレッド 2 に関しては、常に同じ値が返ります。これは明らかに私たちが望ん でいない動作です!

この問題に対応するために Django は、現在レンダされているテンプレートのコンテキ ストに関連づけられた render_context を提供します。 render_context は Python の辞書と同じように振る舞い、 render メソッドの実行ごとに Node の状態を保存するために使われます。

CycleNode の実装を render_context を使うようにリファクタリングしてみま しょう:

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()

あるノードが存在するあいだに属性として変化しないであろうグローバルな情報の保存 については完全に安全であることに注意してください。 CycleNode の場合、 cyclevars 引数はノードがインスタンス化された後に変化しないので、それを render_context に入れる必要はありません。しかし、 CycleNode の繰り返し 状態のような、現在レンダされているテンプレートに特有の状態情報は、 render_context に保存されるべきです。

Note

render_context のなかで CycleNode 特有の情報を取得するために self を使ったことに注目してください。複数の CycleNodes が同じテン プレートのなかに存在するかもしれないので、別のノードの状態情報をめちゃく ちゃにしないように気をつけなければいけません。そのための最も簡単な方法は、 render_context のキーとして self を使うことです。複数の状態変更を 追跡したければ、 render_context[self] に辞書を格納してください。

タグの登録

最後に、上の「カスタムテンプレートフィルタを書く」の節で説明したように、タ グをモジュールの Library インスタンスに登録します:

register.tag('current_time', do_current_time)

tag() メソッドは二つの引数をとります:

  1. テンプレートタグの名前、文字列です。省略すると、コンパイル関数の名前 を使います。
  2. コンパイル関数。 Python の関数です (関数名を表す文字列ではありません)。

フィルタの登録と同様、この関数をデコレータとして使えます:

@register.tag(name="current_time")
def do_current_time(parser, token):
    ...

@register.tag
def shout(parser, token):
    ...

上の例の下の定義のようにして name 引数を省略すると、 Django は関数名を そのままフィルタ名として使います。

テンプレート変数をタグに渡す

token.split_contents() を使えば、テンプレートタグにいくつでも引数を渡せ ますが、引数はすべて文字列リテラルになってしまいます。そのため、テンプレー トタグの引数に動的なコンテンツ (テンプレート変数) を渡すには、もうひと工夫 必要です。

先に挙げた例では、現在時刻を文字列にフォーマットして文字列で値を返していま したが、今度は以下のように DateTimeField の値を 渡してテンプレートタグに日付と時刻をフォーマットさせたいとしましょう:

<p>この投稿が最後に更新されたのは{% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %} です。</p>

まず、 token.split_contents() は以下の 3 つの値を返します:

  1. タグ名である format_time
  2. blog_entry.date_updated という 文字列
  3. フォーマット文字列 “%Y-%m-%d %I:%M %p” 。 split_contents() は、 文字列リテラルの先頭と末尾にあるクオート文字を除去しません。

今度は、タグの定義は以下のようになります:

from django import template
def do_format_time(parser, token):
    try:
        # split_contents() はクオート付き文字列を分解しない
        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])

レンダラの方も変更して、 blog_entry オブジェクトの date_updated プ ロパティの実際の中身を取り出せるようにせねばなりません。これは django.templateVariable() クラスで実現します。

Variable クラスの使い方は、単に解決されるべき変数名を渡してインスタンスを 作り、それから variable.resolve(context) を呼びます。従って:

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 ''

現在のページのコンテキストに、 Variable に渡した名前がなかった場合、 名前解決の際に VariableDoesNotExist 例外が送出されます。

簡単なタグ

多くのテンプレートタグは、文字列やテンプレート変数への参照をいくつか引数と して取り、入力引数や外部の何らかの情報に基づいて処理を行った後、文字列を返 します。例えば、上で書いた current_time タグがその例で、フォーマット文 字列を指定して、時刻を文字列の形で返すようになっています。

この種のタグを簡単に作成できるようにするため、 Django では simple_tag というヘルパー関数を提供しています。この関数は django.template.Library のメソッドで、何らかの関数を引数にとり、その関数を render メソッドにラッ プし、これまでに述べてきたいくつかのお作法を適用した後、テンプレートシステ ムにタグを登録します。

simple_tag を使うと、上の 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):
    ...

simple_tag ヘルパ関数については、以下の点に注意してください:

  • 引数の個数チェックなどは関数の呼び出しの際に行われるので、わざわざ自 分でチェックしなくてもかまいません。
  • 関数に渡される引数がクオートされている場合、クオートは自動的に取り去 られます。従って、関数は通常の文字列を受け取ることになります。
  • 引数がテンプレート変数なら、関数に渡されるのは変数自体ではなく、 変数の現在値になります。
Django 1.3 で新たに登場しました: リリースノートを参照してください

もしテンプレートタグが現在のコンテキストにアクセスする必要があるなら、タグを登 録する時に takes_context 引数を使うことができます:

# ここで最初の引数は "context" と呼ばれなければ *いけません*
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)

takes_context オプションの動きについてもっと詳しく知りたければ 埋込タグ の章を読んでくださ い。

Django 1.4 で新たに登場しました: リリースノートを参照してください

タグの名前を変更したければ、カスタムの名前を与えることができます:

register.simple_tag(lambda x: x - 1, name='minusone')

@register.simple_tag(name='minustwo')
def some_function(value):
    return value - 2
Django 1.4 で新たに登場しました: リリースノートを参照してください

simple_tag 関数は任意の位置指定引数 (positional arguments) またはキーワー ド引数を受け取ります。例:

@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|lower profile=user.profile %}

埋め込みタグ

テンプレートタグによくあるもう一つのタイプは、 別の テンプレートをレンダ して表示するものです。例えば、 Django の admin インタフェースではカスタムの テンプレートタグを使って「追加/変更」フォームページのボタンを表示していま す。これらのボタンはいつも同じように表示されていますが、リンクのターゲット は編集対象のオブジェクトによって変わります。従って、こうした処理は、小さな テンプレートを使って現在のオブジェクトに応じた値を埋めるのが最良の方法と言 えます。 (admin の submit_raw がそのタグです)。

こうしたタグのことを 「埋め込みタグ (inclusion tag)」 と呼びます。

埋め込みタグの書き方を説明するには、例を挙げるのがベストでしょう。 チュートリアル で作成した Poll オブジェクトを 例に、ある Poll オブジェクトに対する選択肢のリストを出力するようなタグ を書いてみましょう。タグは以下のようになります:

{% show_results poll %}

出力は以下のようになります:

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

まず、引数を受け取って、対応するデータの辞書を生成するような関数を定義しま す。ここで重要なのは、辞書を返さねばならず、それ以外の複雑なデータを返して はならないということです。戻り値はテンプレートフラグメントをレンダするとき のテンプレートコンテキストに使われます。例を示します:

def show_results(poll):
    choices = poll.choice_set.all()
    return {'choices': choices}

次に、タグの出力をレンダする際に使うテンプレートを作成します。このテンプレー トはタグ固有のもので、テンプレートデザイナではなくタグの開発者が書くべきも のです。上の例に従えば、テンプレートはとても単純な形になります:

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

さて、 Library オブジェクトの inclusion_tag() メソッドを呼び出して、 埋め込みタグを作成し、登録しましょう。上のテンプレートがテンプレートローダ の検索対象ディレクトリ下にある result.html だとすると、タグの登録は以下 のようにして行います:

# Here, register is a django.template.Library instance, as before
register.inclusion_tag('results.html')(show_results)
Django 1.4 で変更されました: リリースノートを参照してください

ここでもデコレータ構文を使えます。従って、関数を定義する時点で下記のようにも書 けます:

@register.inclusion_tag('results.html')
def show_results(poll):
    ...

時として、埋め込みタグが大量の引数を取るようになっていて、テンプレートの作 者が全ての引数やその順番を管理しきれなくなるような場合があります。この問題 を解決するために、 Django では埋め込みタグに対して takes_context オプショ ンを提供しています。テンプレートタグの作成時に takes_context を指定する と、タグは必須の引数を取らなくなり、タグのラップしている Python 関数は単一 の引数を取るようになります。この引数には、タグが呼び出されたときのテンプレー トコンテキストが入ります。

例えば、メインページに戻るためのリンクに使う home_link および home_title という変数の入ったコンテキストの下で使う埋め込みタグを書いて いるとしましょう。タグの Python 関数は以下のようになります:

# 最初の引数は *必ず* "context" と *呼ばねばなりません*
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }
# 定義したカスタムタグを takes_context=True の埋め込みタグとして登録する
register.inclusion_tag('link.html', takes_context=True)(jump_link)

(関数の最初のパラメタは 必ず context という名前に せねばならない の で注意してください。)

register.inclusion_tag() の行で、 takes_context=True を指定し、テン プレートの名前を指定しています。 link.html テンプレートの中は以下のよう になります:

Jump directly to <a href="{{ link }}">{{ title }}</a>.

このカスタムタグを使う場合は常に、まずライブラリを呼び出しておき、下記のよ うに引数なしでタグを呼び出します:

{% jump_link %}

takes_context=True を使っている場合、テンプレートタグに引数を渡す必要は ありません。タグが自動的にコンテキストにアクセスします。

takes_context パラメタはデフォルトでは False です。この値を True に設定すると、タグの引数にはコンテキストオブジェクトが渡されます。この点だ けは前述の inclusion_tag の例と異なります。

Django 1.4 で新たに登場しました: リリースノートを参照してください

inclusion_tag 関数は任意の数の位置指定引数またはキーワード引数を受け取るこ とができます。例:

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

すると、テンプレートでは、任意の数の引数を空白区切りでテンプレートタグに渡すこ とができます。 Python でそうなのと同じく、キーワード引数の値はイコール記号 (“=”) を使って、位置指定引数の後に渡される必要があります。例:

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

コンテキスト中の変数を設定する

上の例は、単に値を出力するという単純なものでした。一般的な話として、テンプ レートタグが値を出力するだけでなく、テンプレート変数の値を設定できたりする ともっと柔軟性が増すでしょう。そうすれば、テンプレートの作者はテンプレート タグのつくり出す変数を再利用できるようになります。

コンテキスト中に変数を設定するには、 render() メソッドで渡されるコンテ キストオブジェクトを辞書として代入するだけです。先程の 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 ''

render() は空文字列を返していることに注意して下さい。 render() は常に文字列を出力せねばなりません。全てのテンプレートタグが変数の設定 しかしなければ、 render() は空の文字列を返すだけです。

新しいバージョンのタグの使い方を示します:

{% current_time "%Y-%M-%d %I:%M %p" %}<p>時刻は {{ current_time }} です。</p>

コンテキストにおける変数のスコープ

コンテキストにセットされるいかなる変数も、でそれが定義されたのと同じテンプ レートブロック (block) の中でしか使うことができません。この挙動は意図 的なもので、変数のスコープを提供しています。そのため異なるブロックのコンテ キストが変数が衝突することがありません。

ところで、 CurrentTimeNode2 には問題があります: 変数名 current_time がハードコードされているのです。これはすなわち、テンプレートの他の部分で {{ current_time }} を使わないようにせねばならないことを意味します。とい うのも、 {% current_time %} は変数の値を盲目的に上書きしてしまうからで す。より綺麗な解法は、出力用の変数名を定義させるというものです:

{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}
<p>時刻は {{ my_current_time }} です。</p>

この機能を実現するには、コンパイル関数と Node の両方をリファクタして、 以下のようにせねばなりません:

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):
    # このバージョンはタグのコンテンツを解析するのに正規表現を使っています
    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)

ここでの違いは、 do_current_tume() がフォーマット文字列と変数名を取り、 CurrentTimeNode3 に渡すという点です。

Finally, if you only need to have a simple syntax for your custom context-updating template tag, you might want to consider using an assignment tag.

Assignment タグ

Django 1.4 で新たに登場しました: リリースノートを参照してください

コンテキストに変数をセットするタグを簡単に作れるように、 Django は assignment_tag ヘルパー関数を提供しています。この関数は simple_tag と同じように動作しま す。ただしこちらは指定したコンテキスト変数でのタグの結果を、直接出力する代わり に保存します。

少し前に示した current_time 関数はこのように書くことができます:

def get_current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

register.assignment_tag(get_current_time)

デコレータ構文も使えます:

@register.assignment_tag
def get_current_time(format_string):
    ...

as 引数の後ろに変数名を続けることで、この結果をテンプレート変数に保存し、 ちょうどいい場所で自分で出力することができます:

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

テンプレートタグが現在のコンテキストにアクセスする必要がある場合、 タグを登録する時に takes_context 引数を使うことができます:

# ここで最初の引数は "context" でなければ *いけません*
def get_current_time(context, format_string):
    timezone = context['timezone']
    return your_get_current_time_method(timezone, format_string)

register.assignment_tag(takes_context=True)(get_current_time)

デコレータ構文を使うこともできます:

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

takes_context オプションの動きについてもっと詳しく知りたければ 埋込タグ の章を読んでくださ い。

assignment_tag 関数は任意の数の位置指定引数またはキーワード引数を受け取る ことができます。例:

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

すると、テンプレートでは、任意の数の引数を空白区切りでテンプレートタグに渡すこ とができます。 Python でそうなのと同じく、キーワード引数の値はイコール記号 (“=”) を使って、位置指定引数の後に渡される必要があります。例:

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile as the_result %}

他のブロックタグに到達するまでパージングする

テンプレートタグは違いに連携させられます。例えば、標準の {% 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 ''

Note

{% comment %} の現実の実装はこれとは少し違っていて、 {% comment %}{% endcomment %} の間に壊れたテンプレートタグが現 れることを許容します。 parser.delete_first_token() の前で parser.parse(('endcomment',)) の代わりに parser.skip_past('endcomment') を呼び、ノードリストの生成を避けること によってそれを行っています。

parser.parse() は「そこまで読み進める」ブロックタグからなるタプルを引数 にとります。 parser.parse()django.template.NodeList のインスタ ンスを返します。このインスタンスは、タプルに指定したブロックタグのいずれか に到達する「より以前」に、パーザが出会った全ての Node オブジェクトから なるリストです。

上の例の "nodelist = parser.parse(('endcomment',))" では、 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, {{ your_name }}.{% endupper %}

これまでの例と同様、 parser.parse() を使います。ただし、今回は得られた nodelistNode に渡します:

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) が、新たに導入 されたコンセプトを表しています。

複雑なレンダリングについて他にも例を見たければ、 {% if %}{% for %}, {% ifequal %}, {% ifchanged %} のソースを参照してください。ソースは django/template/defaulttags.py にあります。