はじめての Django アプリ作成、その 3Link to this heading

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

オーバービューLink to this heading

ビューとは、 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 は「URLconf」と呼ばれているものを使います。URLconf はURLパターンをビューにマッピングします。

このチュートリアルでは URLconf の基本的な使い方を知ってもらいます。より詳しくは URL ディスパッチャ を参照してください。

もっとビューを書いてみるLink to this heading

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

polls/views.py
Python
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)

次の path() コールを追加して、新しいviewを polls.urls モジュールと結びつけます。

polls/urls.py
Python
from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/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 に渡します。これは '<int:question_id>/' に一致し、結果として下記のように detail() が呼び出されます。

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

question_id=34 の部分は <int:question_id> から来ています。 山括弧を使用すると、URLの一部が「キャプチャ」され、キーワード引数としてビュー関数に送信します。 文字列の :question_id> 部分は、一致するパターンを識別するために使用される名前を定義し、<int: 部分は、URLパスのこの部分に一致するパターンを決定するコンバータです。

.html のような文字列をURLに追加する必要はありません。ただし、次のようにすれば、表現できます:

Code
path('polls/latest.html', views.index),

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

実際に動作するビューを書くLink to this heading

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

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

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

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

polls/views.py
Python
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 はそこからテンプレートを探します。

プロジェクトの TEMPLATES には、Django がどのようにテンプレートをロードしレンダリングするかが書かれています。デフォルトの設定ファイルでは、 DjangoTemplates バックエンドが設定されており、その APP_DIRS のオプションが True になっています。規約により、 DjangoTemplatesINSTALLED_APPS のそれぞれの "templates" サブディレクトリを検索します。

先ほど作成した templates ディレクトリ内で polls というディレクトリを作成し、さらにその中に index.html というファイルを作成してください。つまり、テンプレートは polls/templates/polls/index.html に書く必要があります。 app_directories テンプレートローダは前述(訳注: APP_DIRS の説明)のように動くため、Django 内でこのテンプレートを単に polls/index.html のように参照できます。

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

polls/templates/polls/index.html
Django template
{% 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
Python
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()Link to this heading

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

polls/views.py
Python
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)

全部の view をこのように書き換えてしまえば、 loaderHttpResponse を import する必要はなくなります (detailresultsvote を引き続きスタブメソッドにするなら、 HttpResponse はそのままにしておいたほうがいいでしょう)。

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

404 エラーの送出Link to this heading

さて、質問詳細ビューに取り組みましょう。このページは、指定された投票の質問文を表示するものです。ビューは次のようになります:

polls/views.py
Python
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
Django template
{{ question }}

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

ショートカット: get_object_or_404()Link to this heading

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

polls/views.py
Python
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.

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

テンプレートシステムを使うLink to this heading

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

polls/templates/polls/detail.html
Django template
<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を削除Link to this heading

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

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

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

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

これが機能するのは、 polls.urls モジュールに指定されたURLの定義を検索するからです。 'detail' のURL名は以下の箇所で定義されています:

Code
...
# the 'name' value as called by the {% url %} template tag
path('<int:question_id>/', views.detail, name='detail'),
...

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

Code
...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...

URL 名の名前空間Link to this heading

このチュートリアルプロジェクトが持つアプリは polls アプリ1つだけです。実際の Django プロジェクトでは、5個、10個、20個、あるいはそれ以上のアプリがあるかもしれません。 Django はどうやってこれらの間の URL 名を区別するのでしょうか? 例えば、 polls アプリは detail ビューを含みますが、同じプロジェクトにブログのためのアプリがあり、そのアプリも同名のビューを含むかもしれません。 {% url %} テンプレートタグを使ったとき、 Django はどのアプリのビューに対して url を作成すればいいでしょうか? これを Django にどう知らせればいいでしょうか。

答えは、 URLconf に名前空間を追加すること、です。どうぞ polls/urls.py ファイル内で app_name を追加し、アプリケーションの名前空間を設定してください。

polls/urls.py
Python
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

それでは polls/index.html テンプレートを変更します。以下の形から、

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

以下の形にし、名前空間つきの詳細ビューを指すようにします:

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

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