暇人じゃない

「はじめての Django アプリ作成」チュートリアルをやったメモ その4
DjangoPython

はじめての Django アプリ作成、その 4 — Django v1.0 documentation http://djangoproject.jp/doc/ja/1.0/intro/tutorial04.html

Django | Writing your first Django app, part 4 | Django documentation http://docs.djangoproject.com/en/1.2/intro/tutorial04/

前回の続きでチュートリアルを追ってみた個人的メモです。

環境:

  • MacOS X 10.6.4
  • Python 2.6.5
  • Django 1.2.3

簡単なフォームを書く

詳細ビューのテンプレート polls/detail.html に投票フォームを追加する。

# {{ poll.question}} {% if error_message %}
<p>**{{ error_message }}**</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="Vote" />
</form>
  • forloop.counter とは、for ループが何回実行されたかを表す。
  • 日本語版のチュートリアルには無いけど、csrf_token が必要。

csrf_token

settings.py で...:

MIDDLEWARE_CLASSES = (
    #
    'django.middleware.csrf.CsrfViewMiddleware',
)

CsrfViewMiddleware を有効にしていると、CSRF チェックをしてくれる。 POST 処理を行う場合、render_to_response() を使用していると、Context だけではなく、RequestContext も渡さないといけない。

detail() 関数を以下のように書き換える 質問に対する選択肢が表示されるページ。

# polls/views.py
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))

こんな感じになる。

質問詳細ページ

2010 11 06 1

vote() 関数を作成する 選択肢を選んで投票した時に呼び出される関数。

# polls/views.py
from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.template import RequestContext
from mysite.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()
        # ユーザーが戻るボタンを押して同じフォームを送信するのを防ぐため
        # POST データを処理できた場合には必ず HttpResponseRedirect を返す
        return HttpResponseRedirect(reverse('mysite.polls.views.results', args=(p.id,)))

try 部分: request.POST['choice'] で、POST で送信された choice というキーのデータを参照できる。 GET データにアクセスするための request.GET もある。

except 部分: 選択肢が選ばれていなかった時に詳細ページを呼び出す。 detail() 関数でやったのと同じように、RequestContext を渡すようにする。

選択肢が選択されずに投票した場合のエラー表示

2010 11 06 2

else 部分: choice のカウントを増やした後で、HttpResponseRedirect を返している。 引数はリダイレクト先の URL を指定する。 POST データの処理に成功した時は常に HttpResponseRedirect を返すこと!

HttpResponseRedirect 内で使用されている reverse() とは、ビューの名前を渡し、パラメーターを指定する。 例の場合だと、URLconf に従って '/polls/1/results/'のような URL を返す。

CakePHP だとこんな書き方かな:

$this->redirect(array('controller' => 'polls', 'action' => 'results', $id));

results() 関数を作成する 投票処理が行われた後に表示される開票結果のページ。

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

polls/mytemplates/polls/results.html:

# {{ poll.question }} {% for choice in poll.choice_set.all %} * {{ choice.choice
}} -- {{ choice.votes }} vote{{ choice.votes|pluralize }} {% endfor %}

vote{{ choice.votes|pluralize }} という書き方をすると、choice.votes が 2 つ以上の場合は votes と複数形にしてくれる。

開票結果(results)ページ

2010 11 06 3

汎用ビューを使う

detail()/results()/index() は URL を介して渡されたパラメーターに従ってデータベースからデータを取り出し、テンプレートをロードしてレンダリングしたテンプレートを返すという、よくあるパターン。 これらを簡略化するために、Django では、汎用ビュー(generic views)というシステムが提供されている。

汎用ビュー(generic views) よくあるパターンを抽象化して、コードを書かずにアプリケーションを書けるようにしたもの。

polls アプリケーションを汎用ビューシステムに変換する

# polls/urls.py
from django.conf.urls.defaults import *

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

これを以下のように変更する。

# polls/urls.py
from django.conf.urls.defaults import *
from mysite.polls.models import Poll

info_dict = {
    'queryset': Poll.objects.all()
}

urlpatterns = patterns('',
    (r'^$', 'django.views.generic.list_detail.object_list', info_dict),
    (r'^(?P<object_id>\d+)/$',
        'django.views.generic.list_detail.object_detail', info_dict),
    url(r'^(?P<object_id>\d+)/results/$',
        'django.views.generic.list_detail.object_detail',
        dict(info_dict, template_name='polls/results.html'), 'poll_results'),
    (r'^(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),

)

ポイント

  • 汎用ビューはそれぞれ...

    • object_list: オブジェクトのリストを表示する
    • object_detail: オブジェクトの詳細をページを表示する という概念を抽象化している。
  • 各汎用ビューは、自分がどのデータに対して動作するのか知っておく必要がある。 info_dict という辞書内の queryset というキーが、この汎用ビューで操作するオブジェクトのリストを指している。
  • 汎用ビューには object_id という名前で URL から ID が渡ってくる。 pollid を objectid に書き換えている。
  • 結果を表示するビューに poll_results という名前をつけている。
  • object_detail() 汎用ビューは <app_name>/<model_name>_detail.html という名前のテンプレートを使う。 polls/detail.html を poll_detail.html というファイル名に変更する。
  • object_list() 汎用ビューは <app_name>/<model_name>_list.html という名前のテンプレートを使う。 polls/index.html を poll_list.html というファイル名に変更する。
  • results については、dict(info_dict, template_name='polls/results.html') と指定している。 これは object_detail が複数あるので、手動でテンプレートを指定しないと同じテンプレートを使用しようとするため。 dict() を使用して、新しい辞書形式で指定しているのもポイント。

テンプレートを変更

poll_list.html

{% if object_list %} {% for object in object_list %} * [{{ object.question
}}](/polls/{{ object.id }}/ "{{ object.question }}") {% endfor %} {% else %}
<p>No polls are available.</p>
{% endif %}

poll_detail.html

# {{ object.question}} {% if error_message %}
<p>**{{ error_message }}**</p>
{% endif %}

<form action="/polls/{{ object.id }}/vote/" method="post">
  {% csrf_token %} {% for choice in object.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="Vote" />
</form>

results.html

# {{ object.question }} {% for choice in object.choice_set.all %} * {{
choice.choice }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }} {%
endfor %}

index()/detail()/results() 関数を削除する

vote() 関数の rendertoresponse() 内の poll コンテキスト変数を object に変更する

# polls/views.py
return render_to_response('polls/detail.html', {
    'object': p,
    'error_message' : "選択肢を選んでいません。",
}, context_instance=RequestContext(request))

HttpResponseRedirect 内のリダイレクト先を poll_results に変更する

# polls/views.py
return HttpResponseRedirect(reverse('poll_results', args=(p.id,)))

About

chocoby (GitHub / Twitter)

フリーのソフトウェア開発者です。 Ruby を使った Web 開発を得意としています。