revision-up-to: | 17812 (1.4) |
---|
Django テンプレートシステムには、いろいろな 組み込みタグとフィルタ が付属していて、アプリケーションのプレゼンテーショ
ンロジックにまつわる問題を解決できます。とはいえ、コアのテンプレートタグプリミ
ティブだけでは、要求を満たせない場合もあります。そういう場合のために、テンプ
レートエンジンを拡張できます。Python で自作のタグやフィルタを書き、テンプレー
ト上で {% load %}
タグを使って使えるのです。
カスタムのテンプレートタグやフィルタは Django のアプリケーション内に置きま す。既存のアプリケーションに関連があるのなら、そのアプリケーションにバンド ルすればよいでしょう。そうでなければ、コードを入れておくための新たなアプリ ケーションを作成します。
アプリケーション内には、 templatetags
ディレクトリを置かねばなりません。
このディレクトリは、 models.py
や views.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.py
や
django/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()
は二つの引数を取ります:
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_safe
と needs_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 の自動エスケープとどう関わっ ているかに少し配慮しておく必要があります。まず、テンプレートのコード内では、 3 種類の文字列が受け渡しされています:
生の文字列 は、 Python ネイティブの str
や unicode
型の
オブジェクトです。出力時、自動エスケープが有効であればエスケープされ、
そうでなければ変更されません。
safe 文字列 は、出力時にさらなるエスケープ処理を行わなくてもよ い文字列です。safe 文字列は、必要なエスケープが済んでいる文字列です。 クライアント側でそのまま解釈されるべき生の HTML を含んだ文字列を表現 するのに使われます。
内部的には、これらの文字列は SafeString
や SafeUnicode
型で表
現されています。 SafeString
と SafeUnicode
は SafeData
を
共通の基底クラスに持つので、以下のようにすれば判別できます:
if isinstance(value, SafeData):
# "safe" 文字列を扱う処理をここに
“エスケープが必要” であるとマークされている文字列 は、
autoescape
ブロックの指定に関係なく、 必ず エスケープされます。
マークされている文字列は、自動エスケープの設定に関係なく、一度だけエ
スケープ処理を受けます。
内部的には、エスケープ必要文字列は、 EscapeString
や
EscapeUnicode
型で表現されています。通常、これらの型のことを気に
する必要はありません。 EscapeString
や EscapeUnicode
は、
escape
フィルタの実装のために用意されています。
テンプレートフィルタのコードは、以下の二つのうちどちらかに落ち着きます:
フィルタコードの出力が、 HTML 出力として安全でない文字 (<
,
>
, '
, "
or &
) を含まない場合。この場合、自動エスケー
プの処理は全て Django に任せられます。必要なのは、フィルタ関数の
is_safe
属性に True
を設定しておくことだけです:
@register.filter(is_safe=True)
def myfilter(value):
return value
この属性を設定しておくと、このフィルタは「安全な」文字列を渡したとき には出力は「安全な」ままであり、「安全でない」文字列を渡した時には、 必要に応じて自動的にエスケープします。
is_safe
は、「このフィルタは安全 (safe) であり、安全でない HTML
を生成するおそれがない」という意味だと考えてください。
is_safe
が必要なのは、通常の文字列操作で、 SafeData
オブジェ
クトを str
や unicode
オブジェクトに変換するものが数多くあ
るからです。全てを try と catch で拾うのは難しいので、 Django はフィ
ルタ処理が完了した時点でダメージを回復する戦略を取っています。
例えば、入力の末尾に xx
を追加するようなフィルタを書くとします。
このフィルタは危険な HTML 文字を出力しないので、フィルタ関数を
is_safe
でマークしておきます:
@register.filter(is_safe=True)
def add_xx(value):
return '%sxx' % value
このフィルタをテンプレート上の自動エスケープが有効な部分で使うと、 Django は “safe” にマークされていない入力の時にフィルタの出力をエス ケープします。
デフォルトでは、 is_safe
は False
なので、 is_safe
が
必要でなければ無視してかまいません。
フィルタを作成するときには、フィルタが本当に安全な文字を安全なままに
出力するのかをよく考えてください。例えば、フィルタが文字を 削除 す
る場合、出力結果に、対応の取れていないタグや不完全なエンティティ表現
が意図せず混じる可能性があります。例えば、 >
を入力から削ってし
まうと、 <a>
の出力は <a
になってしまい、問題を起こさないよ
うにするためには、出力をエスケープせねばならなくなってしまいます。同
様に、セミコロン (;
) を除去すると、 &
は &
になっ
てしまい、有効なエンティティ表現でないために、エスケープが必要になっ
てしまいます。たいていのケースでは、このようなトリッキーなことは起こ
りませんが、コードをレビューする際にはこの手のことが起こらないように
よく確認してください。
フィルタを is_safe
にマークするとフィルタに文字列を返すように強制させる
ことになります。真偽値や非文字列型の値を返すフィルタを is_safe
にマーク
すると、おそらく意図しない結果を起こすことになります (真偽値の False が文字
列の ‘False’ に変換されるなど) 。
もう一つは、フィルタのコードで必要なエスケープを行うというものです。 出力に新たな HTML マークアップを含めたい場合に必要です。この場合、 出力する HTML マークアップがそれ以上エスケープされないよう、出力を safe にする必要があるので、入力は自分で扱わねばなりません。
出力を safe 文字列としてマークするには、
django.utils.safestring.mark_safe()
を使います。
ただし、注意してください。出力を safe であるとマークするだけでは十分 ではありません。出力が本当に safe である ことを保証せねばならず、 それは自動エスケープが有効か無効かで変わります。自動エスケープがオン でもオフでも使え、テンプレートの作者が簡単に扱えるフィルタを書くのが 理想です。
現在の自動エスケープ状態をフィルタに教えるには、関数の
needs_autoescape
属性を True
に設定します (この属性を設定
しない場合のデフォルト値は False
です)。 needs_autoescape
を True
にすると、フィルタ関数には 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
属性は何ら影響をもた
ないのです。
リリースノートを参照してください
is_safe
と needs_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
リリースノートを参照してください
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]
ではぎ取られています。カスタムタグを書く二つめのステップは、 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 %}
ブロック内で使った場合の出力が期待
とは違うものになってしまいます。
リリースノートを参照してください
ノードがパージングされると、そのノードの 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 つのテンプレートレンダリングを行うと:
CycleNode.render()
が ‘row1’ を
返します。CycleNode.render()
が ‘row2’ を
返します。CycleNode.render()
が ‘row1’ を
返します。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()
メソッドは二つの引数をとります:
フィルタの登録と同様、この関数をデコレータとして使えます:
@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 つの値を返します:
format_time
。blog_entry.date_updated
という 文字列 。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.template
の Variable()
クラスで実現します。
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
ヘルパ関数については、以下の点に注意してください:
リリースノートを参照してください
もしテンプレートタグが現在のコンテキストにアクセスする必要があるなら、タグを登
録する時に 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
オプションの動きについてもっと詳しく知りたければ
埋込タグ の章を読んでくださ
い。
リリースノートを参照してください
タグの名前を変更したければ、カスタムの名前を与えることができます:
register.simple_tag(lambda x: x - 1, name='minusone')
@register.simple_tag(name='minustwo')
def some_function(value):
return value - 2
リリースノートを参照してください
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)
リリースノートを参照してください
ここでもデコレータ構文を使えます。従って、関数を定義する時点で下記のようにも書 けます:
@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
の例と異なります。
リリースノートを参照してください
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.
リリースノートを参照してください
コンテキストに変数をセットするタグを簡単に作れるように、 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()
を使います。ただし、今回は得られた
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)
が、新たに導入
されたコンセプトを表しています。
複雑なレンダリングについて他にも例を見たければ、 {% if %}
や
{% for %}
, {% ifequal %}
,
{% ifchanged %}
のソースを参照してください。ソースは
django/template/defaulttags.py
にあります。
Oct 26, 2017