はじめての Django アプリ作成、その 4

revision-up-to:17812 (1.4)

このチュートリアルは チュートリアルその 3 の続き です。ここでは、引続き Web 投票アプリケーションの開発を例にして、簡単なフォー ム処理とコードの縮小化を中心に解説します。

簡単なフォームを書く

それでは、前回のチュートリアルで作成した Poll の詳細ビュー用テンプレート ("polls/detail.html") を更新して、 HTML <form> エレメントを入れてみ ましょう:

<h1>{{ poll.question }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="/polls/{{ poll.id }}/vote/" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}"
     value="{{ choice.id }}" />
    <label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
{% endfor %}
<input type="submit" value="投票する" />
</form>

簡単に説明しましょう:

  • 上のテンプレートでは、 Poll の選択肢ごとにラジオボタンを表示していま す。各ラジオボタンの value は Choice の ID に関連づけられています。 ラジオボタンの name はいずれも "choice" です。つまり、投票者 がラジオボタンのいずれかを選択してフォームを提出 (submit) すると、 choice=3 という内容のPOST データを送信します。これは HTML フォー ムの基本ですね。
  • フォームの action/polls/{{ poll.id }}/vote/ に設定し、 method="post" にしています。 (method="get" ではなく) method="post" を使っている点は極めて重要です。というのも、このフォー ムの提出はサーバ側のデータの更新につながるからです。サーバ側のデータ を更新するようなフォームを作成するときは、常に method="post" を使 いましょう。これは Django 固有の話ではなく、いわば Web 開発の王道です。
  • forloop.counter は、 for タグのループが何度実行されたかを 表す値です。
  • データが改ざんされる恐れのある POST のフォームを作成しているので、クロス サイトリクエストフォージェリ (Cross Site Request Forgeries) のことを心配 する必要があります。 ありがたいことに、 Django がこれに対応するとても使いやすい仕組みを提供し てくれているので、あまり心配する必要はありません。手短に言うと、全ての 自サイトへ向けての POST フォームに対しては {% csrf_token %} テンプレートタグを使いましょう。

{% csrf_token %} タグは、テンプレートコンテキストから はアクセスできないようなリクエストオブジェクトの情報を必要とします。このた めに、少し detail ビューに変更を加える必要があります。変更を加えた後は 以下のようになります:

from django.template import RequestContext
# ...
def detail(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p},
                               context_instance=RequestContext(request))

これがどのように動くかについての詳細は、 RequestContext で詳しく説明し ています。

さあ、今度は提出されたデータを処理するための Django ビューを作成しましょう。 チュートリアルその 3 で、以下のような行を polls アプリケーションの URLconf に入れたことを思い出しましょう:

(r'^(?P<poll_id>\d+)/vote/$', 'vote'),

また、 vote() 関数のダミーの実装も作成しました。今度は本物を作成しま しょう。以下を polls/views.py に追加してください:

from django.shortcuts import get_object_or_404, render_to_response
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from django.template import RequestContext
from polls.models import Choice, Poll
#...
def vote(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Poll 投票フォームを再表示します。
        return render_to_response('polls/detail.html', {
            'poll': p,
            'error_message': "選択肢を選んでいません。",
        }, context_instance=RequestContext(request))
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # ユーザが Back ボタンを押して同じフォームを提出するのを防ぐ
        # ため、POST データを処理できた場合には、必ず
        # HttpResponseRedirect を返すようにします。
        return HttpResponseRedirect(reverse('polls.views.results', args=(p.id,)))

このコードには、これまでのチュートリアルで扱っていなかったことがいくつか 入っています:

  • request.POST は辞書ライクなオ ブジェクトです。このオブジェクトを使うと、キー名を使って入力されたデー タにアクセスできます。この例では、 request.POST['choice'] で投票者の選んだ選択肢を文字列で返させています。 request.POST に入っている値は 常に文字列です。

    Django では、 POST と同様、 GET データにアクセスするための request.GET も提供しています。 ただし、このコードでは、POST を経由した呼び出しでないとデータを更新さ せないようにするために、 request.POST を明示的に使って います。

  • choice が POST データ上になければ、 request.POST['choice']KeyError を送出します。上のコードでは KeyError をチェッ クして、 choice がない場合にはエラーメッセージ付きの Poll フォー ムを再表示しています。

  • choice のカウントを増やした後で、 HttpResponse ではなく HttpResponseRedirect を返しています。 HttpResponseRedirect はリダイレクト先の URL 一 つだけを引数にとります (ここでは reverse() を使って URL を生成していま すが、これについては後で説明します)。

    上のコードの Python コメント文で指摘しているように、 POST データの処 理に成功したときは常に HttpResponseRedirect を 返してください。これは Django 固有の話ではなく、 Web 開発の王道です。

  • 例では、 HttpResponseRedirect のコンストラクタ の中で reverse() という関数を使ってい ます。この関数を使うと、ビュー関数中での URL のハードコードを防げます。 reverse() にはビューの名前を渡し、同 時に URL パターンからビューにマップするときに取り出される変数を指定し ます。上の例では、 reverse()チュートリアルその 3 で設定した URLconfに従っ て:

    '/polls/3/results/'
    

    のような URL を返します。 3p.id の値です。リダイレクト先 の URL は 'results' ビューを呼び出し、最終的なページを表示します。 (プレフィクスを含めた) ビューの完全な名前を指定せねばならないので注意 してください。

チュートリアルその 3 で触れたように、 requestHTTPRequest オブジェクトです。 HTTPRequest の詳細は リクエスト・レスポンスオブジェクトのドキュメント を参照してください。

投票者が Poll に投票すると、 vote() ビューは開票結果ページにリダイレク トします。開票ページを書きましょう:

def results(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/results.html', {'poll': p})

テンプレート名が違うことだけを除き、 チュートリアルその 3detail() とほとんど同 じですね。この冗長さは後で修正することにします。

今度は results.html テンプレートを作成します:

<h1>{{ poll.question }}</h1>

<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }} -- {{ choice.votes }}</li>
{% endfor %}
</ul>

<a href="/polls/{{ poll.id }}/">Vote again?</a>

さあ、ブラウザで /polls/1/ を表示して、投票してみましょう。票を入れるた びに、結果のページが更新されていることがわかるはずです。選択肢を選ばずにフォー ムを提出すると、エラーメッセージを表示するはずです。

汎用ビューを使う: コードが少ないのはいいことだ

チュートリアルその 3detail()results() という二つのビューはバカバカしいくらいに単純で、先程も述べた ように冗長です。(これまた チュートリアルその 3 の) Poll のリストを表示する index() ビューも同様です。

こうしたビューは、基本的な Web 開発においてよくあるケース。すなわち、URL を 介して渡されたパラメタに従ってデータベースからデータを取り出し、テンプレー トをロードして、レンダリングしたテンプレートを返す、というケースを体現して います。これはきわめてよくあるケースなので、 Django では「汎用ビュー (generic view)」というショートカットのシステムを提供しています。

汎用ビューとは、よくあるパターンを抽象化して、 Python コードすら書かずにア プリケーションを書き上げられる状態にしたものです。

これまで作成してきた polls アプリケーションを汎用ビューシステムに変換して、 コードをばっさり捨てられるようにしましょう。変換にはほんの数ステップしかか かりません。そのステップとは:

  1. URLconf を変換する。
  2. 不要になった古いビューを削除する。
  3. 新しいビュー用に URL のハンドリングを修正する。

です。詳しく見てゆきましょう。

なぜ今更コードを入れ換えるの?

一般に Django アプリケーションを書く場合は、まず自分の問題を解決するため に汎用ビューが適しているか考えた上で、最初から汎用ビューを使い、途中ま で書き上げたコードをリファクタすることはありません。ただ、このチュート リアルでは中核となるコンセプトに焦点を合わせるために、わざと「大変な」 ビューの作成に集中してもらったのです。

電卓を使う前に、算数の基本を知っておかねばならないのと同じです。

まず polls の URLconf である polls/urls.py を開きます。チュートリアルで のこれまでの作業から、中身は以下のようになっているはずです:

from django.conf.urls import patterns, include, url

urlpatterns = patterns('polls.views',
    url(r'^$', 'index'),
    url(r'^(?P<poll_id>\d+)/$', 'detail'),
    url(r'^(?P<poll_id>\d+)/results/$', 'results'),
    url(r'^(?P<poll_id>\d+)/vote/$', 'vote'),
)

これを以下のように変更しましょう:

from django.conf.urls import patterns, include, url
from django.views.generic import DetailView, ListView
from polls.models import Poll

urlpatterns = patterns('',
    url(r'^$',
        ListView.as_view(
            queryset=Poll.objects.order_by('-pub_date')[:5],
            context_object_name='latest_poll_list',
            template_name='polls/index.html')),
    url(r'^(?P<pk>\d+)/$',
        DetailView.as_view(
            model=Poll,
            template_name='polls/detail.html')),
    url(r'^(?P<pk>\d+)/results/$',
        DetailView.as_view(
            model=Poll,
            template_name='polls/results.html'),
        name='poll_results'),
    url(r'^(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
)

この例では二つの汎用ビュー、 ListViewDetailView を使っています。こ れらのビューはそれぞれ、「オブジェクトのリストを表示する」および「あるタイ プのオブジェクトの詳細ページを表示する」という二つの概念を抽象化しています。

  • 各汎用ビューは自分がどのモデルに対して動作するのか知っておく必要があ ります。これは model パラメタによって提供されます。
  • DetailView 汎用ビューには、 "pk" という名前で URL から プライマリキー をキャプチャ して渡すことになっています。そこで、汎用ビュー向けに poll_idpk に書き換えてあります。
  • 結果を表示するビューに poll_results という名前をつけてあります。 こうすると、このビューを呼び出すような URL を後で生成できます (詳しくは「 名前付きパターン <naming-url-patterns> 」の説明を参照し てください。)また、 django.conf.urls モジュールの url() 関数を使っています。上の例のように、 パターン名を指定する場合には、 url() を使う よう薦めます。

デフォルトでは、 DetailView 汎用 ビューは <app name>/<model name>_detail.html という名前のテンプレート を使います。私達のアプリケーションでは、テンプレートの名前は "polls/poll_detail.html" です。 template_name 引数は Django に自動生成されたデフォルトのテンプレート名 ではなく、指定した名前を使うように伝えるために使われます。また、 result リストビューにも template_name を指定します。これは結果 (result) ビューと詳細 (detail) ビューが、お互い実は DetailView であるにも関わらず、 レンダリングされたときに違った外観を持っているためです。

同様に、 ListView 汎用ビューも <app name>/<model name>_list.html という名前のテンプレートを使うので、 template_name を使って ListView に既存の polls/index.html テンプレートを使用するように伝えます。

このチュートリアルの前の部分では、 polllatest_poll_list といった変数の入ったコンテキスト (context) をテンプレートに渡していました。 DetailView には、 poll という変数が自動的に渡されます。なぜなら、今 私達は Django モデル (Poll) を使用していて、 Django はコンテキスト 変数にふさわしい名前を決めることができるからです。一方で、 ListView では、 自動的に生成されるコンテキスト変数は poll_list となります。これを上書 きするには、 context_object_name オプションを与えて、 latest_poll_list を代わりに使用すると指定します。この代替アプローチと して、新しいデフォルトのコンテキスト変数と一致するようにテンプレートを変 えることもできます。しかし、ただ Django に使用したい変数名を伝えるほうが 簡単でしょう。

さて、 index(), detail() および results() ビューのコードを polls/views.py から削除できるようになりました。これらのビュー関数は汎用 ビューで置き換わったので、もう必要ありません。

最後に、 URL が汎用ビューを指すように修正します。上の vote ビューでは、 reverse() 関数を使って URL のハードコードを 防いでいます。汎用ビューに切替えたので、 reverse() を変更して、URL が新しく追加した 汎用ビューを指すようにします。汎用ビューのビュー関数を使えれば簡単なのです が、汎用ビューというものは一つのサイトの中で何度も使われることがあるので、 そういうわけにはいかないのです。そこで、先程指定しておいたビューの名前を使 います:

return HttpResponseRedirect(reverse('poll_results', args=(p.id,)))

サーバを実行して、新しく汎用ビューベースにした投票アプリケーションを使って みましょう。

汎用ビューの詳細は 汎用ビューのドキュメント を参照してく ださい。

次回予告

このチュートリアルはここでしばらく中断します。今後は以下のような内容をカバー する予定です:

  • 高度なフォーム処理
  • RSS フレームワークを使う
  • キャッシュフレームワークを使う
  • コメントフレームワークを使う
  • 管理サイトの高度な機能: パーミッション
  • 管理サイトの高度な機能:カスタム JavaScript

さしあたっては、 次のステップへ に進むとよいでしょ う。