Django 2.0 - Custom template tags and filters

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




django

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

コードレイアウト

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

appには、 models.pyviews.pyと同じレベルのtemplatetagsディレクトリが含まれているはずです。これがまだ存在しない場合は、作成します。 - __init__.pyファイルを忘れないでください。 Pythonパッケージ。

開発サーバーは自動的に再起動しません

templatetagsモジュールを追加した後、 templatetagsタグまたはフィルタを使用するには、サーバを再起動する必要があります。

カスタムタグとフィルタはtemplatetagsディレクトリ内のモジュールに存在します。 モジュールファイルの名前は後でタグを読み込むために使用する名前なので、別のアプリでカスタムタグやフィルタと衝突しないように注意してください。

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

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

そしてあなたのテンプレートでは、以下を使用します:

{% load poll_extras %}

{% load %}タグが機能するには、カスタムタグを含むアプリがINSTALLED_APPSに存在する必要があります。 これはセキュリティ機能です。単一のホストマシン上の多くのテンプレートライブラリ用のPythonコードをDjangoインストールごとにアクセスすることなくホストすることができます。

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

ほとんどのフィルタは引数をとりません。 この場合、引数を関数から外してください。 例:

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

上記の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()

このようにすると、整数をこのフィルタに渡すことができ、(整数にlower()メソッドがないため) AttributeError発生しません。

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

カスタムフィルタを書くときに、フィルタがDjangoの自動エスケープ動作とどのように作用するかを考えてください。 テンプレートコードの内部では、次の2種類の文字列を渡すことができます。

  • 生の文字列はネイティブのPython文字列です。 出力時には、自動エスケープが有効な場合はエスケープされ、それ以外の場合は変更されません。
  • 安全な文字列は、出力時にさらにエスケープすることから安全とマークされた文字列です。 必要なエスケープは既に行われています。 これらは、クライアント側でそのまま解釈されることを意図した生のHTMLを含む出力によく使用されます。

    内部的には、これらの文字列はSafeTextSafeText 。 次のようなコードを使ってテストできます:

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

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

  1. あなたのフィルタは、まだ存在していない結果にHTMLで安全でない文字( <>'" )を導入しません。この場合、自動エスケープ処理をDjangoに任せてください。フィルタ関数を登録するときに、 is_safeフラグをTrueに設定するis_safeです。

    @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><a変わる可能性があります。これは、問題を起こさないように出力時にエスケープする必要があります。 同様に、セミコロン( ; )を削除すると&amp; &amp ;は有効なエンティティではなくなり、したがってさらにエスケープする必要があります。 ほとんどの場合、これはほとんど難しいことではありませんが、あなたのコードを見直す際には、そのような問題に気をつけてください。

    フィルタis_safeをマークすると、フィルタの戻り値が文字列にis_safeされます。 フィルタがブール値またはその他の文字列以外の値を返す必要がある場合は、 is_safeをマークすると意図しない結果が生じる(ブール値Falseを文字列 'False'に変換するなど)。

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

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

    しかし、注意してください。 あなたは出力を安全なものとしてマークするだけではありません。 あなた本当に安全であることを保証する必要があり、あなたがすること自動エスケープが有効かどうかによって異なります。 このアイデアは、テンプレート作成者にとってより簡単なものにするために、自動エスケープがオンまたはオフになっているテンプレートで動作するフィルタを記述することです。

    フィルタで現在の自動エスケープ状態を知るには、フィルタ関数を登録するときにneeds_autoescapeフラグをTrueに設定します。 (このフラグを指定しない場合は、デフォルトでFalseに設定されます)。 このフラグはDjangoにフィルタ関数がautoescapeという余分なキーワード引数をautoescapeうとしていることを伝えます。これは、自動エスケープが有効な場合は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()関数は、 SafeDataインスタンスではない入力をエスケープする以外は、 escape()SafeDataます。 SafeDataインスタンスがconditional_escape()渡されたconditional_escape() 、データは変更されずに返されます。

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

    このケースでは、 is_safeフラグを心配する必要はありません(ただし、何かを傷つけることはありません)。 自動エスケープの問題を手動で処理し、安全な文字列を返すときは、 is_safeフラグは何も変更しません。

警告

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

Djangoに組み込まれているフィルターはautoescape=Trueをデフォルトで持っています。これは、適切なautoescaping動作を得て、クロスサイトスクリプトの脆弱性を避けるためです。

Djangoの古いバージョンでは、Djangoの組み込みフィルタをautoescapeデフォルトとしてautoescape再利用するときに注意してautoescapeautoescape=Trueを渡す必要があります。

たとえば、 urlizeurlize_and_linebreaksフィルタを組み合わせたurlizeというカスタムフィルタを作成する場合、フィルタは次のようになります。

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設定して登録し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関数と他の必要なビットにラップしてテンプレートシステムに登録する関数をとりdjango.template.Library

したがって、 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ます:

  • 必要な引数の数などをチェックすることは、関数が呼び出された時点で既に行われているので、そのようにする必要はありません。
  • 引数の周りの引用符は既に削除されているので、単純な文字列を受け取るだけです。
  • 引数がテンプレート変数である場合、関数は変数自体ではなく変数の現在の値を渡します。

他のタグユーティリティとは異なり、テンプレートコンテキストがautoescapeモードの場合、 simple_tagはその出力をconditional_escape()に渡し、正しいHTMLを保証し、XSSの脆弱性からあなたを守ります。

追加のエスケープが必要ない場合は、コードにXSSの脆弱性がないことを絶対に確信している場合は、 django.utils.safestring.mark_safe()を使用する必要があります。 小さなHTMLスニペットを作成するには、 format_html()代わりにmark_safe()使用することを強くお勧めします。

テンプレートタグが現在のコンテキストにアクセスする必要がある場合は、タグを登録するときに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引数のあとに変数名を続けて使用し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タグです)。

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

インクルードタグを書くことは、おそらく例で最もよく示されます。 tutorials作成されたような、指定された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()メソッドを呼び出して、包含タグを作成して登録します。 この例の後、上記のテンプレートがtemplatesローダーによって検索されたディレクトリの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_title変数とhome_title変数を含むコンテキストで常に使用されるインクルードタグをhome_linkとします。 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とテンプレートの名前を指定し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()メソッドを持っていrender() 。 コンパイルされたテンプレートは、単純にNodeオブジェクトのリストです。 コンパイルされたテンプレートオブジェクトに対してrender()を呼び出すと、テンプレートは、ノードリスト内の各Nodeで、指定されたコンテキストでrender()を呼び出し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.contents.split()するため、堅牢ではありません。 常にtoken.split_contents()使用することをおtoken.split_contents()ます。
  • この関数は、構文エラーの場合に便利なメッセージとともにdjango.template.TemplateSyntaxError発生させます。
  • TemplateSyntaxError例外は、 tag_name変数を使用します。 エラーメッセージでタグの名前をハードコードしないでください。これは、タグ名を関数に結合するためです。 token.contents.split()[0]タグに引数がない場合でも、 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は、間違った数や型の引数を受け取った場合、 django.template.TemplateSyntaxErrorます。

最終的に、このコンパイルとレンダリングの分離は効率的なテンプレートシステムをもたらします。これは、テンプレートが複数回解析されることなく複数のコンテキストをレンダリングできるためです。

自動エスケープの考慮事項

テンプレートタグからの出力自動エスケープフィルター(上記のsimple_tag()simple_tag()を介して自動的に実行されるわけではありません 。 しかし、まだテンプレートタグを書くときに留意すべきことがいくつかあります。

テンプレートのrender()関数が(文字列で結果を返すのではなく)コンテキスト変数に結果を格納する場合は、必要に応じてmark_safe()を呼び出す必要があります。 変数が最終的にレンダリングされると、その時点で有効な自動エスケープ設定の影響を受けるため、さらにエスケープする必要があるコンテンツをそのようにマークする必要があります。

また、テンプレートタグがいくつかのサブレンダリングを実行するための新しいコンテキストを作成する場合は、自動エスケープ属性を現在のコンテキストの値に設定します。 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はマルチスレッド環境で実行されることがあるので、2つの別々の要求に応じて、単一のノードが異なるコンテキストで同時にレンダリングすることがあります。 したがって、テンプレートタグがスレッドセーフであることを確認することが重要です。

テンプレートタグがスレッドセーフであることを確認するには、ノード自体に状態情報を格納しないでください。 たとえば、Djangoは、レンダリングされるたびに指定された文字列のリストを循環する組み込みcycleテンプレートタグを提供します:

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

CycleNodeな実装は、 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状態を格納するために使用する必要がありrender

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

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. The compilation function – a Python function (not the name of the function as a string).

As with filter registration, it is also possible to use this as a decorator:

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

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

If you leave off the name argument, as in the second example above, Django will use the function's name as the tag name.

Passing template variables to the tag

Although you can pass any number of arguments to a template tag using token.split_contents() , the arguments are all unpacked as string literals. A little more work is required in order to pass dynamic content (a template variable) to a template tag as an argument.

While the previous examples have formatted the current time into a string and returned the string, suppose you wanted to pass in a DateTimeField from an object and have the template tag format that date-time:

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

Initially, token.split_contents() will return three values:

  1. The tag name format_time .
  2. The string 'blog_entry.date_updated' (without the surrounding quotes).
  3. The formatting string '"%Y-%m-%d %I:%M %p"' . The return value from split_contents() will include the leading and trailing quotes for string literals like this.

Now your tag should begin to look like this:

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

You also have to change the renderer to retrieve the actual contents of the date_updated property of the blog_entry object. This can be accomplished by using the Variable() class in django.template .

To use the Variable class, simply instantiate it with the name of the variable to be resolved, and then call 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 resolution will throw a VariableDoesNotExist exception if it cannot resolve the string passed to it in the current context of the page.

Setting a variable in the context

The above examples simply output a value. Generally, it's more flexible if your template tags set template variables instead of outputting values. That way, template authors can reuse the values that your template tags create.

To set a variable in the context, just use dictionary assignment on the context object in the render() method. Here's an updated version of CurrentTimeNode that sets a template variable current_time instead of outputting it:

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

Note that render() returns the empty string. render() should always return string output. If all the template tag does is set a variable, render() should return the empty string.

Here's how you'd use this new version of the tag:

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

Variable scope in context

Any variable set in the context will only be available in the same block of the template in which it was assigned. This behavior is intentional; it provides a scope for variables so that they don't conflict with context in other blocks.

But, there's a problem with CurrentTimeNode2 : The variable name current_time is hard-coded. This means you'll need to make sure your template doesn't use {{ current_time }} anywhere else, because the {% current_time %} will blindly overwrite that variable's value. A cleaner solution is to make the template tag specify the name of the output variable, like so:

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

To do that, you'll need to refactor both the compilation function and Node class, like so:

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)

The difference here is that do_current_time() grabs the format string and the variable name, passing both to CurrentTimeNode3 .

Finally, if you only need to have a simple syntax for your custom context-updating template tag, consider using the simple_tag() shortcut, which supports assigning the tag results to a template variable.

Parsing until another block tag

Template tags can work in tandem. For instance, the standard {% comment %} tag hides everything until {% endcomment %} . To create a template tag such as this, use parser.parse() in your compilation function.

Here's how a simplified {% comment %} tag might be implemented:

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

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

注意

The actual implementation of {% comment %} is slightly different in that it allows broken template tags to appear between {% comment %} and {% endcomment %} . It does so by calling parser.skip_past('endcomment') instead of parser.parse(('endcomment',)) followed by parser.delete_first_token() , thus avoiding the generation of a node list.

parser.parse() takes a tuple of names of block tags ''to parse until''. It returns an instance of django.template.NodeList , which is a list of all Node objects that the parser encountered ''before'' it encountered any of the tags named in the tuple.

In "nodelist = parser.parse(('endcomment',))" in the above example, nodelist is a list of all nodes between the {% comment %} and {% endcomment %} , not counting {% comment %} and {% endcomment %} themselves.

After parser.parse() is called, the parser hasn't yet “consumed” the {% endcomment %} tag, so the code needs to explicitly call parser.delete_first_token() .

CommentNode.render() simply returns an empty string. Anything between {% comment %} and {% endcomment %} is ignored.

Parsing until another block tag, and saving contents

In the previous example, do_comment() discarded everything between {% comment %} and {% endcomment %} . Instead of doing that, it's possible to do something with the code between block tags.

For example, here's a custom template tag, {% upper %} , that capitalizes everything between itself and {% endupper %} .

使用法:

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}

As in the previous example, we'll use parser.parse() . But this time, we pass the resulting nodelist to the 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()

The only new concept here is the self.nodelist.render(context) in UpperNode.render() .

For more examples of complex rendering, see the source code of {% for %} in django/template/defaulttags.py and {% if %} in django/template/smartif.py .

原文