Djangoでurlタグを使ったら謎のエラー

urlタグを使うと下の1や2の書き方をする代わりに3の書き方ができる。1の書き方は「現在表示しているURLは何か」を意識しないといけなくて、もしそのURLが変わったならば適切に../の数を変更しないとリンク切れになる可能性がある。2の書き方は「このアプリケーションが提供されているURL」を意識する必要があって、たとえば「http://example.com/programmers_nightmare/」で提供していたのを「http://programmers_nightmare.com/」での提供に変えた場合には書き直す必要がある。でもまあ1よりは変更頻度が低いのでまだまし。3の方法は「そのページを表示する関数」を指定してそこから逆算でURLを求めるので1や2ではリンク切れになるようなシチュエーションでも修正が必要ない。

1: <a href="../../new_game/{{ id }}/">start new game</a>
2: <a href="/programmers_nightmare/new_game/{{ id }}/">start new game</a>
3: <a href="{% url programmers_ightmare.views.new_game id %}">start new game</a>

という便利な機能なので使ってみようとしたのだけども、まだエラーメッセージがわかりにくいので戸惑った。

組み込みタグ/フィルタリファレンス — Django v1.0 documentation

まったく関係ない物を探しに行っているように見える

まず{% url new_game id %}と書いてみて最初にぶつかったのは下記のエラー。

Caught an exception while rendering: Tried download_file in module programmers_nightmare.views. Error was: 'module' object has no attribute 'download_file'

download_fileって!{% url new_game id %}って書いてあるのになんでそれを探しに行っている!

これはdownload_fileってのがどこで出現するのか調べてみると割と簡単にわかる。たとえばurls.pyでURLとビューのマッピングを指定する際に下のように文字列して指定している場合、いままでは/download/foobar/とかにアクセスしたときに初めて"download_file"という名前の関数を探しに行って「見つかりません」というエラーになっていたんだけども、urlタグを使うと「ビューからURLへ」の対応表を作る関係上全部のビューが定義済である必要がある。

    (r'^new_game/(?P<key>\d+)/$', 'new_game'),
    (r'^download/(?P<key>\d+)/$', 'download_file'),

なので、urls.pyからまだ中身を定義していない行をコメントアウトするか、views.pyで空のビューを定義してやればいい。

def download_file(request, key):
    raise NotImplementedError

settings.new_gameを探しているエラー

Caught an exception while rendering: Reverse for 'settings.programmers_nightmare.views.new_game' with arguments '('',)' and keyword arguments '{}' not found.


settingsというよけいなプレフィックスがついて検索に失敗している。URLパターンに名前を付けるなどを試してみたけど解決しない。google-app-patchの使っているurlsauto.pyが悪さをしているんじゃないか、とかもしかしてurlタグの実装との相性が悪いのか、とか思って調べてみた。urlタグは「与えられた引数で検索してみて見つからなかった場合にSETTINGS_MODULEのピリオド以前を付け加えて再検索する」という設計になっている。つまり、なぞのsettingsがついているけども単純に「検索が失敗した」と言っているだけ。

    try:
        url = reverse(self.view_name, args=args, kwargs=kwargs)
    except NoReverseMatch:
        project_name = settings.SETTINGS_MODULE.split('.')[0]
        try:
            url = reverse(project_name + '.' + self.view_name,
                          args=args, kwargs=kwargs)
        except NoReverseMatch:

改めてエラーログを見てみると、ああなるほど。変数idが未定義(=空文字列)なのでパターンにマッチしなかったんだ。それを修正したら問題なく動いた。

Caught an exception while rendering: Reverse for 'settings.programmers_ightmare.views.new_game' with arguments '('',)' and keyword arguments '{}' not found.

「変数が未定義の時は空文字列を返す」というテンプレートの挙動って割と罠なので、「そんな変数はないよ!」って例外を投げるStrictモードをつけたい気分なのだけど
無効な値の扱いによれば「Admin サイトのテンプレートをはじめとする多くのテンプレートは、実在しない値を呼び出しても何も出力しないという動作を前提に作られています。」ということなのでそうもいかないようだ。


まあurlタグがこけたら引数の変数名をtypoしていないか、ちゃんと期待した値が入っているか、を確認するのがいいのだろう。