カスタムテンプレートタグとフィルター—Djangoドキュメント

提供:Dev Guides
< DjangoDjango/docs/3.2.x/howto/custom-template-tags
移動先:案内検索

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

Djangoのテンプレート言語には、アプリケーションのプレゼンテーションロジックのニーズに対応するように設計されたさまざまな組み込みタグとフィルターが付属しています。 それでも、テンプレートプリミティブのコアセットでカバーされていない機能が必要な場合があります。 Pythonを使用してカスタムタグとフィルターを定義することでテンプレートエンジンを拡張し、 :ttag: `{%load%} ` 鬼ごっこ。

コードレイアウト

カスタムテンプレートタグとフィルターを指定する最も一般的な場所は、Djangoアプリ内です。 それらが既存のアプリに関連している場合は、そこにバンドルするのが理にかなっています。 それ以外の場合は、新しいアプリに追加できます。 Djangoアプリが:setting: `INSTALLED_APPS` に追加されると、以下で説明する従来の場所で定義されたタグはすべて、テンプレート内で自動的にロードできるようになります。

アプリには、models.pyviews.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 %}

カスタムタグを含むアプリは、 :setting: `INSTALLED_APPS` のために :ttag: `{%load%} ` 動作するタグ。 これはセキュリティ機能です。Djangoのインストールごとにすべてのテンプレートライブラリにアクセスできるようにすることなく、単一のホストマシンで多くのテンプレートライブラリのPythonコードをホストできます。

templatetagsパッケージに入れるモジュールの数に制限はありません。 ただそれを覚えておいてください :ttag: `{%load%} ` ステートメントは、アプリの名前ではなく、指定されたPythonモジュール名のタグ/フィルターをロードします。

有効なタグライブラリであるためには、モジュールには、すべてのタグとフィルターが登録されているtemplate.Libraryインスタンスであるregisterという名前のモジュールレベルの変数が含まれている必要があります。 したがって、モジュールの上部近くに、次のように配置します。

from django import template

register = template.Library()

または、'libraries'引数を使用して DjangoTemplates にテンプレートタグモジュールを登録することもできます。 これは、テンプレートタグをロードするときに、テンプレートタグモジュール名とは異なるラベルを使用する場合に役立ちます。 また、アプリケーションをインストールせずにタグを登録することもできます。

舞台裏

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

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


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

カスタムフィルターは、1つまたは2つの引数を取るPython関数です。

  • 変数(入力)の値–必ずしも文字列である必要はありません。
  • 引数の値–これはデフォルト値を持つことも、完全に省略されることもあります。

たとえば、フィルターテンプレート:Varでは、フィルターfooに変数varと引数"bar"が渡されます。

テンプレート言語は例外処理を提供しないため、テンプレートフィルターから発生した例外はサーバーエラーとして公開されます。 したがって、返す適切なフォールバック値がある場合、フィルター関数は例外の発生を回避する必要があります。 テンプレートの明らかなバグを表す入力の場合、例外を発生させる方が、バグを隠すサイレントエラーよりも優れている場合があります。

フィルタ定義の例を次に示します。

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

そして、そのフィルターがどのように使用されるかの例を次に示します。

{{ somevariable|cut:"0" }}

ほとんどのフィルターは引数を取りません。 この場合、引数を関数から除外します。

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

カスタムフィルターの登録

django.template.Library.filter()

フィルタ定義を記述したら、それをLibraryインスタンスに登録して、Djangoのテンプレート言語で使用できるようにする必要があります。

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

上記の2番目の例のように、name引数を省略した場合、Djangoは関数の名前をフィルター名として使用します。

最後に、register.filter()は、is_safeneeds_autoescape、およびexpects_localtimeの3つのキーワード引数も受け入れます。 これらの引数については、以下のフィルターと自動エスケープおよびフィルターとタイムゾーンで説明しています。


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

django.template.defaultfilters.stringfilter()

最初の引数として文字列のみを期待するテンプレートフィルターを作成している場合は、デコレーター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種類の文字列を渡すことができることに注意してください。

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

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

    内部的には、これらの文字列のタイプは SafeString です。 次のようなコードを使用して、それらをテストできます。

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

テンプレートフィルターコードは、次の2つの状況のいずれかに分類されます。

  1. フィルタは、HTMLに安全でない文字(<>'"、または&)を結果に導入しません。まだ存在していませんでした。 この場合、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に渡す必要があるかどうかを判断します。 (後者の場合、恒等関数を「エスケープ」関数として使用します。)conditional_escape()関数はescape()に似ていますが、ではなくの入力のみをエスケープする点が異なります。 SafeDataインスタンス。 SafeDataインスタンスがconditional_escape()に渡された場合、データは変更されずに返されます。

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

    この場合、is_safeフラグについて心配する必要はありません(ただし、それを含めても何も害はありません)。 自動エスケープの問題を手動で処理して安全な文字列を返す場合は常に、is_safeフラグはどちらの方法でも何も変更しません。

警告

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

Djangoの組み込みフィルターには、適切な自動エスケープ動作を取得し、クロスサイトスクリプトの脆弱性を回避するために、デフォルトでautoescape=Trueがあります。

古いバージョンのDjangoでは、autoescapeのデフォルトがNoneであるため、Djangoの組み込みフィルターを再利用する場合は注意が必要です。 自動エスケープを取得するには、autoescape=Trueを渡す必要があります。

たとえば、:tfilter: `urlize` フィルターと:tfilter:` 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 ''

このフラグが設定されている場合、フィルターの最初の引数がタイムゾーン対応の日時である場合、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()に渡し、正しいHTMLを確保し、XSSの脆弱性からユーザーを保護します。

追加のエスケープが必要ない場合、コードに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オプションの動作の詳細については、包含タグのセクションを参照してください。

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

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>

包含タグ

django.template.Library.inclusion_tag()

テンプレートタグのもう1つの一般的なタイプは、別のテンプレートをレンダリングすることによって一部のデータを表示するタイプです。 たとえば、Djangoの管理インターフェースはカスタムテンプレートタグを使用して、「追加/変更」フォームページの下部にボタンを表示します。 これらのボタンは常に同じように見えますが、リンクターゲットは編集中のオブジェクトに応じて変化するため、現在のオブジェクトの詳細が入力された小さなテンプレートを使用するのに最適です。 (管理者の場合、これはsubmit_rowタグです。)

この種のタグは「包含タグ」と呼ばれます。

包含タグの記述は、おそらく例によって最もよく示されます。 チュートリアルで作成されたような、特定の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()メソッドを呼び出して、包含タグを作成して登録します。 この例に従って、上記のテンプレートがテンプレートローダーによって検索されるディレクトリのresults.htmlというファイルにある場合、次のようにタグを登録します。

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

または、 django.template.Template インスタンスを使用して包含タグを登録することもできます。

from django.template.loader import get_template
t = get_template('results.html')
register.inclusion_tag(t)(show_results)

…最初に関数を作成するとき。

場合によっては、包含タグに多数の引数が必要になることがあり、テンプレートの作成者がすべての引数を渡してその順序を覚えるのが面倒になります。 これを解決するために、Djangoは包含タグ用のtakes_contextオプションを提供しています。 テンプレートタグの作成時にtakes_contextを指定すると、タグには必須の引数がなくなり、基になるPython関数には1つの引数(タグが呼び出されたときのテンプレートコンテキスト)が含まれます。

たとえば、メインページを指す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と呼ばれる必要があることに注意してください。

その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 %}

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

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


簡単な概要

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

Djangoがテンプレートをコンパイルすると、生のテンプレートテキストが「ノード」に分割されます。 各ノードはdjango.template.Nodeのインスタンスであり、render()メソッドがあります。 コンパイルされたテンプレートは、Nodeオブジェクトのリストです。 コンパイルされたテンプレートオブジェクトでrender()を呼び出すと、テンプレートは、指定されたコンテキストで、ノードリスト内の各Noderender()を呼び出します。 結果はすべて連結されて、テンプレートの出力を形成します。

したがって、カスタムテンプレートタグを定義するには、生のテンプレートタグを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を取得します。 オプション/パラメータ/引数は常に__init__()を介してNodeに渡してください。
  • 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この例では、結果は次のようになります。 いつもテンプレートタグが内部で使用されている場合、これは望ましい動作ではない可能性があります。 :ttag: `{%autoescape off%} ` ブロック。


スレッドセーフに関する考慮事項

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

テンプレートタグがスレッドセーフであることを確認するために、ノード自体に状態情報を保存しないでください。 たとえば、Djangoには、レンダリングされるたびに指定された文字列のリスト間を循環する組み込みの:ttag: `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は現在レンダリングされているテンプレートのcontextに関連付けられているrender_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固有の情報をスコープする方法に注目してください。 特定のテンプレートには複数のCycleNodesが含まれている可能性があるため、別のノードの状態情報を壊さないように注意する必要があります。 これを行う最も簡単な方法は、常にselfrender_contextへのキーとして使用することです。 複数の状態変数を追跡している場合は、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):
    ...

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


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

token.split_contents()を使用してテンプレートタグに任意の数の引数を渡すことができますが、引数はすべて文字列リテラルとして解凍されます。 動的コンテンツ(テンプレート変数)を引数としてテンプレートタグに渡すには、もう少し作業が必要です。

前の例では現在の時刻を文字列にフォーマットして文字列を返しましたが、オブジェクトから DateTimeField を渡し、その日時のテンプレートタグ形式を使用するとします。

<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()ショートカットの使用を検討してください。


別のブロックタグまで解析する

テンプレートタグは連携して機能します。 たとえば、標準 :ttag: `{%コメント%} ` タグはまですべてを非表示にします{% 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 ''

ノート

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


parser.parse()は、ブロックタグの名前のタプルを まで解析します。 django.template.NodeListのインスタンスを返します。これは、パーサーがタプルで指定されたタグのいずれかに遭遇する「前」に遭遇したすべてのNodeオブジェクトのリストです。

上記の例の"nodelist = parser.parse(('endcomment',))"では、nodelist{% comment %}{% endcomment %}の間のすべてのノードのリストであり、{% comment %}と[ X132X] 自体。

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

複雑なレンダリングのその他の例については、のソースコードを参照してください。 :ttag: `{%for%} `django/template/defaulttags.py:ttag: `{%if%} `django/template/smartif.py