独自のテンプレートタグとフィルタを作る

Django のテンプレート言語は、アプリケーションのプレゼンテーションロジックのニーズに対応するように設計された、多種多様な 埋め込みタグやフィルタ を搭載しています。それでもなお、テンプレート構成要素のコア・セットでカバーされていない機能が必要になることもあるでしょう。そのときは Python を使用し、独自のタグやフィルタを定義することによって、テンプレートエンジンを拡張できます。その上で、{% load %} タグを使用すると、テンプレートでそれらの機能を利用することができるようになります。

コードのレイアウト

独自のテンプレートタグやフィルタを指定するための最も一般的な場所は、Django のアプリケーションの内部です。それらが既存のアプリに関連するものである場合は、この場所にバンドルするのが最適です。それ以外の場合は、新しいアプリケーションに追加されてしまいます。Django のアプリケーションが INSTALLED_APPS に追加されると、以下に記載された従来の場所に定義したタグは、自動的にテンプレート内に読み込むことが可能になります。

アプリケーションは、"models.py" や "views.py" などと同じレベルに "templatetags" ディレクトリを含むべきです。まだ存在していない場合は、ディレクトリが Python パッケージとして扱われるようにするため、"__init__.py" を忘れないでください。

開発用サーバが自動的にリスタートしない場合

templatetags モジュールを追加した後、テンプレートでタグやフィルタを使用する前に、サーバーを再起動する必要があります。

カスタムタグやフィルタは templatetags ディレクトリ内のモジュールにあります。モジュールファイルの名前は、あとでタグをロードして使うので、別のアプリでカスタムタグやフィルタと衝突しない名前を選択するように心がけてください。

例えば、カスタムタグ/フィルタが poll_extras.py というファイルにある場合、アプリのレイアウトは次のようになります:

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

そして、テンプレートでは次のように使います:

{% load poll_extras %}

カスタムタグを含むアプリケーションは、{% load %} タグを機能させるために INSTALLED_APPS 内に記述される必要があります。これは、セキュリティ機能です: 毎回の Django のインストールでこれらへのアクセスを有効化することなく、単一のホストマシン上の多数のテンプレートライブラリに対して Python のコードをホストできるようにします。

templatetags パッケージに入れられるモジュールの数に制限はありません。ただ、{% load %} ステートメントは、アプリケーションの名前ではなく、与えられた Python のモジュール名に対してタグ/フィルタをロードすることに注意してください。

有効なタグライブラリにするため、モジュールは、register という名前のモジュールレベルの変数を含む必要があります。これは、すべてのタグとフィルタが登録されている template.Library のインスタンスです。そのため、あなたのモジュールの上部に、次のコードを記述してください:

from django import template

register = template.Library()

あるいは、テンプレートタグのモジュールは、DjangoTemplates への 'libraries' 引数を通じて登録することもできます。テンプレートタグをロードするときに、テンプレートタグのモジュール名とは異なるラベルを使用したい場合に便利です。また、アプリケーションをインストールせずに、タグを登録できるようになります。

背景

豊富な例については、 Django のデフォルトのフィルタとタグのソースコードを読んでください。それぞれ django/template/defaultfilters.pydjango/template/defaulttags.py にあります。

load タグの詳細については、ドキュメントを参照してください。

独自のテンプレートフィルタを記述する

独自のフィルタは、1つか2つの引数を取るPythonの関数です:

  • 変数の値 (インプット) -- 文字列とは限りません。
  • 引数の値 -- デフォルト値を持つことも、完全に省略することもできます。

例えば、フィルタ {{ var|foo:"bar" }} の中で、フィルタ foo は変数 var と引数 "bar" を渡されます。

テンプレート言語は例外処理を提供しないので、テンプレートフィルタから生成された例外はすべてサーバーエラーとして公開されます。したがって、フィルタ関数は、返すべき妥当なフォールバック値がある場合には例外を発生させないようにする必要があります。テンプレートの明確なバグを表すインプットの場合、例外を発生させる方が、バグを隠すサイレントな失敗よりも適切でしょう。

以下はフィルタ定義の例です:

def cut(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(arg, "")

そして、以下はフィルタがどのように使われるかの例です:

{{ somevariable|cut:"0" }}

ほとんどのフィルタは引数を取りません。この場合、次のように関数の第2引数を省略してください:

def lower(value):  # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

独自のフィルタを登録する

django.template.Library.filter()

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

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

Library.filter() メソッドは 2 つの引数を取ります:

  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 引数を省略した場合、上記の 2 番目の例と同じように、Django はフィルタ名として関数の名前を利用します。

最後に、register.filter() は 3 つのキーワード引数 (is_safeneeds_autoescapeexpects_localtime) を受け入れます。これらの引数は、後述の フィルタと自動エスケープフィルタとタイムゾーン の中で説明されています。

文字列を要するテンプレートフィルタ

django.template.defaultfilters.stringfilter()

第 1 引数として文字数を要求するだけのテンプレートフィルタを記述している場合、デコレータ stringfilter を使う必要があります。これは、関数に渡される前にオブジェクトを文字列に変換します。

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

register = template.Library()


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

これにより、このフィルタに整数を渡したとしても AttributeError は発生しません。(整数は lower() メソッドを持ちませんが。)

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

独自のフィルタを作成する場合、フィルタがDjangoの自動エスケープの挙動とどのように関連するか考慮してください。2種類の文字列がテンプレートコードに渡されることに留意してください:

  • Raw strings はPythonのネイティブの文字列です。出力時に、自動エスケープが有効な場合はエスケープされ、それ以外の場合は変更されません。

  • Safe strings は出力時にさらなるエスケープがされないように安全とマークされた文字列です。必要なエスケープはすでに行われています。これらはクライアント側でそのまま解釈されることを目的とした生のHTMLを含む出力によく利用されます。

    内部では、これらの文字列は SafeString 型になります。次のようなコードでこれらの値を検証することができます:

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # Do something with the "safe" string.
        ...
    

テンプレートフィルタのコードは、次の2つの状況のいずれかに当てはまります:

  1. フィルタが結果に新たにHTMLで安全でない文字 (<, >, ', " or &) を導入しない場合。この場合、Djangoにすべての自動エスケープ処理を任せることができます。フィルタ関数を登録する際に、 is_safe フラグを True に設定するだけです。次のようにします:

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

    このフラグは、"安全"な文字列がフィルタに渡された場合、結果も依然として"安全"であることをDjangoに伝えます。そして、安全でない文字列が渡された場合、必要に応じてDjangoが自動的にエスケープします。

    この場合、"このフィルタは安全である ―― それはどんな非安全なHTMLも導入する可能性はない。" と考えて構いません。

    is_safe が必要な理由は、 SafeData オブジェクトを普通の str オブジェクトに変換するような標準的な文字列操作が多数存在し、これら全てを捉えることが非常に難しいため、Djangoではフィルタの処理が完了した後に問題を修正するためです。

    例えば、任意の入力の末尾に文字列 xx を追加するフィルタがあるとします。これは結果に危険なHTML文字を導入しません(元々存在していたものを除く)。よって、フィルタを is_safe でマークすべきです:

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

    このフィルタが自動エスケープが有効なテンプレートで使用される場合、入力が既に"安全"としてマークされていない限り、Djangoは出力をエスケープします。

    デフォルトでは is_safeFalse ですが、それが必要でないフィルタでは省略できます。

    フィルタが本当に安全な文字列を安全なまま残すかどうかを判断するときには注意してください。文字を 除去 している場合、不注意で結果の中に不均衡なHTMLタグやエンティティが残ってしまうかもしれません。例えば、入力から > を取り除くと <a><a になってしまうかもしれません。同様に、セミコロン(;)を削除すると、&amp;&amp に変わります。ほとんどの場合、ここまでトリッキーになることはありませんが、コードをレビューする際はこのような問題がないか注意してください。

    フィルタを is_safe としてマークすると、フィルタの戻り値が文字列に強制されます。フィルタがブール値や他の文字列でない値を返すべき場合、 is_safe とマークすると意図しない結果になる可能性があります(例えば、ブール値の False を文字列の 'False' に変換するなど)。

  2. または、フィルタのコードで必要なエスケープ処理を手動で行うこともできます。これは、結果に新しいHTMLマークアップを導入する場合に必要です。HTMLマークアップがさらにエスケープされないように、出力を安全としてマークしたい場合は、入力を自分で処理する必要があります。

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

    ただし注意が必要です。出力を安全としてマークするだけでは不十分です。本当にそれが 安全である ことを確認する必要があり、行うべきことは自動エスケープが有効かどうかによって異なります。テンプレート作成者のために、自動エスケープがオンまたはオフのどちらのテンプレートでも操作できるフィルタを書くことが考えられます。

    フィルタが現在の自動エスケープの状態を知るためには、フィルタ関数を登録する際に needs_autoescape フラグを True に設定します。(このフラグを指定しない場合、デフォルトは False です)。このフラグは、追加のキーワード引数 autoescape をDjangoに渡すようにフィルタ関数に伝えます。この引数は自動エスケープが有効の場合は True 、そうでない場合は False となります。 autoescape パラメータのデフォルトは True に設定しておくことが推奨されます。これにより、Pythonコードから関数を呼び出したときにデフォルトでエスケープが有効になります。

    例えば、文字列の最初の文字を強調するフィルタを書いてみましょう:

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=True):
        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 に通す必要があるかどうかを決定します(必要なければ、恒等関数を "escapse" 関数として使用します)。 conditional_escape() 関数は escape() に似ていますが、 SafeData インスタンス でない 入力のみをエスケープします。 SafeData インスタンスが conditional_escape() に渡された場合、データは変更されずにそのまま返されます。

    最後に、上の例では、HTMLがさらにエスケープされることなくテンプレートに直接挿入されるように、結果を安全としてマークすることを忘れないでください。

    この場合、 is_safe フラグを気にする必要はありません (フラグを指定しても何も問題はありませんが)。自動エスケープの問題を手動で処理して安全な文字列を返す場合、 is_safe フラグを指定しても何も変わりません。

警告

組み込みのフィルタを再利用するときに XSS 脆弱性を回避する

Django に組み込みのフィルタは、適切な自動エスケープを行い、クロスサイトスクリプティング(XSS)の脆弱性を回避するために、デフォルトで autoescape=True となっています。

古いバージョンの Django では、 autoescape のデフォルトは None ですので、 Django の組み込みのフィルタを再利用する際には注意してください。オートエスケープを有効にするには autoescape=True を渡す必要があります。

例えば、 urlize フィルタと linebreaksbr フィルタを組み合わせた urlize_and_linebreaks というカスタムフィルタを書きたい場合、フィルタは次のようになります:

from django.template.defaultfilters import linebreaksbr, urlize


@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
    return linebreaksbr(urlize(text, autoescape=autoescape), autoescape=autoescape)

次のように使います:

{{ comment|urlize_and_linebreaks }}

これは以下と同等です:

{{ comment|urlize|linebreaksbr }}

フィルタとタイムゾーン

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

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

このフラグがセットされていると、フィルタの最初の引数がタイムゾーンを意識した datetime である場合、Django は テンプレートにおけるタイムゾーン変換ルール に従って、フィルタに渡す前に現在のタイムゾーンに変換します。

独自のテンプレートタグを記述する

タグはあらゆることができるため、フィルタより複雑です。Django は、ほとんどのタイプのタグを簡単に書けるように、多数のショートカットを提供しています。まず最初にこうしたショートカットを見てから、ショートカットでは機能が不足している場合にゼロからタグを書く方法を説明します。

シンプルなタグ

django.template.Library.simple_tag()

多くのテンプレートタグは、文字列やテンプレート変数などの引数を取り、入力引数と外部情報のみに基づいて処理を行った後、結果を返します。 たとえば、current_time タグはフォーマット文字列を受け取り、その時刻を適切な文字列フォーマットとして返します。

これらのタイプのタグの作成を容易にするため、Django はヘルパー関数 simple_tag を提供しています。 この関数は django.template.Library のメソッドで、任意の数の引数を受け取る関数を取り、それを render 関数と上記で説明した他の必要なビットにラップし、そしてテンプレートシステムに登録します。

私たちの current_time 関数は、以下のように書くことができます:

import datetime
from django import template

register = template.Library()


@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

simple_tag ヘルパー関数について、注意すべきことがいくつかあります:

  • 必須の引数の数などのチェックは、すでにこの関数が呼ばれる時点までに完了しているため、自分でチェックをする必要はありません。
  • 引数を囲む引用符(もしあれば)はすでに取り除かれているので、プレーンな文字列を受け取ります。
  • もし引数がテンプレート変数だった場合、テンプレート中でタグが呼ばれた時点の値が渡されます。テンプレート変数そのものが渡されるわけではありません。

他のタグユーティリティとは異なり、 simple_tag は、テンプレートコンテキストが自動エスケープモードの場合、 conditional_escape() を通して出力を渡します。

追加のエスケープが不要な場合、コードに XSS 脆弱性が絶対にないと確信できるのであれば、 mark_safe() を使う必要があります。小さな HTML コードを作成する場合は、 mark_safe() の代わりに format_html() を使うことを強く推奨します。

テンプレートタグの中からコンテキストにアクセスしたい場合、タグを登録する際に takes_context 引数を使うことでできるようになります。

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context["timezone"]
    return your_get_current_time_method(timezone, format_string)

最初の引数は contextしなければならない ことに注意してください。

takes_context オプションがどのように動くかについて、詳しくは inclusion tag を参照してください。

タグの名前を変更する必要がある場合は、カスタム名を指定できます:

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


@register.simple_tag(name="minustwo")
def some_function(value):
    return value - 2

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

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

このようにすることで、テンプレートからはスペースで区切られた変数をいくつでもテンプレートタグに渡すことができます。Pythonの文法と同じように、キーワード引数の値は等号("=")を用いて位置引数の後に記述します。たとえば:

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

タグの処理結果を直接出力することのほかに、テンプレート変数に格納することも可能です。これは as に続けて変数名を書くことで実現可能です。これによって、タグの処理結果を好きなように出力することができるようになります。

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

Inclusion tag (インクルージョン・タグ)

django.template.Library.inclusion_tag()

テンプレートタグのもう一つの一般的なタイプは、 別の テンプレートをレンダリングしてデータを表示するものです。例えば、 Django の admin インタフェースでは、カスタムテンプレートタグを使って、"add/change" フォームページの下にあるボタンを表示しています。これらのボタンはいつも同じように見えますが、編集中のオブジェクトによってリンク先が変わります -- なので、現在のオブジェクトの詳細で埋められた小さなテンプレートを使うのに最適なケースです。(admin の場合、これは submit_row タグです)。

この種のタグは "inclusion tag" と呼ばれます。

Inclusion tag の書き方は例で示すのが一番わかりやすいでしょう。 チュートリアル で作成したような、与えられた 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() メソッドを呼び出して、 inclusion tag を作成し、登録します。例に従って、上記のテンプレートがテンプレートローダーによって検索されるディレクトリの results.html というファイルにある場合、次のようにタグを登録します:

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag("results.html")
def show_results(poll): ...

あるいは、 django.template.Template インスタンスを使って inclusion tag を登録することもできます:

from django.template.loader import get_template

t = get_template("results.html")
register.inclusion_tag(t)(show_results)

…これは、関数を最初に作成する際に行います。

時に、 inclusion tag は多くの引数を要求することがあり、テンプレート作者が全ての引数を渡したり、引数の順番を覚えたりするのは面倒です。これを解決するために、 Django は inclusion tag に takes_context オプションを用意しています。テンプレートタグを作成する際に takes_context を指定すると、タグには必要な引数がなくなり、 Python 関数の引数は 1 つになります――タグが呼び出された時点のテンプレートコンテキストです。

例えば、メインページを指す home_linkhome_title 変数を含むコンテキストの中で常に使用される inclusion tag を書くとします。Python の関数は次のようになります:

@register.inclusion_tag("link.html", takes_context=True)
def jump_link(context):
    return {
        "link": context["home_link"],
        "title": context["home_title"],
    }

最初の引数は contextしなければならない ことに注意してください。

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

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

そして、そのカスタムタグを使いたいときはいつでも、そのライブラリをロードして、引数なしで次のように呼び出します:

{% jump_link %}

takes_context=True を使用している場合、テンプレートタグに引数を渡す必要がないことに注意してください。自動的に context にアクセスします。

takes_context パラメータのデフォルトは False です。これを True に設定すると、この例のようにタグに context オブジェクトが渡されます。これが先ほどの 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 %}

高度なカスタムテンプレートタグ

カスタムテンプレートタグを作成するための基本的な機能だけでは不十分な場合があります。ご心配なく、 Django はテンプレートタグを一から構築するのに必要な内部機能への完全なアクセスを提供します。

簡単な概要

テンプレート・システムは、コンパイルとレンダリングの 2 段階のプロセスで動作します。カスタムテンプレートタグを定義するには、コンパイルの仕組みとレンダリングの仕組みを指定します。

Django はテンプレートをコンパイルするとき、生のテンプレートテキストを "ノード" に分割します。各ノードは django.template.Node のインスタンスで、 render() メソッドを持っています。コンパイルされたテンプレートは Node オブジェクトのリストです。コンパイルされたテンプレートオブジェクトに対して render() を呼び出すと、テンプレートは与えられたコンテキストで、ノードリスト内の各 Node に対して render() を呼び出します。 その結果はすべて連結され、テンプレートの出力となります。

このように、カスタムテンプレートタグを定義するには、生のテンプレートタグを Node に変換する方法(コンパイル関数)と、ノードの render() メソッドの動作を指定します。

コンパイル関数の書き方

テンプレートパーサーが見つけたそれぞれのテンプレートタグに対して、タグの内容とパーサーオブジェクト自身を使って Python 関数を呼び出します。この関数はタグの内容に基づいて Node インスタンスを返す役割を担います。

例えば、テンプレートタグの完全な実装である {% current_time %} を書いてみましょう。このタグは strftime() という構文で、タグで与えられたパラメータに従ってフォーマットされた現在の日時を表示します。何よりも先にタグの構文を決めるのは良いアイデアです。この場合、タグは次のように使用します:

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

この関数のパーサは、パラメータを取得して Node オブジェクトを作成する必要があります:

from django import template


def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires a single argument" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode(format_string[1:-1])

メモ:

  • 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 文法などのテクニックを使って、この構文解析システムの上に小さなフレームワークを書く実験をしましたが、 その実験ではテンプレートエンジンが遅すぎました。 低レベルなのは、それが最速だからです。

レンダラーの書き方

カスタムタグを書く2つ目のステップは、 render() メソッドを持つ Node サブクラスを定義することです。

上記の例の続きで、 CurrentTimeNode を定義する必要があります:

import datetime
from django import template


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() は通常は静かに失敗するべきです。しかし、場合によっては、特に context.template.engine.debugTrue の場合、このメソッドはデバッグを容易にするために例外を発生させることがあります。例えば、いくつかのコアタグは、引数の数や型を間違えると django.template.TemplateSyntaxError を発生させます。

最終的に、コンパイルとレンダリングを切り離すことで、効率的なテンプレートシステムが実現します。なぜなら、テンプレートは何度もパースされることなく、複数のコンテキストをレンダリングできるからです。

自動エスケープに関する注意点

テンプレートタグからの出力は自動エスケープフィルタに自動的に通されることは ありません (上で説明した simple_tag() は例外です)。しかし、テンプレートタグを書く際に注意すべき点がいくつかあります。

テンプレートタグの render() メソッドが(結果を文字列で返すのではなく)コンテキスト変数に結果を格納する場合、適切であれば mark_safe() を呼び出すように注意しなければなりません。変数が最終的にレンダリングされるとき、その時点で有効な自動エスケープ設定の影響を受けます、そのため、さらなるエスケープから安全であるべきコンテンツは、そのようにマークされる必要があります。

また、テンプレートタグがサブレンダリングを行うために新しいコンテキストを作成する場合、auto-escape 属性に現在のコンテキストの値を指定します。 Context クラスの __init__ メソッドはこの目的で使用できる autoescape というパラメータを受け取ります。例えば:

from django.template import Context


def render(self, context):
    # ...
    new_context = Context({"var": obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

これはあまり一般的な状況ではありませんが、テンプレートを自分でレンダリングする場合に便利です。例えば:

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

もしこの例で、現在の context.autoescape の値を新しい Context に渡すのを怠っていたら、結果は 常に 自動的にエスケープされていたでしょう。これは、テンプレートタグが {% autoescape off %} ブロックの中で使用されている場合、望ましい動作ではないかもしれません。

スレッド安全性の考慮

ノードがパースされると、その render メソッドは何度でも呼び出されます。Django はマルチスレッド環境で実行されることがあるので、1つのノードが2つのリクエストに応答して、異なるコンテキストで同時にレンダリングされることがあります。したがって、テンプレートタグをスレッドセーフにすることが重要です。

テンプレートタグを常にスレッドセーフに保つため、ノード自体に状態情報を保存してはいけません。例えば、 Django にはレンダリングされる度に与えられた文字列のリストを循環させる cycle テンプレートタグが用意されています:

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

ナイーブな CycleNode の実装は次のようになります:

import itertools
from django import template


class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)

    def render(self, context):
        return next(self.cycle_iter)

しかし、上のテンプレートコードを同時にレンダリングする2つのテンプレートがあるとします:

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

CycleNodeは反復していますが、グローバルに反復しています。スレッド1とスレッド2に関しては、常に同じ値を返しています。これは私たちが望んでいることではありません!

この問題に対処するために、 Django は render_context を提供します。この render_context は、現在レンダリング中のテンプレートの context に関連付けられます。この render_context は Python の辞書のように動作し、 render メソッドを呼び出す間の Node の状態を保存するために使う必要があります。

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

class CycleNode(template.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 next(cycle_iter)

Node のライフサイクルを通して変化しないグローバルな情報を属性として保存することは完全に安全であることに注意してください。 CycleNode の場合、 Node がインスタンス化された後も cyclevars 引数は変化しないので、 render_context に格納する必要はありません。しかし、 CycleNode の現在の繰り返し処理のように、現在レンダリングされているテンプレートに固有の状態情報は render_context に格納する必要があります。

注釈

self を使用して render_context 内の CycleNode 固有の情報をスコープしていることに注意してください。 テンプレート内には複数の CycleNode が存在する可能性があるので、他のノードの状態情報を取得しないように注意する必要があります。最も簡単な方法は、常に render_context のキーとして self を使用することです。複数の状態変数を管理している場合は、 render_context[self] を辞書にします。

タグを登録する

最後に、上記の カスタムテンプレートタグを書く で説明したように、タグをモジュールの Library インスタンスに登録します。次に例を示します。

register.tag("current_time", do_current_time)

tag() メソッドは次の2つの引数を取ります。

  1. テンプレート タグの名前の文字列。これを省略した場合は、コンパイル関数の名前が使用されます。
  2. 編集用の関数 -- Python の関数です (文字列としての関数名ではありません)。

フィルタ登録と同様に、これをデコレータとして使うこともできます。

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


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

name 引数を省略した場合、上記の2番目の例と同じように、Django はタグ名として関数の名前を利用します。

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

token.split_contents() を使用するテンプレート タグには任意の個数の引数を渡せますが、引数はすべて文字列リテラルとしてアンパックされます。動的なコンテンツ (テンプレート変数) をテンプレートタグに引数として渡すためには、少し追加の作業が必要になります。

前の例では現在時刻を文字列にフォーマットして文字列として返しましたが、オブジェクトから DateTimeField を渡したくて、テンプレートタグがその値を date-time とフォーマットとするとすると、次のように書けます。

<p>This post was last updated at {% 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() knows not to split quoted strings.
        tag_name, date_to_be_formatted, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires exactly two arguments" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return FormatTimeNode(date_to_be_formatted, format_string[1:-1])

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

ページの現在のコンテキスト内で渡された文字列が解決できない場合、変数を解決しようとすると VariableDoesNotExist 例外が発生します。

コンテキスト内で変数を設定する

上記の例は値を出力します。一般に、テンプレートタグは値を出力する代わりに、テンプレート変数を設定するほうがより柔軟です。それにより、テンプレートの作者はテンプレートタグが作成した値を再利用できるようになります。

コンテキスト内に変数を設定するには、render() メソッド内で、コンテキスト オブジェクト上のディクショナリ代入を使います。以下は、出力する代わりにテンプレート変数 current_time を設定する、更新されたバージョンの CurrentTimeNode です。

import datetime
from django import template


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>The time is {{ current_time }}.</p>

コンテキスト内の変数のスコープ

コンテキストに設定された変数は、その変数が割り当てられたテンプレートの同じ block でのみ使用できます。 この動作は意図的なもので、他のブロックのコンテキストと衝突しないように、変数にスコープを提供します。

しかし、 CurrentTimeNode2 には問題があります: 変数名 current_time がハードコーディングされています。つまり、テンプレートが {{ current_time }} を他の場所で使用していないことを確認する必要があります。なぜなら、 {% current_time %} はその変数の値を盲目的に上書きしてしまうからです。よりクリアな解決策は、次のようにテンプレートタグで出力変数の名前を指定することです:

{% 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 = format_string
        self.var_name = var_name

    def render(self, context):
        context[self.var_name] = datetime.datetime.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:
        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_time() がフォーマット文字列と変数名を取得し、その両方を CurrentTimeNode3 に渡していることです。

最後に、コンテキストを更新するカスタムテンプレートタグの簡単な構文が必要なだけなら、 simple_tag() ショートカットを使うことを検討してください 。 このショートカットは、タグの結果をテンプレート変数に代入することをサポートしています。

他のブロックタグまでパースする

テンプレートタグは連動して動作します。例えば、標準の {% comment %} タグは {% endcomment %} まですべてを隠蔽します。このようなテンプレートタグを作成するには、コンパイル関数の中で parser.parse() を使います。

以下は {% comment %} タグをシンプル化したものです:

def do_comment(parser, token):
    nodelist = parser.parse(("endcomment",))
    parser.delete_first_token()
    return CommentNode()


class CommentNode(template.Node):
    def render(self, context):
        return ""

注釈

実際の {% comment %} の実装は少し異なり、 {% comment %}{% endcomment %} の間に壊れたテンプレートタグを表示することを許可します。これは parser.parse(('endcomment',)) の代わりに parser.skip_past('endcomment') を呼び出し、その後に parser.delete_first_token() を実行することで、ノードリストの生成を回避しています。

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) だけです。

複雑なレンダリングの追加の例としては、django/template/defaulttags.py 内の {% for %}django/template/smartif.py 内の {% if %} のソースコードを見てください。

Back to Top