概要
DjangoドキュメントのチュートリアルPart3中”Write views that actually do something“の冒頭でビュー関数でのモデル操作を実装した後、テンプレートファイルを使った表示を実装している。
テンプレートディレクトリー/ファイルの作成
テンプレートファイルの内容
polls
ディレクトリー下にtemplates
ディレクトリーを、その下にpolls
ディレクトリーを作成する。そのディレクトリー下に以下の内容でindex.html
ファイルを作成する。
これから、polls/views
モジュールからこのテンプレートを参照するよう編集していく。
1 2 3 4 5 6 7 8 9 |
{% 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 %} |
パスの書き方の注意点
テンプレートファイルのアンカータグは以下のように書かれている。
1 |
<a href="/polls/{{ question.id }}/"> |
当初/polls/{{ ... }}
の部分の先頭の'/'
を抜かして書いてしまって、polls/{{ ... }}
としたところ、URLが以下のようになってしまった。
http://localhost:8000/polls/polls/2/
現在のアプリケーションからの相対パスになってしまったためで、
http://localhost:8000/polls/ + /polls/2/
と解釈されてしまう。先頭のスラッシュを入れるとホスト名をルートとするパスとなる。ただしこの記事の最後の方にあるDTLのurl
タグを使うと、このような心配をしなくて済むようになる。
テンプレートの探索
ディレクトリー・ファイル構成は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
polls ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── polls │ └── index.html ├── tests.py ├── urls.py └── views.py |
config/settings.py
で、以下のように設定している。
INSTALLED_APPS
に'polls.apps.PollsConfig'
を登録
→polls
が探索に含まれるTEMPLATES
の'APP_DIRS'
がTrue
に設定
→polls/templates
が探索に含まれる
ので、polls/templates
とそのサブディレクトリー下がテンプレートファイルの探索場所に含まれる。
ビューにおけるレンダリング
loaderによる標準形
ビューの編集
テンプレートファイルを導入した場合の標準形は以下の構成になる。
django.template
パッケージのloader
オブジェクトをインポートloader.get_template('テンプレートへのパス')
でテンプレートを読み込み- テンプレートの
render
メソッドでレンダリングした結果を、HttpResponse
に渡す
→render(context, request)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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] # output = ', '.join([q.question_text for q in latest_question_list]) template = loader.get_template('polls/index.html') context = { 'latest_question_list': latest_question_list, } return HttpResponse(template.render(context, request)) |
動作確認
http://localhost:8000/polls
にアクセスすると、2つのリンクが表示される。
このHTMLのソースコードは以下のようになっている。
1 2 3 4 |
<ul> <li><a href="/polls/2/">How're you doing?</a></li> <li><a href="/polls/1/">What's up?</a></li> </ul> |
それぞれのリンクをクリックしたときの遷移先URLとブラウザー表示は以下の通り。
href="/polls/2/"
http://localhost:8000/polls/2/
- You’re looking at question 2.
href="/polls/1/"
http://localhost:8000/polls/1/
- You’re looking at question 1.
renderショートカット
loader
を用いた標準形では、
- テンプレートのローダーによってテンプレートファイルを読み込み、
- これをレンダリングした結果を
HttpResponse
に渡す、
という2段階の処理となっている。Djangoにはこれを1行で済ませるrender
ショートカットが準備されている。render
ショートカットを使って、index
ビューをより簡潔にしてみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from django.http import HttpResponse #from django.template import loader from django.shortcuts import render 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)) return render(request, 'polls/index.html', context) .... |
1行目のHttpResponse
のインポートはまだ手を入れていない他のビュー関数のためで、render
による処理には必要ない。loader
のインポートも不要で、必要なのは3行目のrender
のインポートのみ。
14行目がrender
ショートカットを導入した効果で、この1行でリクエスト、テンプレート、context
の指定を済ませてレスポンスを返している。
render(request, 'テンプレートパス', context)
404エラーの実装
存在しないidへの反応
index
テンプレートではQuestion
の2つの要素が表示され、それぞれid=1, 2
をリクエストパラメーターとして渡している(href="/polls/1/"
など)。ところがQuestion
に存在しないid
を指定したURLでアクセスすると以下のようなエラーがブラウザーに表示される。
1 2 |
DoesNotExist at /polls/3/ Question matching query does not exist. |
このエラーの代わりに、404エラーで反応させたい。
例外処理による標準形
ビューの編集
現在のdetail
ビューは以下のような内容。
1 2 3 4 5 6 7 8 9 |
from django.http import HttpResponse from django.shortcuts import render .... def detail(request, question_id): return HttpResponse("You're looking at question %s." % question_id) .... |
存在しないインスタンスを指定すると例外が投げられるので、それをキャッチして404エラーをレイズするようにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from django.http import HttpResponse 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("Qustion does not exist!!") return render(request, 'polls/detail.html', {'question': question}) .... |
テンプレートの準備
またrender
で指定しているdetail
テンプレートを、polls/template/polls
ディレクトリーに以下の内容で作成しておく。
1 |
Detail: {{ question }} |
実行結果の確認
これで存在しないid
の値を指定したときのエラーは以下のように変化する。メッセージがHttp404
クラスのコンストラクターに与えたテキストになっている。
get_object_or_404ショートカット
Djangoには、上のような複数行の例外処理を一行で書くget_object_or_404
ショートカットが準備されている。
以下のようにget_object_or_404
ショートカットをインポートすると、1行でQuestion
データを取得し、存在しなければ404エラーをレイズしてくれる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from django.http import HttpResponse #from django.http import Http404 from django.shortcuts import render, get_object_or_404 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}) .... |
存在しないidを指定したときのエラー表示は以下のようになる。エラーメッセージの内容は固定されていて変更できないようだ。
なおsettings.py
のDEBUG
をFalse
にすると、ブラウザー表示は以下の2行のより簡素な内容になる
1 2 |
Not Found The requested resource was not found on this server. |
テンプレートシステム
DTLによる記述
先ほどのindex
テンプレートの内容は1行で変数を表示するだけの内容だったが、これをテンプレートらしくDTL~テンプレート言語を使って以下のように書き換える。
1 2 3 4 5 6 7 |
<h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}</li> {% endfor %} </ul> |
そうすると大きな見出しでQuestion
データのテキストが表示され、そのQuestion
データに関連付けられたChoice
データのテキストが箇条書きで表示される。
今回のDTLでは、オブジェクトの属性を得るのにドット検索が行われている。通常のPythonの場合リストや辞書型のオブジェクトの要素は[]
で参照されるが、DTLではこの記述方法が使えない。代わりにドットを使って以下の順序で探していく
- 辞書型のキー
- オブジェクトの属性
- リストのインデックス
今回の例では、辞書型のキーで取得できず、オブジェクトの属性として取得に成功する。
URLのハードコーディング回避
ハードコーディングの問題
現時点でindex
テンプレートのアンカー要素のURLは"/polls/{{ question_id }}/"
と書かれている。
1 2 3 4 5 6 7 8 9 |
{% 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 %} |
この場合にリンクが張られるdetail
のURLはurlsで定義されていて、たとえば以下の通り。
http://localhost/polls/1
ここでプロジェクト進行中に以下のように変更することとなったとする。
http://localhost/polls/specifics/1
この場合、index
テンプレートのアンカー要素の内容を全て以下のように変更する必要がある。
href="/polls/specifics/{{ question.id }}/"
さらにindex
テンプレート以外でもこのルーティングを使っている場合、その分だけ変更が生じてしまう。
ハードコーディングの回避
urls.pyの確認
まずpolls/urls.py
のdetail
ビューへのルーティングにname='detail'
で名前が付けられていることに留意。
1 2 3 4 5 6 7 8 9 |
from django.urls import path from polls import views 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'), ] |
ビューの変更
index/view.py
中、アンカー要素のURL部分を以下のように変更する。
href="..."
のURL部分を{% url %}
に入れ替え- このタグの引数に、
urls
のname
で指定した名前とパラメーターを書く
1 |
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> |
一般化した書き方は以下の通り。このとき、先頭や末尾の'/'
は不要。
{% url 'urlsのnameで定義した名前' クエリーパラメーター %}
HTMLの確認
生成されるHTMLは以下の通り。
1 2 3 4 5 6 7 |
<ul> <li><a href="/polls/2/">How're you doing?</a></li> <li><a href="/polls/1/">What's up?</a></li> </ul> |
URLの変更
URLの変更が必要な場合、1箇所だけ、urls
の内容を変更すればよい。
1 |
path('specifics/<int:question_id>/', views.detail, name='detail'), |
以下のように生成されるHTMLもすべて変更される。
1 2 3 4 5 6 7 |
<ul> <li><a href="/polls/specifics/2/">How're you doing?</a></li> <li><a href="/polls/specifics/1/">What's up?</a></li> </ul> |
そして以下のようにアクセスできるようになる。
1 |
http://localhost:8000/polls/specifics/1/ |
URLの名前空間管理
名前空間管理の必要性
DTLのurl
タグで、ルーティングに付けた名前を使って抽象化することができた。ところが、先の例の場合polls
アプリケーションのdetail
と同じ名前を他のアプリケーションでも使う場合、見分けがつかない。
そこで、ルート名がどのアプリケーションのものか分かるようにする必要がある。
urls.pyの編集
urls.py
に以下の1行を追加する。
1 2 3 4 5 6 7 8 9 10 11 |
from django.urls import path from polls 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'), ] |
indexテンプレートの編集
次にindex
テンプレートの名前指定部分を、以下のように'アプリケーション名:ルート名'
に変更。
1 2 3 4 5 6 7 8 9 |
{% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} |
この場合の実行結果は変わらないが、他のアプリケーションでもdetail
という名前のルーティングがある場合でも、polls
アプリケーションの同名のdetail
が識別される。
urls
にapp_name
が定義されている状態でurl
タグでpolls:
を書かないと以下のエラーになる。
1 2 |
NoReverseMatch at /polls/ Reverse for 'detail' not found. 'detail' is not a valid view function or pattern name. |