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

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

オーバービュー

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

  • Blog ホームページ - 最新エントリーをいくつか表示

  • エントリー詳細ページ - 1エントリーへのパーマリンク (permalink) ページ

  • 年ごとのアーカイブページ - 指定された年のエントリーの月を全て表示

  • 月ごとのアーカイブページ - 指定された月のエントリーの日をすべて表示

  • 日ごとのアーカイブページ - 指定された日の全てのエントリーを表示

  • コメント投稿 - エントリーに対するコメントの投稿を受付

投票アプリケーションでは、以下4つのビューを作成します:

  • 質問 “インデックス” ページ – 最新の質問をいくつか表示

  • 質問 “詳細” ページ – 結果を表示せず、質問テキストと投票フォームを表示

  • 質問 “結果” ページ – 特定の質問の結果を表示

  • 投票ページ – 特定の質問の選択を投票として受付

Django へだ、各ページとコンテンツはビューによって提供されます。各ビューは単純に Python 関数 (クラスベースビューの場合はメソッド) として実装されています。 Django はビューを、リクエストされたURLから決定します (正確には、URLのドメイン移行の部分)

インターネットサーフィンをしてるときなどに、 “ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B” のような美しいURLを見かけることがあるでしょう。 Django はこんなものよりもっとエレガントな “URLパターン” を提供していると知っておいてください。

URLパターンはURLをシンプルに一般化したものです。例えば “/newsarchive/<year>/<month>/” などです。

URLからビューには、 Django は ‘URLconfs’ として知られているものを使います。 URLconf は (正規表現で表される) URLパターンとビューを対応付けます。

このチュートリアルでは基本的な URLconf の使い方を紹介します。詳細については django.urls を参照してください。

もっとビューを書いてみる

それではもう少しviewを polls/views.py に追加していきましょう。これから追加するviewでは引数をとります。

polls/views.py
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

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

以下の url() コールを追加して、新しいviewを polls.urls と結びつけます。

polls/urls.py
from django.conf.urls import url

from . import views

urlpatterns = [
    # ex: /polls/
    url(r'^$', views.index, name='index'),
    # ex: /polls/5/
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    # ex: /polls/5/results/
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    # ex: /polls/5/vote/
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

お使いのブラウザで、 “/polls/34/” を見てください。 detail() メソッドが実行され、URLで提供したIDが表示されます。”/polls/34/results/” と “/polls/34/vote/” も試してください。結果と投票ページのプレースホルダがそれぞれ表示されます。

誰かがWebサイトの “/polls/34/” をリクエストすると、 Django は ROOT_URLCONF に指定されている、 Python モジュール mysite.urls をロードします。そのモジュール内の urlpatterns という変数を探し、その中に入っている正規表現を順に検査してゆきます。 '^polls/' に合致した箇所を見つけた後、一致した文字列 ("polls/") を除き、残りの文字列である "34/" を次の処理のために ‘polls.urls’ の URLconf に渡します。これは r'^(?P<question_id>[0-9]+)/$' に合致し、結果として下記のように detail() が呼ばれます。

detail(request=<HttpRequest object>, question_id='34')

question_id='34' の部分は、 (?P<question_id>[0-9]+) から来ています。パターンの前後に括弧を使用すると、そのパターンにマッチしたテキストを “キャプチャ” し、ビュー関数の引数として、それを送信します。 ?P<question_id> はマッチしたパターンを識別するために使用する名前を定義します。 [0-9]+ は一桁以上の数字(すなわち、数)にマッチする正規表現です。

URL パターンは正規表現であるため、正規表現で実現できる限り制限はありません。また、 .html のような文字列を URL に追加する必要もありません。ただし、次のようにすれば、表現できます:

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

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

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

各ビューには二つの役割があります: 一つはリクエストされたページのコ ンテンツを含む HttpResponse オブジェクトを返すこと、もう一つは Http404 のような例外の送出です。それ以外の処理はユーザ次第です。

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

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

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

polls/views.py
from django.http import HttpResponse

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

このコードには問題があります。ビューの中で、ページのデザインがハードコードされています。ページの見栄えを変更するたびに、 Python コードを編集する必要があります。 Django のテンプレートシステムを使って、ビューから使用できるテンプレートを作成し、Python からデザインを分離しましょう。

最初に、 polls ディレクトリの中に、 templates ディレクトリを作成します。 Django はそこからテンプレートを探します。

Django がどのようにテンプレートをロードしレンダリングするかについて、プロジェクトの TEMPLATES の設定を説明します。デフォルトの DjangoTemplates の設定ファイルは、 APP_DIRS のオプションが True に設定されています。設定により、 DjangoTemplatesINSTALLED_APPS のそれぞれのサブディレクトリの “templates” を検索します。

先ほど作成した templates ディレクトリ内では、 polls と呼ばれる別のディレクトリを作成し、その中に index.html というファイルを作成します。つまり、テンプレートは polls/templates/polls/index.html にする必要があります。そのため、 app_directories テンプレートローダは前述したように、Django 内でこのテンプレートを polls/index.html として参照することができます。

テンプレートの名前空間

作ったテンプレートを polls という別のサブディレクトリを作らずに、直接 polls/templates の中に置いてもいいのではないか、と思うかもしれませんね。しかし、それは実際には悪い考えです。Django は、名前がマッチした最初のテンプレートを使用するので、もし 異なる アプリケーションの中に同じ名前のテンプレートがあった場合、Django はそれらを区別することができません。そのため、Django に正しいテンプレートを教えてあげる必要がありますが、一番簡単な方法は、それらに 名前空間を与える ことです。アプリケーションと同じ名前をつけた もう一つの ディレクトリの中にテンプレートを置いたのは、そういうわけなのです。

テンプレートには次のコードを書きます:

polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

テンプレートを使用するために polls/views.pyindex ビューを更新してみましょう:

polls/views.py
from django.http import HttpResponse
from django.template import loader

from .models import Question


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

このコードは、 polls/index.html というテンプレートをロードし、コンテキストを渡します。コンテキストは、テンプレート変数名を Python オブジェクトへのマッピングしている辞書です。

ブラウザで “/polls/” を開くと、 チュートリアルその2 で作った、 “What’s up” という質問の入ったブレットリストを表示します。リンクは質問の詳細ページを指します。

ショートカット: render()

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

polls/views.py
from django.shortcuts import render

from .models import Question


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

この作業によって、 loaderHttpResponse ( detailresultsvote のスタブメソッドがある場合は HttpResponse のままにします) を import する必要はなくなりました。

render() 関数は、第1引数として request オブジェクト、第2引数としてテンプレート名、第3引数としてその他のオプション辞書を受け取ります。この関数はテンプレートを指定のコンテキストでレンダリングし、 HttpResponse オブジェクトを返します。

404 エラーの送出

指定された投票の質問文を表示するページの詳細ビューを片付けましょう。ビューは次のようになります:

polls/views.py
from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

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

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

polls/templates/polls/detail.html
{{ question }}

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

ショートカット: get_object_or_404()

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

polls/views.py
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

The get_object_or_404() function takes a Django model as its first argument and an arbitrary number of keyword arguments, which it passes to the get() function of the model’s manager. It raises Http404 if the object doesn’t exist.

設計思想

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

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

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

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

投票アプリの detail() ビューに戻りましょう。コンテキスト変数 question とすると、 polls/detail.html テンプレートは次のようなります:

polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

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

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

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

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

polls/index.html テンプレートで質問へのリンクを書いたとき、リンクの一部は次のようにハードコードされていました:

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

このハードコードされた、密結合のアプローチの問題は、多くのテンプレートを伴うプロジェクトで、URLを変更することを困難にすることです。しかし、 polls.urls モジュール内の url() 関数の name 引数を定義したため、テンプレートタグの {% url %} を用いることで、 URL 設定で定義された特定の URL パスへの依存をなくすことができます:

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

これが機能する方法は、 polls.urls モジュールに指定されたURLの定義を調べることです。’detail’ のURL名は次に定義されている場所を正確に見ることができます:

...
# the 'name' value as called by the {% url %} template tag
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...

投票の詳細ビューの URL を何か他のものに変更したい場合、たとえば polls/specifics/12/ のように変更したいとき、対象となる(複数の)テンプレートを変更する代わりに、 polls/urls.py を変更します:

...
# added the word 'specifics'
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
...

URL 名の名前空間

チュートリアルプロジェクトは「polls」というただ一つのアプリを含みます。本物のDjangoプロジェクトでは、これらは5, 10 , 20かより多くのアプリになるでしょう。Djangoはどうやってこれらの間のURL 名を区別するのでしょうか?例えば、「polls」アプリは詳細ビューを含みます、同じプロジェクトにブログのためのアプリがあるかもしれません。どうすれば、Djangoは {% url %} テンプレートタグを使ってurlに対してどのアプリのビューを作成すればいいと知ることができるのでしょう?

URLconfに名前空間を追加すること、が答えです。 polls/urls.py ファイル内の、アプリケーションの名前空間を設定するため app_name の箇所に向かい追加します。

polls/urls.py
from django.conf.urls import url

from . import views

app_name = 'polls'
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
    url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

それでは polls/index.html テンプレートを変更します:

polls/templates/polls/index.html
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

詳細ビューの名前空間を指します:

polls/templates/polls/index.html
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

ビューを書けるようになったら、 チュートリアルその4 に進んで、簡単なフォームの処理と汎用ビューについて学びましょう。

Back to Top