Skip to content

3.ビューとテンプレート#

ビューを追加#

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)

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

polls/urls.py
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()が呼び出される。

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

各ビューには二つの役割がある 1. リクエストされたページのコンテンツを含むHttpResponseオブジェクトを返すこと 2. Http404 のような例外の送出

Django にとって必要なのは HttpResponse か、あるいは例外。 試しに次のような 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 はそこからテンプレートを探す

  • プロジェクトの TEMPLATES には、Django がどのようにテンプレートをロードしレンダリングするかが書かれている。

  • デフォルトの設定ファイルでは、 DjangoTemplates バックエンドが設定されており、その APP_DIRS のオプションが True になっている。
  • 規約により、 DjangoTemplates は INSTALLED_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 %}
    

Info

チュートリアルを短くするために、すべてのテンプレートの例では不完全なHTMLを使用しています。独自のプロジェクトでは 完全な HTML ドキュメント を使用する必要があります。

このテンプレートを使用するために polls/views.py の index ビューを更新する

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 オブジェクトにマッピングする辞書。
  • http://localhost:8000/polls/を開くと「Whtat'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)

全部の view をこのように書き換えてしまえば、 loader や HttpResponse を import する必要はなくなる (detail 、 results 、 vote を引き続きスタブメソッドにするなら、 HttpResponse はそのままにしておいたほうがいい)。 render() 関数は、第1引数として request オブジェクトを、第2引数としてテンプレート名を、第3引数(任意)として辞書を受け取ります。この関数はテンプレートを指定のコンテキストでレンダリングし、その HttpResponse オブジェクトを返します。

404 エラーの送出#

このビューは、リクエストした ID を持つ質問が存在しないときに Http404 を送出する。

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

polls/detail.html テンプレートには以下のように記述する

detail.html
{{ question }}

get_object_or_404()#

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

get_object_or_404() 関数は、Django モデルを第一引数に、任意の数のキーワード引数を取り、モデルのマネージャの get() 関数に渡します。オブジェクトが存在しない場合は Http404 を発生させます。

Tip

なぜ ObjectDoesNotExist 例外を高水準で自動的にキャッチせず、ヘルパー関数 get_object_or_404() を使うのでしょうか、また、なぜモデル API に ObjectDoesNotExist ではなく、 Http404 を送出させるのでしょうか? 答えは、モデルレイヤとビューレイヤをカップリングしてしまうからです。 Django の最も大きな目標の一つは、ルーズカップリングの維持にあります。いくつかの制御カップリングは、 django.shortcuts モジュールの中にあります。

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

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

コンテキスト変数を 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 モジュール の path() 関数で name 引数を定義したので、テンプレートタグの {%url%} を使用して、URL 設定で定義されている特定の URL パスへの依存をなくすことができる:

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

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

path('<int:question_id>/', views.detail, name='detail'),

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

path('specifics/<int:question_id>/', views.detail, name='detail'),

URL 名の名前空間#

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

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

polls/urls.py
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 テンプレートを以下の形から、

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

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

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