1. ホーム
  2. Django

Djangoの基本(16)。テンプレートタグの紹介とそのカスタマイズ方法

2022-02-19 14:07:43

での Django基礎講座(15):テンプレートフィルタの仕組みとカスタマイズ方法 では、Django のテンプレートフィルタの性質と仕組みを紹介し、そのカスタマイズ方法を詳しく説明しました。今日は、 Django のテンプレートタグのカテゴリとそのカスタマイズ方法についてお話します。

テンプレートタグ(タグ)とは

テンプレートタグは、{% %}括弧で囲みます。一般的なテンプレートタグは、{% load xxxx %}, {% block xxxx %}, {% if xxx %}, {% url 'xxxx' %}です。これらのテンプレートタグは、本質的には関数でもあり、タグ名は一般に関数名となります。これらのタグの主な目的は、テンプレートをレンダリングするためのコードを読み込んだり、渡されたパラメーターに対して特定の論理的な判断や計算を行い、それを返すことです。

例えば、以下の例のurlタグは、名前付きurlと記事IDの2つのパラメータを受け取り、それらを解析してblog/article/4/のようなリンクを生成して返します。

<a href="{% url 'blog:article_detail' article.id %}">details</a>

Django テンプレートタグ (タグ) のカテゴリ

Django のテンプレートタグは 2 つのカテゴリに分類されます。

  • simple_tag (シンプルなタグ : データを処理したり、文字列を返したり、コンテキストに変数を設定したり追加したりします。

  • inclusion_tag (包含タグ) : データを処理し、レンダリングされたテンプレートを返します。

Django に慣れている人なら、一般的にビュービューでコンテキストを設定し、それを通してデータをテンプレートに渡すことを知っています。コンテキストとは、変数とその値のコレクションです。simple_tag を使うことで、ビューの外側のコンテキストに変数を設定したり、追加したりすることができます。注意: Django 1.9 以降では assign_tag をサポートしていませんので、 simple_tag を使ってください。

テンプレートタグをカスタマイズする方法

まず、アプリのディレクトリに templatetags という名前の新しいフォルダを作成する必要があります(他の名前は使用できません)。そのディレクトリに、カスタムテンプレートタグ関数のための python ファイルを作成する必要があります。この場合は blog_extras.py ですが、他の名前でもかまいません。全体のディレクトリ構造は以下の通りです。

blog/
   __init__.py
   models.py
   templatetags/
       __init__.py
       blog_extras.py
   views.py

テンプレート内でカスタムテンプレートタグを使用するには、{% load blog_extras %}を使ってカスタムフィルタをロードし、{% tag_name %}を介して使用する必要があります。

カスタムテンプレートタグの簡単な3つの例

文字列を返すタグ、テンプレート・コンテキストに変数を渡すタグ、レンダリングされたテンプレートを表示するタグの3つの単純なテンプレート・タグを定義します。以下のコードをblog_extra.pyに追加します。

#blog_extra.py

from django import template
import datetime
from blog.models import Article

register = template.Library()

# use simple tag to show string
@register.simple_tag
def total_articles():
    return Article.objects.filter(status='p').count()

# use simple tag to set context variable
@register.simple_tag
def get_first_article():
    return Article.objects.filter(status='p').order_by('-pub_date')[0]

# show rendered template
@register.inclusion_tag('blog/latest_article_list.html')
def show_latest_articles(count=5):
    latest_articles = Article.objects.filter(status='p').order_by('-pub_date')[:count]
    return {'latest_articles': latest_articles, }


# latest_article_list.html

<ul>
{% for article in latest_articles %}
<li>{
{ article.title }} </li>
{% endfor %}
</ul>

# index.html (独自のテンプレートタグを使用)

{% extends "blog/base.html" %}
{% load blog_extras %}

{% block content %}

<p> Number of articles: {% total_articles %}</p>
{% show_latest_articles %}

{% get_first_article as first_article %}
<p>First article: </p>
<p>{
{ first_article.title }}</p>

{% endblock %}

最終的な表示イメージは以下のようになります。

複雑な例:テンプレートやコンテキストからパラメータを受け取り、結果を返す。

上記の3つの簡単な例では、メッセージの受け渡しは一方通行です。タグ関数がテンプレートやコンテキストから引数を受け取り、それらを処理し、文字列やレンダリングされたテンプレートを返す方が一般的です。

次の例のshow_resultsタグは、投票結果を返すために、テンプレートから渡されたpollの引数を受け取る必要があります。

{% show_results poll %}

この時点で、show_results関数を次のように書くことができます。

@register.inclusion_tag('results.html')
def show_results(poll):
    choices = poll.choices_set.all()
    return {'choices': choices}

もちろん、poll変数がテンプレートに表示される必要はありません。多くの場合、オブジェクトやオブジェクトのリストはすでにグローバル変数のコンテキストに存在しており、takes_context=Trueを使用してコンテキストから直接変数を使用することができます。pollがすでにコンテキストに存在すると仮定すると、上のコードは次のように変更することができます。

@register.inclusion_tag('results.html', takes_context=True)
def show_results(context):
    choices = context['poll'].choices_set.all()
    return {'choices': choices}

この時点で、テンプレートは以下のコードに簡略化され、投票結果を表示するための引数pollが不要になります。

{% show_results %}

テンプレートから渡される複数のパラメータを処理する方法

show_results poll %}のカスタムタグ関数は、テンプレートから渡されるpollパラメータを1つだけ受け取ります。テンプレートは複数のパラメータを渡すこともでき、 Django のタグ関数は、パラメータ名や数がわかっている場合、渡されたパラメータを位置で処理することができます。

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

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


しかし、{% url "article_detail" article.id article.slug %}のようなurlタグは明らかにもっと複雑です。未知の数のパラメータと未知の名前のパラメータを取ることができ、二重引用符の有無も問いません。

この場合の Django のアプローチは、タグがあるノードをパースし (パーサ)、受け取った文字列を全体としてトークンとし、まずトークンを分割し、その後個別に処理する、というものです。

次に、{% format_time %}タグが時刻の日付をどのようにフォーマットするかを見てみましょう。

<p>Published at at {% format_time article.pub_date "%Y-%m-%d %I:%M %p" %}. </p>

カスタムformat_timeタグ関数の全コードは以下のとおりです。

from django import template

register = template.Library()

@register.tag(name="format_time")
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])


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

では、上記のコードがどのように動作するのかに注目してみましょう。

  • Django テンプレートパーサーはテンプレート全体をスキャンし、 format_time タグを見つけ、新しいノード Node として扱い、長い文字列 format_time article.pub_date "%Y-%m-%d" をトークンとして得ます。

  • get_format_timeメソッドは、トークン自身のsplit_contentsメソッドを使って、上記の文字列をタグ名、フォーマットされる日付(date)、フォーマットの3つに分割し、FormatTimeNodeが処理するためのフォーマットされる日付とフォーマットを返します format_string[1:-1] の機能は、ダブルクォートを削除することです。

  • ノードクラス FormatTimeNode は、ノードのレンダリング、render メソッドによる新しいノードのレンダリング、およびコンテキストによるテンプレートへの他の変数の受け渡し(下図参照)を担当します。render メソッドが具象値を返さない場合、空文字列を返す必要があります。

def render(self, context):
    actual_date = self.date_to_be_formatted.resolve(context)
    context['formatted_time'] = actual_date.strftime(self.format_string)
    return ''
    

parseメソッドによる連続パージング

時々、{% comment %}や{% endcomment %}のようなタグに出くわすことがあります。このとき、parseメソッドで本当のnodelistをパースする必要があります。これは非常に複雑なので、後で Django のソースコードを解析するときに取り上げる予定です。

概要

この記事では、Django のテンプレートタグの 2 つの主要なカテゴリ (シンプルタグと包含タグ) と、テンプレートタグをカスタマイズする方法について、テンプレートがタグ関数に 1 つ以上の引数を渡す方法に焦点を当てて解説しています。この記事があなたのお役に立てれば幸いです。

グレートリバードッグ

2018.9.19