Refactoring My Intelligence

技術的/非技術的な雑記,備忘録等々

Django1.5 tutorial Part3 個人的メモ

Part 3ではwiewについて説明している.

哲学

  • view - はDjnagoアプリ中の一つのWebページに相当する.これは通常指定された(HTMLを表現する)テンプレートを持ち指定された機能を提供する.このチュートリアルのWeb-Pollアプリでは以下のviewを持つ

  • Poll "index" ページ - 最近のPollを表示する

  • Poll "detail" ページ - ひとつのPoll questionを表示.結果ではなく投票用のフォーム表示する
  • Poll "result" ページ - 特定のPollの結果を表示する
  • Vote action - あるPollにおける特定の選択の投票を操作する

DjangoではWebページやその他のコンテンツはviewによってもたらさせる.DjangoはリクエストのURLを基に一つのviewを選択する.URLからviewを得るためにDjangoではURLConfsという仕組みを使う.URLConfsはURLの(文字列)パターンとviewを紐付けるもので,urls.pyファイルに紐付け情報を記述する.

最初のviewを書く

polls/views.pyに以下を記述する.

# polls/views.py

from django.http import HttpResponse

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

このviewを呼ぶためにはURLConfを使ってURLにこのindex()関数を紐付ける必要がある.polls/urls.pyを作成して内容を以下のようにする.

# polls/urls.py

from django.conf.urls import patterns, url

from polls import views

urlpatterns = patterns('',
    url(r'^$', views.index, name='index')
)

次の手順はルートのURLConf(settings.pyで指定する.デフォルトはプロジェクト名のディレクトリ配下に生成されたurls.py)内でpolls.urlsモジュールにリンクを追加することである.これはmysite/urls.py中でinclude()メソッドを追加することで実現する.

# mysite/urls.py

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)),
)

これでhttp://localhost:XXXX/polls/で上記で作成したpolls.views.index()の結果が得られる.

url()について

url()関数は二つの必須引数(regex, view)と二つのオプション引数(kwargs, name)をとる.

url() 引数: regex

urlパターンを指定する正規表現.この正規表現にマッチすればviewに指定された関数を呼び出す.この正規表現ではURL文字列中のGETやPOSTメソッドのパラメータ,ドメイン名の部分はマッチング対象外である.

例) 
リクエスト: http://www.example.com/myapp/ の場合
URLConfでパターンマッチング対象となるのは:myapp/

リクエスト: http://www.example.com/myapp/?page=3 の場合
URLConfでパターンマッチング対象となるのは:myapp/

url() 引数: view

regexでマッチしたURLはこのview引数で指定した関数に処理を渡す.viewを呼び出す際に,HttpRequestオブジェクトを第一引数にし,正規表現で捕捉された項目をそれ以降の引数として渡す.regexで単純に捕捉した場合はその順番での引数として渡されるが,名前付きで捕捉した場合はキーワード引数として渡される.

url() 引数: kwargs

任意のキーワード引数をview関数に渡す.この引数は辞書型で渡す.

url() 引数: name

Django中でURLを一意に識別するために用いられる

さらにviewを書く

indexとは違う(今度は引数ありの)viewをいくつか追加する.

# polls/views.py

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)

これらの新規viewを以下のようにしてpolls.urlsモジュールに接続する.

# polls/urls.py

from django.conf.urls import patterns, url

from polls import views

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

include()メソッドはマッチしたURLの文字列部分を取り除き,残りの文字列部分をインクルードされるURLConfに渡す.

実際に何か行うviewを書く

viewは以下のうちどちらを行う責務を持つ

  • リクエスト内容を含んでいるHttpResponseオブジェクトを返す
  • Http404等の例外を発生させる

以下の例はpolls/にアクセスしたときに表示するindexページを最近5件のPoll questionをカンマ区切りで表示する.

# polls/views.py

from django.http import HttpResponse

from polls.models import Poll

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

これはページデザインをハードコードしているので問題である.こういった問題に対処するためにDjangoではテンプレートシステムを提供している.

templateの作り方

  1. polls/templatesディレクトリを作る
  2. さらにその下にpollsディレクトリを作る
  3. そこにindex.htmlファイルを作る
  4. 以下のコードを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 %}
{% endif %}

出来あがったindex.htmlは以下のパスとなる.

polls/templates/polls/index.html

何故templatesディレクトリの下に再度pollsディレクトリを作るかは,他のアプリのhtmlファイル等のリソースと名前が重複しないようにするため.

そしてpolls/views.pyのindex()を次のように変更する.

# polls/views.py

from django.http import HttpResponse
from django.template import Context, loader

from polls.models import Poll

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

このコードはtemplates/polls/index.htmlのテンプレートをロードし,コンテキストに通す.contextはテンプレート変数とPythonオブジェクトをマッピングさせるための辞書である.

ショートカット:render()

テンプレートをロードするコードは定石なのでショートカットが存在する.index()を書きなおしたものは以下の通り.

# polls/views.py

from django.shortcuts import render

from polls.models import Poll

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

render()メソッドは:

  • 第一引数にrequestオブジェクト
  • 第二引数にテンプレートの名前
  • 第三引数に辞書型データ(テンプレート変数とPythonオブジェクトのマッピング)

を受け取り,HttpResponseオブジェクトを返す.第三引数はオプショナルらしい. 上記に変更すると,loaderとContextはインポートする必要は無くなる.

404エラーを発生させる

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

もし要求されたIDのpollが存在しなければhttp404例外を投げる.上記コードでコンテキストに渡したpollオブジェクトはテンプレート中で以下のように書くと参照出来る.

{{ poll }}

ショートカット: get_object_or_404()

get()とHttp404のraiseは頻出イディオムなのでこれにもショートカットが存在する.以下はdetail viewを書きなおしたものである.

from django.shortcuts import render, get_object_or_404

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

get_object_or_404()は第一引数にDjangoのModelオブジェクトを受け取り,任意の数のキーワード引数をそれ以降で受け付ける.これらはmodelsのマネージャのget()メソッドに通すものである.もしオブジェクトが存在しなけばHttp404例外を投げる.

404(page not found)viewを書く

Http404例外があるviewで発生したら,Djangoは404エラーを操作するハンドラ―をロードしようとする.DjangoはルートURLConf中のhandler404変数を参照し,そこに登録されたハンドラ―(通常の場合と同様のview)を呼び出す.

500(server error)viewを書く

同様にルートURLConfにはhandler500も定義できる.これはサーバーエラー(ステータスコードが500)に対応するviewを呼ぶ.

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

コンテキスト変数としてpollが与えられたとしたときのpolls/detail.htmlは以下のようになる.

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

Djangoのテンプレートシステムでは,変数の属性へのアクセスはdot-lookup構文(ドットでオブジェクトを区切るおなじみのやつ)で行う.その手順は以下の通り({{ poll.question }}を例として説明)

  1. オブジェクトpoll上の辞書探索を行う.
  2. もし見つからなければオブジェクトの属性探索を行う.(上記例ではこれで見つかる)
  3. もし見つからなければリストでの探索を行う.

より詳しいテンプレートの情報はここを参照

テンプレート中のハードコードされたURLの削除

今のpolls/index.htmlはURLがハードコードされている.

<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>

これだと沢山のテンプレートを使うプロジェクトではURLを変更するのが非常に難しくなる.でもurlsモジュール(urls.py)でurl()関数に名前を付けていた場合,その名前でURLを参照することが出来る.

<li><a href="{% url 'detail' poll.id %}">{{ poll.question }}</a></li>

こうすると,もしURLを変更したくなったらテンプレートを修正するのではなく,polls/urls.pyを変更すればよい.例えば以下のように

# added the wod 'specifics'
url(r'^/specifics/(?P<poll_id>\d+)/$', views.detail, name='detail')

URL名の名前空間

{% url %}の仕組みは便利だが,一つのプロジェクト中の異なるアプリ同士で同じURLを使っている場合,DjangoはどちらのアプリのURLかが判断出来なくなる.このような問題に対応するためにURLの名前空間を利用する.利用方法はプロジェクトのurls.pyurl()関数にnamespaceキーワード引数を指定すればよい.

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

from django.contrib import admin
admin.autodiscover()

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

こうした後にテンプレートのURL指定を以下のようにすればOK.

<li><a href="{% url 'polls:detail' poll.id %}">{{ poll.question }}</a></li>