Djangoのテンプレートでforのようなendタグを持つカスタムタグを書く
概要
pythonのWebフレームワークDjangoで{% for athlete in athlete_list %}<div>{{athlete.name}}</div>{% endfor %}
のような、HTMLや他のタグを間に挟めるend
タグを持つカスタムタグを自作する方法です。
初Django(というかpythonも少ししか書いたことない)なのでお作法とか違ったらすいません。
ドキュメントは下記ですが少々内容が足りないです。タグの引数の解析の説明がありません。
デフォルトタグのソースを見るのが手っ取り早いと思います。
この記事では以下のようなことが理解できると思います。
- カスタムブロックタグの書き方、登録の仕方。
- 登録したタグを他のAPPで使う方法。
- カスタムタグの引数の解析方法と変数の解決方法。文字列引数の扱い。
- contextへの新しい変数の代入とその掃除方法。
コードと解説
タグの登録
app直下にtemplatetagsディレクトリを作り__init__.py
とmy_tags.py
(名前は任意)を置き、my_tags.py
でタグを登録します。
some_app/
templatetags/
__init__.py
my_tags.py
# some_app/templatetags/my_tags.py
from django import template
register = template.Library()
@register.tag(name="foobar")
def do_foobar(parser, token):
nodelist = parser.parse(("endfoobar",))
parser.delete_first_token()
return FoobarNode(nodelist)
class FoobarNode(template.Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
output = self.nodelist.render(context)
return output
タグの登録は@register.tag
でします。@register
には幾つかの登録メソッドがありますが、下記の回答にまとめてる人がいて分かりやすかったです。
do_foobar
はテンプレートに書かれたタグを解析してtemplate.Node
の子クラスを返します。とりあえず基本的なコードなのでブロックの中身だけ解析してFoobarNode
に渡し、返してます。
nodelist = parser.parse(("endfoobar",))
でendfoobar
までのテンプレートに書いてあるnodeの行を集めます。endfoobar
の直前までを集めてるので、まだendfoobar
タグは解析前の状態で残っているためparser.delete_first_token()
でendfoobar
のタグを捨てます。これをしないとInvalid block tag on line 15: 'endfoobar', expected 'endblock'. Did you forget to register or load this tag?
のようなエラーになります。
FoobarNode
にrender
関数を定義して、そこでHTMLテキストを返すと、それがテンプレートにレンダリングされます。
返すHTMLテキストですがfor
のソースを見てください。
django.utils.safestring.SafeString
を返してるのがわかると思います。なのでHTMLの生テキスト返すとエスケープされタグとして扱われないのかと思いましたがreturn '<div>foobar</div>'
としてもそのまま出力されました。まあ、一応他のソースに倣いdjango.utils.safestring.SafeString
を返しておいた方が無難だと思います(ちなみにself.nodelist.renderの返り値はSafeStringです)。
これをテンプレートでload
すれば使えます。
# some_app/templates/some_app/index.html
{% load my_tags %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% foobar %}
<div>foobar</div>
{% endfoobar %}
</body>
</html>
別のappでも使いたい
別のappでも使えるようにするにはsettings.py
で登録してやる必要があります。
# settings.py
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
'libraries': {
'some_app_my_tags': 'some_app.templatetags.my_tags',
}
},
},
]
{% load some_app_my_tags %}
{% foobar %}
<div>foobar</div>
{% endfoobar %}
タグの解析
ビルトインのタグを見ると色々引数を渡せますが、自作タグにも当然渡せます、引数はテキストの配列でくるので解析が割と面倒です。
タグにテンプレート変数とテキストの引数を渡してみましょう。
# some_app/views.py
def index(request)
render('some_app/index.html', { 'values': ['foo', 'bar'] })
# some_app/templates/some_app/index.html
{% foobar values 'テキスト' %}
<div>{{label}}:{{value}}</div>
{% endfoobar %}
これで下記のようなHTMLがレンダリングされる実装してみようと思います。
<div>テキスト:foo</div>
<div>テキスト:bar</div>
タグに書いた引数はdo_foobar
の2番目の引数token
に入ってきます。これはdjango.template.base.Token
のインスタンスです。
@register.tag(name="foobar")
def do_foobar(parser, token):
bits = token.split_contents()
var_name = parser.compile_filter(bits[1])
label_string = parser.compile_filter(bits[2])
nodelist = parser.parse(("endfoobar",))
parser.delete_first_token()
return FoobarNode(nodelist, var_name, label_string)
bits
の中身は['foobar', 'values', "'テキスト'"]
となります。values
がテキストで、"'テキスト'"はクオートで囲まれた状態できます。変数の解決はcontext
にアクセスできるNode
でやりますのが、この関数ではparser.compile_filter
を使って変数なのか、テキストなのか解析までやっておきます。parser.compile_filter
の返り値はdjango.template.base.FilterExpression
です。
class FoobarNode(template.Node):
def __init__(self, nodelist, var_name, label_string):
self.nodelist = nodelist
self.var_name = var_name
self.label_string = label_string
def render(self, context):
values = self.var_name.resolve(context, ignore_failures=True) or []
label = self.label_string.resolve(context)
with context.push():
context['label'] = label
outputs = []
for value in values:
context['value'] = value
outputs.append(self.nodelist.render(context))
return mark_safe(''.join(outputs))
context
を引数にresolve
を呼ぶことで変数を解決できます。この変数名はform.email
のようなプロパティーへのアクセスもちゃんと解決できます。label_string
の方は文字列ですが、resolve
を呼んでおくことで変数にも対応可能です。
context
に新しい変数をアサインしますがブロックの外側からはアクセスできないように綺麗にしておきたいのでwith context.push():
で囲います(pythonはブロックじゃなくてインデントなので囲うとは言わない?)。こうするとcontextに新しい空のdictが生成され、代入した変数はそこに登録、ブロックを抜けるとそのdictは削除されます。
Discussion