revision-up-to: | 8961 (1.0) |
---|
このチュートリアルは チュートリアルその 2 の続き です。ここでは、引続きWeb 投票アプリケーションの開発を例にして、公開用のイ ンタフェース、ビュー(view) の作成を焦点に解説します。
ビューとは、 Django のアプリケーションにおいて特定の機能を提供するウェブペー ジの「型 (type)」であり、独自のテンプレートを持っています。例えばブログアプ リケーションなら、以下のようなビューがあるでしょう:
Poll アプリケーションの場合には、以下の 4 つのビューを作成します:
Django では、各ビューは簡単な Python の関数として表現されます。
ビューを書く上での最初のステップは、 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.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" とい う投票項目の入ったブリットリストを表示します。
テンプレートをロードしてコンテキストに値を入れ、テンプレートをレンダリング した結果を 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 オブジェクトにし て返します。
さて、今度は 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 を送出します。
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 を送出します。
ビュー内で 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 と同じように、 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 を編集 していくうちに、実はかなり冗長な部分があることに気づくかもしれません:
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'),
)
機能的には前の形式と全く同じです。でもとてもすっきりしましたね。
ところで、すこし時間を割いて 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 は ~django.conf.urls.defaults.include を見つけると、リクエスト URL 中のマッ チした部分を切り離し、残りの部分を include されている URLconf に送って処理 させます。
従って、このシステムでユーザが "/polls/34/" にアクセスすると、次のように処 理されます:
これでプロジェクト側の脱カップリングはできました。今度は '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) の使い方を学びましょ う。
Aug 31, 2012