日記

日々のことと、Python/Django/PHP/Laravel/nodejs などソフトウェア開発のことを書き綴ります

自前のタグを作る

前回はフィルタを作ったので、今回はタグを作ってみたいと思います。
フィルタとタグの作り方は、かなり似ている。では、フィルタとタグの使い方から見た時に何が違うかと言うと、インプットの有無だと思う。フィルタは、テンプレート上の動的な値がありき。でもタグはそんなことは無い。

では、タグのサンプルコードを見てみると以下のようになります。

from django import template
import random

register = template.Library()

class RandomNumberNode(template.Node):
    def __init__(self):
        pass
    
    def render(self, context):
        return str(random.randint(10000, 99999))

@register.tag(name='random_number')
def random_number(parser, token):
    return RandomNumberNode()

前回のフィルタのファイルとは分けて、sample_tags.pyというファイルを作りました。ちなみにファイルを置く場所はフィルタと同じく templatetags の下に置きます。

続いて、first.htmlは以下のように編集しました。

<html>
<head>
</head>
<body>
{% load sample_filters %}
{% load sample_tags %}

<p>サンプルアプリケーション はじめの一歩</p>

{% random_number %}

</body>
</html>

フィルタのときと同じように load sample_tags で自前のタグをロードしてから、 random_numberタグを呼び出しています。これをブラウザからアクセスして実行すると

<html>
<head>
</head>
<body>

<p>サンプルアプリケーション はじめの一歩</p>

99580

</body>
</html>

ちゃんと数値が表示できてますね!
タグはちょっと複雑なので、ソースコードをもう少し説明すると、フィルターと同様にタグもDjangoに登録するときは、引数を決まった個数持っている関数になります。

@register.tag(name='random_number')
def random_number(parser, token):
    return RandomNumberNode()

この部分ですね。フィルタとは違い、この関数は template.Node を継承したクラスを返す必要があります。そのために、RandomNumberNodeというクラスを定義しました。

class RandomNumberNode(template.Node):
    def __init__(self):
        pass
    
    def render(self, context):
        return str(random.randint(10000, 99999))

これです。このクラスの render 関数が Django から呼ばれて関数が戻した文字列をテンプレート(HTML)に出力しているわけです。それと render 関数の引数で渡される context は Viewで設定した値を保持しています。context を通じて View で設定された値を取得してtagの処理利用できます。

Nodeクラスはこれくらいにして、tagのエントリーポイントだった、関数に戻ります。

def random_number(parser, token):
    return RandomNumberNode()

parserとtokenは、parserは Djangoのテンプレートを解析するオブジェクトで、tokenは引数です。
ちょっとHTMLテンプレートをいじってみます。

<html>
<head>
</head>
<body>
{% load sample_filters %}
{% load sample_tags %}

<p>サンプルアプリケーション はじめの一歩</p>

{% random_number a b c %}
<p>ほげほげ</p>
{% endrandom_number %}

</body>
</html>

変更したらブラウザでアクセスしてみます。

TemplateSyntaxError at /sample/first
Invalid block tag: 'endrandom_number'

といった内容のエラーが出ると思います。これは endrandom_numberなんて知らないよ、とDjangoが言っているわけです。end...というのは、どこかで見たことがありますね。そう、ビルトインの if や for がこんな実装をしていました。では、randowm_numberタグでは、囲まれた文字列の前後に異なる乱数をくっつける実装をしてみたいと思います。
sample_tags.py を次のように書き換えます。

# vim:fileencoding=utf-8
from django import template
import random

register = template.Library()

class RandomNumberNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist
    
    def render(self, context):
        output = self.nodelist.render(context)
        return str(random.randint(10000, 99999)) + output + str(random.randint(10000, 99999))

@register.tag(name='random_number')
def random_number(parser, token):
    nodelist = parser.parse(('endrandom_number',))
    parser.delete_first_token()
    return RandomNumberNode(nodelist)

parser.parseで指定しているのが、読み込みの終わりになるタグ名です。endrandom_numberが出てくるまで読み込み、Nodeを取得します。次に parser.delete_first_token()で endrandom_numberを削除しています。どうして delete_first_tokenが最後に出てくるはずのタグを消しているのか、よくわかりませんでした。このエントリに気がついたエロい人が教えてくれると願いましょう。
RandomNumberNodeのコンストラクタも引数を追加して、parserから取り出したNodeリストを渡しています。ちなみにこのNodeリストは、Djangoの template.Nodeを継承したクラスのようです。
render 関数の中では、nodelistのrender関数を読んでいます。これは、外部からRandomNumberNodeのrender関数が呼ばれているのと同じことでしょう。

では、これをブラウザ経由で実行すると

<html>
<head>
</head>
<body>

<p>サンプルアプリケーション はじめの一歩</p>

78550
<p>ほげほげ</p>
12563

</body>
</html>

成功!!

実はさっきのコードで、

output = self.nodelist.render(context)

これを呼ばないとrandom_numberとendrandom_numberに挟まれた部分を出力しないなんていうことも簡単にできてしまいます。(if/elseもこんなことをやっているって推測できちゃいますね)

ちなみに仕込みだけしておいて、全く触れなかった tag 関数の token ですが、あれは引数がそのまま文字列として入ってきます。引数の分解などは自前でやる必要があります。DjangoRails 世代のフレームワークなので、そのあたりは至れり尽くせりなのかと思ったら意外とローレベル。。。

まぁ、そんなことは気にせず、次回は Context Processorに触れてみたいと思います。