Jinja2のfilterとmacroをいじる

filterはsettingsで指定して、テンプレートのどこからでも使える。macroはテンプレートの中で直接定義するか、定義しているテンプレートをimportして使う。

filterで色々定義してあるもののうち、macroにする方がいいに違いないものを移動して行く。一番シンプルなフィルタ

def q2img(q):
    return '<img src="/q/image/%s.png">' % q.short_key

使われ方。

{{ q|q2img|safe() }}

これをマクロに変える。

{% macro to_img(q) -%}
<img src="/q/image/{{ q.short_key }}.png">'
{%- endmacro %}

冒頭でimport

{% import "main/macro_for_quiz.html" as macro4q %}

で、こう使う。

{{ macro4q.to_img(q) }}

ふむふむ。この件に関してはマクロの方が自然だな。



その他のフィルタもマクロにすべきだったらマクロにしようと思ったのだが、なんというかこれifとか算術演算とかクエリとか含んでるし、最初の2つに関してはマクロでもかけるけどダークサイドな気がするので

あえてやる!

元のフィルタ

def q2buttons(q):
    result = []
    w = 100 / len(q.answer_arrangement.split()[0])
    for c in q.answer_arrangement:
        if c == " ":
            result.append("<br/>")
        else:
            result.append('<input type=submit name="answer" value="%s" style="width:%d%%;font-size:200%%;">' % (c, w))
    
    return "".join(result)

作ったマクロ

{% macro to_buttons(q) -%}
{% set width = 100 / q.answer_arrangement.split()[0].__len__() %} 

{% for c in q.answer_arrangement -%}
    {%- if c == " " -%}
        <br/>
    {%- else -%}
        <input type=submit name="answer" value="{{ c }}"
	       style="width:{{ width }}%; font-size:200%;">
    {%- endif -%}
{%- endfor %}

{%- endmacro %}

おお、なんかできてしまった。そして意外と醜くない。加減乗除やifの条件式が「普通の文法」だからだなぁ。

len(xs)しようとして怒られたのでかわりに__len__を使ったけど、builtin filtersにlengthってのがあるからxs.__len__()のかわりにxs|lengthでいいんだな。



もう一歩ダークサイドに足を踏み入れてみる。元のフィルタ。

def adj_quiz(q, typ):
    "typ: newer | older"
    from models import Question
    assert isinstance(q, Question)
    if typ == "newer":
        adj = Question.all().filter("published_on >", q.published_on).order("published_on").get()
    else:
        adj = Question.all().filter("published_on <", q.published_on).order("-published_on").get()
    
    if not adj:
        return ""
    if typ == "newer":
        return '<<<a href="/q/%s/">Newer Quiz</a>' % adj.short_key
    else:
        return '<a href="/q/%s/">Older Quiz</a>>>' % adj.short_key

assertがないから、予期しない値が渡されたときにデバッグに苦労しそうな気がするなぁ。filterでassert_isinstance(cls)とかがあれば解決するのか(そんなものをテンプレートに書くのかよと)

from models import Questionができないが、まあなんとでもなる(ぇ) 今回は引数がQuestionのインスタンスだから__class__で取れるし、そうじゃなくてもクラスをインポートするフィルタを(マテ

{% macro adj_quiz(q, typ) -%}
    {% if typ == "newer" %}
        {% set adj = q.__class__.all().filter("published_on >", q.published_on).order("published_on").get() %}
    {% else %}
        {% set adj = q.__class__.all().filter("published_on <", q.published_on).order("-published_on").get() %}
    {% endif %}
    
    {% if not adj %}
    {% elif typ == "newer" %}
        <<<a href="/q/{{ adj.short_key }}/">Newer Quiz</a>
    {% else %}
        <a href="/q/{{ adj.short_key }}/">Older Quiz</a>>>
    {% endif %}
{%- endmacro %}

おいおいおいおい、できちゃうよ。

def as_class(class_name):
    import models
    return getattr(models, class_name)
{% set adj = "Question"|as_class().all().filter("published_on <", q.published_on).order("-published_on").get() %}

できちゃうよ。


結論。Jinja2はすごい。自由度がものすごく高い。Pythonをよくわかっている人間がテンプレートを書く場合にはとても便利。自重せずにPythonの機能を使ってしまうような人間(e.g. 僕)にJinja2でテンプレートを書かせると、PythonメタプログラミングできるくらいのPythonの知識がなければ修正できないようなテンプレートが出来上がる。しかも黒魔術にしか見えないas_classフィルタなんかが「(僕にとっては)ソースやフィルタの名前を見れば一目瞭然だろ」という理由でドキュメント化されない!

危険だ!危険な匂いがプンプンとする!Jinja2はテンプレートエンジンに見えるけど実はHTMLとかを出力するのが簡単なようにドメイン特化したプログラミング言語じゃないか!自制心を強く持たないと新たなシスの暗黒卿が生まれてしまう!!