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

revision-up-to:17812 (1.4)

このチュートリアルは チュートリアルその 2 の続き です。ここでは、引続きWeb 投票アプリケーションの開発を例にして、公開用のイ ンタフェース、ビュー(view) の作成を焦点に解説します。

ビューの設計哲学

ビューとは、 Django のアプリケーションにおいて特定の機能を提供するウェブペー ジの「型 (type)」であり、独自のテンプレートを持っています。例えばブログアプ リケーションなら、以下のようなビューがあるでしょう:

  • Blog ホームページ – 最新のエントリをいくつか表示します。
  • エントリの「詳細」ページ – 一つのエントリへの恒久リンク (permalink) ページです。
  • 年ごとのアーカイブページ – エントリのある年を表示します。
  • 月ごとのアーカイブページ – ある年のエントリのある月を表示します。
  • 日ごとのアーカイブページ – ある月のエントリのある日を表示します。
  • コメント投稿 – あるエントリに対するコメントの投稿を受け付けます。

Poll アプリケーションの場合には、以下の 4 つのビューを作成します:

  • Poll の「アーカイブ」ページ – 最新の調査項目 (poll) をいくつか表示し ます。
  • Poll の「詳細」ページ – 調査項目と投票用フォームを表示します。開票結 果は表示しません。
  • Poll の「開票結果」ページ – ある調査項目に対する結果を表示します。
  • 投票ページ – ある調査項目のある選択肢に対する投票を受け付けます。

Django では、各ビューは簡単な Python の関数として表現されます。

URL スキームの設計

ビューを書く上での最初のステップは、 URL 構造の設計です。 URL 構造を定義す るには、 URLconf と呼ばれる Python モジュールを作成します。 Django は URLconf を使ってどの URL をどの Python コードに関連づけるかを決めています。

ユーザが Django で作られたページをリクエストすると、システムは ROOT_URLCONF 設定を探します。この設定には Python モジュールをドッ ト表記で表した文字列が入っています。 Django は指定されたモジュールをロード して、 urlpatterns という名前のモジュールレベル変数を探します。 urlpatterns は以下のような形式のタプルからなるシーケンスです:

(正規表現, Pythonのコールバック関数,  [, オプションの辞書オブジェクト])

Django は先頭のタプルから順に、リクエストされた URL とタプル内の正規表現が マッチするまでテストしてゆきます。

マッチする正規表現が見つかると、Django は該当するタプルに指定されているコー ルバック関数を呼び出します。コールバック関数には HttpRequest オブジェクトを第一引数として渡します。 さらに、正規表現内で「キャプチャした (captured)」値をキーワード引数として渡 します。(タプルの三番目の要素である) オプションの辞書オブジェクトが指定され ていれば、その内容も追加のキーワード引数として渡します。

HttpRequest` オブジェクトの詳細は リクエストオブジェクトとレスポンスオブジェクト を参照してください。 URLconf の詳細は URL ディスパッチャ を参照してください。

チュートリアルその 1 の冒頭で python django-admin.py startproject mysite を実行していれば、URLconf が mysite/urls.py にできているはずです。また、(settings.py の) ROOT_URLCONF 設定にも、自動的に値が入ります:

ROOT_URLCONF = 'mysite.urls'

さて、例題を使って練習する時が来ました。 mysite/urls.py を編集して、 以下のような内容にしましょう:

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

from django.contrib import admin
admin.autodiscover()

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

復習しておいたほうがよいですね。だれかが Web サイトのあるページ、例えば “/polls/23” をリクエストすると、 Django は ROOT_URLCONF に書かれている この Pythonモジュールをロードします。Django はモジュール内の urlpatterns という変数を探し、その中に入っている正規表現を順に検査して ゆきます。マッチする正規表現が見つかった (r'^polls/(?P<poll_id>\d+)/$' ですね) ところで、 Django はこの正規表現に関連づけられている Python パッケー ジ/モジュールである detail()polls/views.py からロードし ます。これは最後に、 Django は以下のような引数で detail() を呼び出し ます:

detail(request=<HttpRequest object>, poll_id='23')

poll_id='23' の部分は、 (?P<poll_id>\d+) からきています。パターンを 丸括弧で囲うと、パターンにマッチしたテキストを「キャプチャ」して、ビュー関 数の引数として送り込みます。 ?P<poll_id> はマッチしたパターンを識別する ための名前をつけています。 \d+ は数字の列 (すなわち番号) にマッチする正 規表現です。

URL パターンは正規表現なので、正規表現で実現できる限り無制限の URL を表現で きます。また、 .php のようなくだらない文字列を URL に追加する必要もあり ません。ただし、病的なユーモアの持ち主のために、以下のようにすれば実現でき ることは示しておきましょう:

(r'^polls/latest\.php$', 'polls.views.index'),

とはいえ、こんな阿呆なことはやめましょう。

これらの正規表現では GET や POST のパラメタ、またドメイン名を検索しないこと に注意してください。例えば、 http://www.example.com/myapp/ というリクエ ストが来ると、 URLconf は /myapp/ を探します。 http://www.example.com/myapp/?page=3 の場合にも、 URLconf は /myapp/ を探します。

正規表現をよく理解できないなら、 Wikipedia のエントリre モジュ ールのドキュメントを参照してください。また、オライリーから出版されている本、 「詳説 正規表現」 (Jeffrey Friedl 著、 訳注: 田和 勝 訳) に秀逸な解説が あります。

最後に、パフォーマンスについての注意です: 正規表現は URLconf モジュールをロー ドする時にコンパイルされ、極めて高速に動作します。

はじめてのビュー作成

さてと、まだビューを作っていませんね。まだ URLconf しかありません。しかし、 まずは Django が URLconf を正しく理解できているか調べておきましょう。

Django 開発サーバを起動してください:

python manage.py runserver

Web ブラウザで “http://localhost:8000/polls/” に行ってみましょう。以下のよ うなメッセージの入った綺麗に彩られたエラーページが表示されるはずです:

ViewDoesNotExist at /polls/

Could not import polls.views.index. View does not exist in module
polls.views.

このエラーは、まだ index() という関数を polls/views.py に書いていな いために発生しています。

“/polls/23/” や “/polls/23/results/” 、 “/polls/23/vote/” も試して下さい。 エラーメッセージから、 Django がどのビューを試した (そしてまだビューを書い ていないので失敗した) かがわかります。

いよいよ最初のビューを書きましょう。 polls/views.py というファイ ルを開いて、以下のような Python コードを書いてください:

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the poll index.")

最も単純なビューです。ブラウザで “/polls/” にアクセスすると、テキストが表示 されるはずです。

もう少しビューを追加しましょう。これらのビューはさきほどと少し違って、引数 をとります (この引数には、URLconf の正規表現内でキャプチャされたものが渡さ れます):

def detail(request, poll_id):
    return HttpResponse("You're looking at poll %s." % poll_id)

def results(request, poll_id):
    return HttpResponse("You're looking at the results of poll %s." % poll_id)

def vote(request, poll_id):
    return HttpResponse("You're voting on poll %s." % poll_id)

ブラウザを使って “/polls/34/” を見てください。これは detail() メソッドを 呼びだし、 URL に指定した ID を表示します。 “/polls/34/results/” と “/polls/34/vote/” を見てください。それぞれ開票結果ページと投票ページを表示 します。

実際に動くビューを作成する

各ビューは、リクエストされたページのコンテンツが入った HttpResponse オブジェクトを返すか、 Http404 のような例外を送出するかの 2 つの動作のうち、い ずれかを実行せねばなりません。それ以外の処理は全てユーザにゆだねられていま す。

ビューはデータベースからレコードを読みだしても、読み出さなくてもかまいませ ん。 Django のテンプレートシステム (あるいはサードパーティの Python テンプ レートシステム) を使ってもよいですし、使わなくてもかまいません。 PDF ファイ ルを生成しても、 XML を出力しても、 ZIP ファイルをオンザフライで生成しても かまいません。 Python ライブラリを使ってやりたいことを何でも実現できます。

Django にとって必要なのは HttpResponse か、あるいは 例外です。

簡単のため、 チュートリアルその 1 で解説した Django のデータベース API を使ってみましょう。 index() ビューを、システ ム上にある最新の 5 件の質問項目をカンマで区切り、日付順に表示させてみます:

from polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    output = ', '.join([p.question for p in latest_poll_list])
    return HttpResponse(output)

残念ながらこのコードには問題があります。ページのデザインがビュー中にハード コードされているのです。これでは、ページの見栄えを変えたくなるたびに Python コードをいじらねばなりません。というわけで、 Django のテンプレートシステム を使って、デザインと Python コードを分離しましょう:

from django.template import Context, loader
from polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    t = loader.get_template('polls/index.html')
    c = Context({
        'latest_poll_list': latest_poll_list,
    })
    return HttpResponse(t.render(c))

このコードは “polls/index.html” とい名前のテンプレートをロードし、テンプレー トにコンテキストを渡します。コンテキストとは、テンプレート変数名を Python のオブジェクトに対応づけている辞書です。

ページをリロードしましょう。エラーが出るようになったはずです:

TemplateDoesNotExist at /polls/
polls/index.html

そうそう、まだテンプレートを作ってませんね。まず、ファイルシステム上の Django がアクセス出来る場所にディレクトリを作成します (Django はサーバを実 行しているユーザと同じユーザ権限で動作します)。ただし、Web サーバのドキュメ ントルートには置かないようにしましょう。セキュリティへの配慮として、テンプ レートを公開するべきではないと思います。次に、 settings.pyTEMPLATE_DIRS を編集して、どこでテンプレートを探せばよいか Django に教えます。チュートリアルその 2 の「admin サイトのルック & フィール をカスタマイズする」でやったのと同じ作業です。

設定がおわったら、テンプレートディレクトリに polls というディレクトリを 作成します。このディレクトリの中に、 index.html という名前のファイルを 作成してください。 loader.get_template('polls/index.html') というコード は、ファイルシステム上の “[template_directory]/polls/index.html” に対応する ことに注意して下さい。

テンプレートには以下のようなコードを書きます:

{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

ブラウザでページをロードすると、チュートリアル 1 で作った “What’s up” とい う投票項目の入ったブレットリストを表示します。リンクは詳細ページを指します。

ショートカット: render_to_response()

テンプレートをロードしてコンテキストに値を入れ、テンプレートをレンダリング した結果を HttpResponse オブジェクトで返す、というイ ディオムは非常によく使われます。 Django はこのイディオムのためのショートカッ トを提供しています。ショートカットを使って index() ビューを書き換えてみ ましょう:

from django.shortcuts import render_to_response
from polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html',
                              {'latest_poll_list': latest_poll_list})

この作業によって、 loaderContextHttpResponse を import する必要はなくなりました。

render_to_response() 関数は第一引数にテンプレートの 名前をとり、オプションの第二引数に辞書をとります。 render_to_response() はテンプレートを指定のコンテキ ストでレンダリングし、 HttpResponse オブジェクトにし て返します。

404 の送出

さて、今度は Poll の詳細ページを片付けましょう。このページは Poll の質問文 (question フィールド) を表示します。ビューは以下のようになります:

from django.http import Http404
# ...
def detail(request, poll_id):
    try:
        p = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        raise Http404
    return render_to_response('polls/detail.html', {'poll': p})

新しい概念がでて来ました。このビューはリクエストした ID を持つ Poll が存在 しないときに Http404 を送出します。

polls/detail.html テンプレートに何を書けばよいかは後で解説しますが、さ しあたって上の例題を動かしたければ、単に:

{{ poll }}

と書いておいてください。

ショートカット: get_object_or_404()

get() を実行し、該当オブジェクトがな い場合には Http404 を返す、という作業は非常によく使われ るイディオムです。 Django はこのイディオムのためのショートカットを提供してい ます。ショートカットを使って detail() ビューを書き換えてみましょう:

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

get_object_or_404() は Django のモデルクラスを第一 引数にとり、任意の個数のキーワード引数をとります。キーワード引数はそのまま get() メソッドに渡ります。オブジェク トが存在しなければ Http404 を送出します。

設計哲学

なぜ ObjectDoesNotExist 例外を高水準で自 動的にキャッチせず、ヘルパー関数 get_object_or_404() を使うのでしょうか、また、 なぜモデルAPI に ObjectDoesNotExist では なく Http404 を送出させるのでしょうか?

答えは、「モデルレイヤとビューレイヤをカップリングしてしまうから」です。 Django の最も大きな目標の一つは、ルーズカップリングの維持にあります。

get_list_or_404() という関数もあります。この関数は get_object_or_404() と同じように働きますが、 get() ではなく filter() を使います。リストが空の 場合は Http404 を送出します。

404 応答 (ページを見つけられません) 用のビューを作る

ビュー内で Http404 を送出すると、 Django は 404 エラー 処理用の特殊なビューをロードします。このビューは URLconf にある handler404 という変数 ( URLconf にあるもののみ有効です。その他に設定さ れたものは影響がありません) を参照して見つけます。変数は URLconf のコール バックで使っているのと同じ、 Python のドット表記法で表した関数名の文字列で す。 404 ビュー自体に特殊なところはありません。単なる普通のビューです。

普通はわざわざ苦労して 404 ビューを書く必要はありません。もし handler404 を設定しなかった場合は、ビルトインビューである django.views.defaults.page_not_found() がデフォルトで使用されます。 この場合、 404.html テンプレートをテンプレートディレクトリのルートに 作成する必要があります。デフォルトの 404 ビューはこのテンプレートを全ての 404 エラーに対して使用します。もし (設定モジュールで) DEBUGFalse に設定していて、なおかつ 404.html を作成しなかった場合、 Http500 が代わりに送出されます。 404.html を作成することを覚えて おいてください。

404 ビューに関するいくつかの注意すべき点:

  • (設定モジュールで) DEBUGTrue に設定している場合、 Django は 404 ビューを使わず、トレースバックを表示します (404.html も使われません)。
  • 404 ビューは、 Django が URLconf の全ての正規表現をチェックして、一致 するものがなかった場合にも呼び出されます。

500 応答 (サーバエラー) 用のビューを作る

404 と同じように、ルートの URLconf で handler500 を定義できます。 handler500 はサーバエラーが生じたときに呼び出されるビューを指します。 サーバエラーになるのは、ビューコードに実行時エラーが生じた場合です。

テンプレートシステムを使う

detail() ビューに戻りましょう。コンテキスト変数を poll とすると、 polls/detail.html テンプレートは以下のように書けます:

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

テンプレートシステムはドットを使った表記法で変数の属性にアクセスします。 {{ poll.question }} を例にとると、まず Django は poll オブジェクト を辞書とみなして question の値を探します。これには失敗するので、今度は 属性として照合を行い、この場合は成功します。仮に属性の照合に失敗すると、 Django は リストインデックスでの照合を行おうとします。

メソッド呼び出しは {% for %} ループの中で行われています。 poll.choice_set.all は、 Python のコード poll.choice_set.all() に解 釈されます。その結果、 Choice オブジェクトからなるイテレーション可能オブジェ クトを返し {% for %} タグで使えるようになります。

テンプレートの詳しい動作は テンプレートガイド を 参照してください。

URLconf の単純化

しばらくビューとテンプレートシステムをいじってみてください。 URLconf を編集 していくうちに、実はかなり冗長な部分があることに気づくかもしれません:

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

明らかに、どのコールバックにも polls.views が入っています。

これはよくあることなので、 URLconf フレームワークではプレフィクスを共有する ためのショートカットがあります。共通のプレフィクスを切り出して、以下のよう に patterns() の最初の引数に追加してくださ い:

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

機能的には前の形式と全く同じです。でもとてもすっきりしましたね。

普通、一つのアプリに対してのプレフィクスを URLconf にある全てのコールバック に適用させたくはないので、複数の patterns() を結合 させることが出来ます。 mysite/urls.py は以下のようになるでしょう:

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

from django.contrib import admin
admin.autodiscover()

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

urlpatterns += patterns('',
    url(r'^admin/', include(admin.site.urls)),
)

URLconf の脱カップリング

ところで、すこし時間を割いて polls アプリケーションの URL 設定を Django プ ロジェクトの設定から切り離しましょう。 Django アプリケーションはプラグ可能、 つまりあれこれ設定しなくても、特定のアプリケーションを別の Django インストー ルに移動できるようになっています。

今のところ、 python manage.py startapp で作成した厳密なディレクトリ構成 のおかげで polls アプリケーションはかなりうまく脱カップリングできていますが、 一点だけ現在の Django の設定とカップリングしている場所があります: それは URLconf です。

これまでは URL 設定を mysite/urls.py で編集してきましたが、本来アプリケー ションの URL 設計はアプリケーション固有のものであって、特定の Django インス トールとは関係のないものです。そこで、 URL 設定をアプリケーションディレクト リ内にもってきましょう。

mysite/urls.py ファイルを polls/urls.py にコピーしてください。 次に mysite/urls.py から polls に関する URL 設定を全て削除し、以下の include() を一つだけ書くと、以下が残ります:

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

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^polls/', include('polls.urls')),
    url(r'^admin/', include(admin.site.urls)),
)

簡単に説明すると、 include`() は別の URLconf への 参照です。正規表現に $ (文字列の末尾にマッチする) が付いておらず、代り にスラッシュがついていることに注意してください。 Django は ~django.conf.urls.include を見つけると、リクエスト URL 中のマッ チした部分を切り離し、残りの部分を include されている URLconf に送って処理 させます。

従って、このシステムでユーザが “/polls/34/” にアクセスすると、次のように処 理されます:

  • Django は '^polls/' へのマッチを検出します。
  • Django はマッチ部分のテキスト ("polls/") を取り去り、残りのテキスト である "34/" を ‘polls.urls’ という URLconf に送り、処理させ ます。

これでプロジェクト側の脱カップリングはできました。今度は polls.urls 側を脱カップリングするために、 URLconf の各行から、 先頭の “polls/” を削除してください。 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'),
)

include() と URLconf の脱カップリングの背景には、 URL のプラグ & プレイを簡単にしようという発想があります。今や polls は独自の URLconf を持っているので、 “/polls/” や “/fun_polls/”, “/content/polls/” といった、どんな パスルート下にも置けて、どこに置いてもきちんと動作します。

polls アプリケーションは絶対パスではなく相対パスだけに注意しているわけです。

ビューの作成に慣れたら、 チュートリアルその 4 に 進んで、簡単なフォーム処理と汎用ビュー (generic view) の使い方を学びましょ う。