はじめての Django アプリ作成、その 3
| revision-up-to: | 5804 (0.97pre SVN) |
|---|
このチュートリアルは チュートリアルその 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 の詳細は URLconf のドキュメント を参照してください。
チュートリアルその 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.defaults import *
urlpatterns = patterns('',
(r'^polls/$', 'mysite.polls.views.index'),
(r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)
復習しておきましょう。だれかが Web サイトのあるページ、例えば "/polls/23" をリクエストすると、 Django は ROOT_URLCONF に書かれているこの Python モジュールをロードします。Django はモジュール内に urlpatterns という変 数をみつけ、正規表現を順に検査してゆきます。マッチする正規表現が見つかった (r'^polls/(?P<poll_id>\d+)/$' ですね) ところで、 Django はこの正規表現 に関連づけられている Python パッケージ/モジュールである mysite.polls.views.detail をロードします。これは mysite.polls.views.py ファイルに定義されている detail() という関数 に対応します。最後に、 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$', 'mysite.polls.views.index'),
とはいえ、こんな阿呆なことはやめましょう。
これらの正規表現では GET や POST のパラメタ、またドメイン名を検索しないこと に注意してください。例えば、 http://www.example.com/myapp/ というリクエ ストが来ると、 URLconf は /myapp/ を探します。 http://www.example.com/myapp/?page=3 の場合にも、 URLconf は /myapp/ を探します。
正規表現をよく理解できないなら、 Wikipedia のエントリ や Python のドキュメント を参照してください。また、オライリーから出版されて いる本、「詳説 正規表現」 (Jeffrey Friedl 著、 訳注: 田和 勝 訳) に秀逸 な解説があります。
最後に、パフォーマンスについての注意です: 正規表現は URLconf モジュールをロー ドする時にコンパイルされ、極めて高速に動作します。
はじめてのビュー作成
さてと、まだビューを作っていませんね。まだ URLconf しかありません。しかし、 まずは Django が URLconf を正しく理解できているか調べておきましょう。
Django 開発サーバを起動してください:
python manage.py runserver
Web ブラウザで "http://localhost:8000/polls/" に行ってみましょう。以下のよ うなメッセージの入った綺麗に彩られたエラーページが表示されるはずです:
ViewDoesNotExist at /polls/ Tried index in module mysite.polls.views. Error was: 'module' object has no attribute 'index'
このエラーは、まだ index() という関数を mysite/polls/views.py に書 いていないために発生しています。
"/polls/23/" や "/polls/23/results/" 、 "/polls/23/vote/" も試して下さい。 エラーメッセージから、 Django がどのビューを試した (そしてまだビューを書い ていないので失敗した) かがわかります。
いよいよ最初のビューを書きましょう。 mysite/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)
ブラウザを使って "/polls/34/" を見てください。このビューは URL に指定した ID を表示します。
実際に動くビューを作成する
各ビューは、リクエストされたページのコンテンツが入った HttpResponse オ ブジェクトを返すか、 Http404 のような例外を送出するかの 2 つの動作のう ち、いずれかを実行せねばなりません。それ以外の処理は全てユーザにゆだねられ ています。
ビューはデータベースからレコードを読みだしても、読み出さなくてもかまいませ ん。 Django のテンプレートシステム (あるいはサードパーティの Python テンプ レートシステム) を使ってもよいですし、使わなくてもかまいません。 PDF ファイ ルを生成しても、 XML を出力しても、 ZIP ファイルをオンザフライで生成しても かまいません。 Python ライブラリを使ってやりたいことを何でも実現できます。
Django にとって必要なのは HttpResponse です。あるいは例外です。
簡単のため、チュートリアルその 1 で解説した Django のデータベース API を使っ てみましょう。 index() ビューを、システム上にある最新の 5 件の質問項目 をカンマで区切り、日付順に表示するようにしてみます:
from mysite.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 mysite.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.py の TEMPLATE_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>{{ poll.question }}</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 mysite.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})
この作業によって、 loader や Context 、 HttpResponse を 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 が存在 しないときに django.http.Http404 を送出します。
ショートカット: 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 を送出します。
設計哲学
なぜ DoesNotExist 例外を高水準で自動的にキャッチせず、ヘルパー関数 get_object_or_404() を使うのでしょうか、また、なぜモデルAPI に DoesNotExist ではなく Http404 を送出させるのでしょうか?
答えは、「モデルレイヤとビューレイヤをカップリングしてしまうから」です。 Django の最も大きな目標の一つは、ルーズカップリングの維持にあります。
get_list_or_404() という関数もあります。この関数は get_object_or_404() と同じように働きますが、 get() ではなく filter() を使います。リストが空の場合に Http404 を送出します。
404 応答 (ページを見つけられません) 用のビューを作る
ビュー内で Http404 を送出すると、 Django は 404 エラー処理用の特殊なビュー をロードします。このビューは handler404 という変数を参照して見つけます。 変数は URLconf のコールバックで使っているのと同じ、 Python のドット表記法で 表した関数名の文字列です。 404 ビュー自体に特殊なところはありません。単なる 普通のビューです。
普通はわざわざ苦労して 404 ビューを書く必要はありません。デフォルトの URLconf の設定の一番上には以下のような行があるはずです:
from django.conf.urls.defaults import *
handler404 の設定はこの import で現在のモジュールに取り込まれています。 django/conf/urls/defaults.py を見れば分かりますが、 handler404 のデ フォルト値は 'django.views.defaults.page_not_found' に設定されています。
404 ビューには、あと 3 つ注意すべき点があります:
- 404 ビューは、 Django が URLconf の全ての正規表現をチェックして、一致 するものがなかった場合にも呼び出されます。
- 自分で 404 ビューを定義せず、単にデフォルトの設定を使う (そうするよう 勧めます) 場合にも、一つだけやらねばならないことがあります: 404.html テンプレートをテンプレートディレクトリのルートに作成せね ばなりません。デフォルトの 404 ビューは、このテンプレートを全ての 404 エラー表示に使います。
- (設定モジュールで) DEBUG を True に設定している場合、 Django は 404 ビューを使わず、トレースバックを表示します。
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 は poll オブジェクトの question() というメソッドを呼び出そうとし ます。
メソッド呼び出しは {% for %} ループの中で行われています。 poll.choice_set.all は、 Python のコード poll.choice_set.all() に解 釈されます。その結果、 Choice オブジェクトからなるイテレーション可能オブジェ クトを返し {% for %} タグで使えるようになります。
テンプレートの詳しい動作については テンプレートガイド を参照してください。
URLconf の単純化
しばらくビューとテンプレートシステムをいじってみてください。 URLconf を編集 していくうちに、実はかなり冗長な部分があることに気づくかもしれません:
urlpatterns = patterns('',
(r'^polls/$', 'mysite.polls.views.index'),
(r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)
明らかに、どのコールバックにも mysite.polls.views が入っています。
これはよくあることなので、 URLconf フレームワークではプレフィクスを共有する ためのショートカットがあります。共通のプレフィクスを切り出して、以下のよう に patterns() の最初の引数に追加してください:
urlpatterns = patterns('mysite.polls.views',
(r'^polls/$', 'index'),
(r'^polls/(?P<poll_id>\d+)/$', 'detail'),
(r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
(r'^polls/(?P<poll_id>\d+)/vote/$', 'vote'),
)
機能的には前の形式と全く同じです。でもとてもすっきりしましたね。
URLconf の脱カップリング
ところで、すこし時間を割いて polls アプリケーションの URL 設定を Django プ ロジェクトの設定から切り離しましょう。 Django アプリケーションはプラグ可能、 つまりあれこれ設定しなくても、特定のアプリケーションを別の Django インストー ルに移動できるようになっています。
今のところ、 python manage.py startapp で作成した厳密なディレクトリ構成 のおかげで polls アプリケーションはかなりうまく脱カップリングできていますが、 一点だけ現在の Django の設定とカップリングしている場所があります: それは URLconf です。
これまでは URL 設定を mysite/urls.py で編集してきましたが、本来アプリケー ションの URL 設計はアプリケーション固有のものであって、特定の Django インス トールとは関係のないものです。そこで、 URL 設定をアプリケーションディレクト リ内にもってきましょう。
mysite/urls.py ファイルを mysite/polls/urls.py にコピーしてください。 次に mysite/urls.py から polls に関する URL 設定を全て削除し、以下の include() を一つだけ書きます:
(r'^polls/', include('mysite.polls.urls')),
簡単に説明すると、 include() は別の URLconf への参照です。正規表現に $ (文字列の末尾にマッチする) が付いておらず、代りにスラッシュがついてい ることに注意してください。 Django は include() を見つけると、リクエスト URL 中のマッチした部分を切り離し、残りの部分を include() している URLconf に送って処理させます。
従って、このシステムでユーザが "/polls/34/" にアクセスすると、次のように処 理されます:
- Django が '^polls/' にマッチすることを発見します。
- Django はマッチ部分のテキスト ("polls/") を取り去り、残りのテキストで ある "34/" を 'mysite.polls.urls' という URLconf に送り、処理させます。
これでプロジェクト側の脱カップリングはできました。今度は 'mysite.polls.urls' 側を脱カップリングするために、 URLconf の各行から、 先頭の "polls/" を削除してください:
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'),
)
include() と URLconf の脱カップリングの背景には、 URL のプラグ & プレイ を簡単にしようという発想があります。今や polls は独自の URLconf を持つよう になったので、置き場所は "/polls/" や "/fun_polls/" 、"/content/polls/" 、 その他どんな URL ルート下にも置けるようになり、どこに置いてもきちんと動作す るようになりました。
polls アプリケーションは絶対 URL ではなく相対 URL だけに注意しているわけで す。
ビューの作成に慣れたら、 チュートリアルその 4 に進んで、簡単なフォーム処 理と汎用ビュー (generic view) の使い方を学びましょう。