4. はじめての Django アプリ作成、その 3
出典:はじめての Django アプリ作成、その 3 | Django ドキュメント | Django
https://docs.djangoproject.com/ja/3.2/intro/tutorial03/
4.1. オーバービュー
Polls アプリ(投票アプリ)では以下の4つのビューを作成します。
- 質問 “インデックス” ページ — 最新の質問をいくつか表示
- 質問 “詳細” ページ — 結果を表示せず、質問テキストと投票フォームを表示
- 質問 “結果” ページ — 特定の質問の結果を表示
- 投票ページ — 特定の質問の選択を投票として受付
そのほか URLconf の基本的な使い方を学びます。
4.2. もっとビューを書いてみる
polls/views.py を編集してビューを追加します。
# インデックス ページ
def index(request):
#(省略)
# 詳細 ページ
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
# printf形式の文字列書式化を使用しています。このやり方は古いらしく、
# "You're looking at question {}.".format(question_id)
# とするほうがイマドキらしい。
# 7. 入力と出力 — Python 3.9.4 ドキュメント - 7.1.4. 古い文字列書式設定方法
# https://docs.python.org/ja/3/tutorial/inputoutput.html#old-string-formatting
# 7. 入力と出力 — Python 3.9.4 ドキュメント - 7.1.2. 文字列の format() メソッド
# https://docs.python.org/ja/3/tutorial/inputoutput.html#the-string-format-method
# 結果 ページ
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
# メッセージを 一回変数 response に入れてるけど、
# やってることは上の detail と同じ。
# 投票 ページ
def vote(request, question_id):
return HttpResponse("You're vooting on question %s." % question_id)
polls.urls モジュール (polls/urls.py) に path()
コールを追加して、ビューと紐付けます。
#(省略)
urlpatterns = [
# ex: /polls/
path('', views.index, name='index'),
# polls/ (pollsのroot) にアクセスすると views.index を呼び出す という意味。
# name は template から url を参照するときに便利らしい。
#
# python、djangoのurls.pyで設定するnameってなんやねん?? - Qiita
# https://qiita.com/sr2460/items/11a1129975913ed584d3
#ex: /polls/5/
path('<int:question_id>/', views.detail, name='detail'),
# <int:question_id> を引数として views.detail に渡す。
# <int:question_id> はパスコンバータ。
# 正規表現の (?P<question_id>[0-9]+) に相当する。
#
# URL ディスパッチャ | Django ドキュメント | Django - Path converters
# https://docs.djangoproject.com/ja/3.2/topics/http/urls/#path-converters
#
# たとえば /polls/5/ にアクセスすると以下のようにviews のメソッドが呼び出される。
# detail(request=<HttpRequest object>, question_id=5)
#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'),
]
開発サーバを立ち上げて、動作を確認します。
% python manage.py runserver
以下のURLにアクセスして、それぞれ数字(34)が反映されたメッセージが表示されればおk。
- http://127.0.0.1:8000/polls/34/
>>> You’re looking at question 34. - http://127.0.0.1:8000/polls/34/results/
>>> You’re looking at the results of question 34. - http://127.0.0.1:8000/polls/34/vote/
>>> You’re vooting on question 34.
4.3. 実際に動作するビューを書く
データベースAPIを利用したビューを作るため、polls/views.py を以下のように Question を読み込み、 index() を書き換えます。
from django.http import HttpResponse
# Question オブジェクトの読み込み(追加)
from .models import Question
# インデックス ページ
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
# Question を order_by(-pub_date) でソートし、前から5つの配列を取得
# マイナスをつけるだけでデータを降順にしてくれる。
# [:5] は配列をスライスする。[0:5]と同じで0番目から5つ取得する意味。
output = ', '.join([q.question_text for q in latest_question_list])
# リスト内包表記 [式 for 任意の変数名 in イテラブルオブジェクト]
# やってることは以下と同じ。
#
# question_array =[]
# for q in latest_question_list:
# question_array.append(q.question_text)
# output = ', '.join(question_array)
#
# 5. データ構造 — Python 3.9.4 ドキュメント - 5.1.3. リストの内包表記
# https://docs.python.org/ja/3/tutorial/datastructures.html#list-comprehensions
return HttpResponse(output)
#(省略)
http://127.0.0.1:8000/polls/ にアクセスすると最新5件の質問をカンマで区切り出力します。
4.3.1. テンプレート
現在の状態ではページデザインを views.py に直接記入しています。これはメンテナンス性が低いので、ページデザインとロジックを切り離すため、テンプレートを作成します。
polls/template/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ディレクトリの構成は以下のような感じです。「+」は今回追加したファイルです。
polls/
__init__.py
admin.py
apps.py
migrations/
+ templates/
+ polls/
+ index.html
__init__.py
models.py
tests.py
urls.py
views.py
あわせて polls/views.py のindex() を以下のように書き換えます。
from django.http import HttpResponse
#テンプレートの読み込み(追加)
from django.template import loader
# Question オブジェクトの読み込み
from .models import Question
# インデックス ページ
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
# Question を order_by(-pub_date) でソートし、前から5つの配列を取得
# マイナスをつけるだけでデータを降順にしてくれる。
# [:5] は配列をスライスする。[0:5]と同じで0番目から5つ取得する意味。
# テンプレートの読み込み
template = loader.get_template('polls/index.html')
# テンプレートで使う変数を設定
context = {
'latest_question_list': latest_question_list,
}
return HttpResponse(template.render(context, request))
#(省略)
http://127.0.0.1:8000/polls/ にアクセスすると詳細ページへのリンクの付いた質問のリストが表示されます。
4.3.2. ショートカット: render()
HttpResponse + template の組合せはよく使われるので、ショートカット render が用意されています。
# ショートカットの読み込み
from django.shortcuts import render
# 最初から入ってるコード。ついに出番がきた。
# view をすべて render を使って書き換えた場合、
# HttpResponse と loader の読み込みが不要になります。
# 現段階ではまだ index 以外で HttpResponse を使っているので残してあります。
from django.http import HttpResponse
#from django.template import loader
# Question オブジェクトの読み込み
from .models import Question
# インデックス ページ
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
# Question を order_by(-pub_date) でソートし、前から5つの配列を取得
# マイナスをつけるだけでデータを降順にしてくれる。
# [:5] は配列をスライスする。[0:5]と同じで0番目から5つ取得する意味。
# テンプレートの読み込み(コメントアウト)
#template = loader.get_template('polls/index.html')
# テンプレートで使う変数を設定
context = {
'latest_question_list': latest_question_list,
}
# HttpResponse を render で置き換え
#return HttpResponse(template.render(context, request))
return render(request, 'polls/index.html', context)
#(省略)
4.4. 404 エラーの送出
リクエストしたIDを持つ質問が存在しない場合は404を返すように polls/views.py に Http404
も読み込み、 detail()
を書き換えます。
from django.http import HttpResponse, Http404 #追加
from django.shortcuts import render
#(省略)
# 詳細 ページ
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
# 主キーはそのカラム名に関わらず pk= で検索できる
except Question.DoseNotExist:
raise Http404('Question does not exist')
return render(request, 'polls/detail.html', {'question': question})
#(省略)
また、テンプレートディレクトリ polls/template/polls/ に detail.html を追加します。
{{ question }}
ここまでで存在しないID http://127.0.0.1:8000/polls/4/ にアクセスすると以下のような画面が表示されます。
4.4.1. ショートカット: get_object_or_404()
404にもショートカットが用意されています。
polls/views.py の import に get_object_or_404
を追加し、 detail を書き換えます。
from django.http import HttpResponse # ショートカット get_object_or_404 読み込みにより Http404 が不要
from django.shortcuts import get_object_or_404, render
#(略)
# 詳細 ページ
def detail(request, question_id):
# try:
# question = Question.objects.get(pk=question_id)
# # 主キーはそのカラム名に関わらず pk= で検索できる
# except Question.DoseNotExist:
# raise Http404('Question does not exist')
# return render(request, 'polls/detail.html', {'question': question})
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
#(省略)
ここまでで存在しないIDにアクセスすると以下のような画面が表示されます。
4.5. テンプレートシステムを使う
polls/detail.html をテンプレート変数を使って書き換えます。
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
http://127.0.0.1:8000/polls/1/ にアクセスすると以下のような画面が表示されます。
4.6. テンプレート内のハードコードされたURLを削除
polls/index.html のaタグを テンプレートタグ url を使って書き換えます。
{% url 'name' param %}
このうち name
は urls.py の path()
で定義されている名前。param
は path()
に渡す引数です。これを使うとテンプレートは以下のように書き換えられます。
<li><a href="{% url 'detail' question.id %}/">{{ question.question_text }}</a></li>
この中の 'detail'
は polls/urls.py の中で name='detail'
と定義されているものです。また、引数の question.id
は <int:question_id>
として渡されます。
path('<int:question_id>/', views.detail, name='detail'),
たとえばこれを以下のように書き換えると詳細ページのURLが polls/specifics/1/ というふうに変わります。また、テンプレートタグ url を使っていればテンプレートを書き換えなくても自動的に新しいURLが適用されます。
path('specifics/<int:question_id>/', views.detail, name='detail'),
4.7. URL 名の名前空間
上の例だと polls/index.html のテンプレートタグ url は polls/urls.py を参照していますが、ほかのアプリに同じ name が存在したり、ほかのアプリを参照したい場合も出てくるかもしれません。そこで明示的に polls アプリを指定できるように polls/urls.py に app_name
を追加します。
#(省略)
app_name = 'polls'
urlpatterns = [
# ex: /polls/
path('', views.index, name='index'),
#(省略)
あわせて、polls/index.html のテンプレートタグ url も name
部分をただの detail
から名前空間つきの polls:detail
に書き換えます。
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>