django: ログイン画面をメッセージでカスタマイズするためのデコレータ

ウェブサイトにユーザを引き込むには、できるだけauth無しで機能を体験させてあげ、本当に必要なところに来たら登録またはロクインを求めるようにしたい。無名のサイトで何もできる前にログイン強制したらユーザは帰てしまうから。

djangoではログインが必要なviewに@login_requiredデコレータを被せて必要に応じてログイン画面に飛ばすのが一般的だ。この際、ログイン画面にviewごとのメッセージを入れると親切だ。「投稿するにはログインが必要です」とか「お気に入りを付けるにはログインしてください」などのメッセージをログイン画面の上に出すとユーザの理解を得やすくなると思う。

ログイン画面のテンプレートはmsgとうい変数がある場合はそれを表示するようにする。あとはviewとメッセージを結び付けるだけ。このようにできたら簡潔でいい:


@add_message_to_login('「投稿するにはログインが必要です」')
@login_required
def private_stuff(request):
""" ログインを要するview """

しかし、このように特化したデコレータでなくてもこのケースは対応できる。


def extend_qs_if_redirect(**qs):
"""
viewハンドラーがRedirectのリスポンスを返したときはクエリーストリングをqdで拡張する
viewデコレータ。

使い方:
@extend_qs_if_redirect(msg='投稿する前にログインしてね。')
@login_required
def private_stuff(request):
...
これで、/login/?next=/hoge/ が /login/?next=/hoge/&msg=...となる
"""
def wrapper_maker(handler):
def wrapper(request, *args, **kw):
"""
if response is redirect, then add qs to query string.
"""
rsp=handler(request, *args, **kw)
if isinstance(rsp, HttpResponseRedirect):
import urllib, urlparse
rsp['Location']=Url(rsp['Location']).query_update(**qs)
return rsp
return wrapper
return wrapper_maker

しかし、これにはセキュリティー上の問題がある。クエリストリングをいじることにより、任意のメッセージをページに挿入することが可能になってしまう。ここはメッセージをIDと結び付けてそれを渡したほうが良さそうだ。そこでこのようにした。


@extend_qs_if_redirect(msg_id=msg.msg_map().index_for('投稿の前にログインお願い'))
@login_required
def toukou(request):
...

ここでmsg_mapはテキストと整数indexを対応づけるミニデータベース。index_for()はテキストを必要あれば挿入し、そのIDを返す。ログイン画面のviewではmsg_idからテキストに戻し、これをテンプレートに渡す:


msg=msg.msg_map().value_for(int(request.REQUEST['msg_id']))

このデコレータでログインページのメッセージをview毎にカスタマイズできるようになった。おまけに、viewがRedirectを出す場合は、その行き先のurlにクエリストリングを簡単に追加できるようにもなった。デコレータって素敵。

ここまで読んでくれた読者にはソースを差し上げます:
http://tengu.us/file/django_extend_qs_if_redirect.py.txt