Djangoチュートリアルを意訳しながら進める~その3~

2018年12月24日

さて、続き。前回はこれ。
Djangoチュートリアルを意訳しながら進める~その2~
今回は公式チュートリアルだと以下の部分。
https://docs.djangoproject.com/en/2.1/intro/tutorial03/
投票システムを作っていく。今回は公開するページを作る。viewという機能を使う。

概要

viewっていうのは目に見える部分。例えば一般的なブログはこんなviewを持っている。

・ブログホームページ・・・最新の記事を数本表示する
・記事詳細ページ・・・記事一本ごとのページ
・1年のアーカイブページ・・・1年分の記事がまとめられたページ

等々。

今回作っている投票アプリでは、こんなviewが必要。

・質問一覧ページ・・・最新の質問をいくつか表示するページ
・質問詳細ページ・・・質問の詳細と投票フォームのページ
・質問結果ページ・・・特定の質問の結果を表示するページ
・投票アクション・・・投票の一連の流れを実行する機能

それぞれのviewはPythonのメソッド(method)かファンクション(function)で作る。DjangoはviewをリクエストされたURLから判断して選ぶ。
あと、DjangoはエレガントなURLパターンを使うことが可能だ。
たとえばこんな感じのURLはダサい↓
“ME2/Sites/dirmod.asp?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B”
Djangoはこんな感じにエレガントにできる。
“/newsarchive/<year>/<month>/”といった感じに。
リクエストされたURLから適切なviewを判断するとき、Djangoは’URLconfs’を使う。

viewを書いていく

polls/views.pyに書き足していこう。こんな感じに。

# question_idは次に編集するurls.pyのとこで出てくる。%sはquestion_idを参照している
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)
# 上のdetailの書き方を少し変えただけ。文章をresponseに代入している
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)

その後は新しく作ったviewをpolls/urls.pyに追加していく。

from django.urls import path

from . import views

urlpatterns = [
    # /polls/にアクセスしたらviews.pyのindexを実行
    path('', views.index, name='index'),
    # 例えば/polls/5/にアクセスしたらviews.pyのdetailを実行。ここのint:question_idは整数が入力されたら、それをquestion_idとしてvies.detailに渡しますよってとこか。
    path('<int:question_id>/', views.detail, name='detail'),
    # 例えば/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を実行して、今の変更を確かめてみる。
アドレスバーに“http://localhost:8000/polls/34/”と打ち込んでエンターを押すと、detail()メソッドが実行される。
んで、下記のような画面が出るだろう。

次に
http://localhost:8000/polls/34/results/
にアクセスするとこんな感じ。

http://localhost:8000/polls/34/vote/ならこう。

今あげたアドレスにアクセスがあった場合、まずmysite/urls.pyが参照される。
その中にはpolls/にアクセスがあったら、polls/urls.pyを参照するように書いてある。
んで、polls/34/にアクセスがあった場合は、detail()がこんな引数で実行される。

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

question_id=34の部分はpolls/url.pyで設定した<int:question_id>/に対応している。この<>で囲った部分は、入力された数値をviewにquestion_idとして渡すことを示している。

なんかするviewを書いてみる

viewがすることは、リクエストを受けてHttpResponseオブジェクトを返すか、Http404を返すこと。
viewではデータベースからデータを読み出すことができる。そこで、テンプレート機能を使うことが出来る。別にテンプレート機能を使わなくてもいい。PDFやZIPファイルを返すことも出来る。
チュートリアルの二回目で使ったDjangoのデータベースAPIをここで使おう。polls/views.pyのindexに次の文を書き込もう。
これは最新の5つの質問をカンマで区切って表示するプログラムだ。

def index(request):
	# Questionからpub_dateを見て最新のを5つ選んでデータを引っ張ってくる
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
	# 5つのデータをカンマで結合する
    output = ', '.join([q.question_text for q in latest_question_list])
	# レスポンスを返す
    return HttpResponse(output)

これで一応、最新の5つの質問がブラウザに表示されるようになった。
しかし、見た目が最悪だ。
いつもの通り実行するとこんな画面になる。

見た目を変えるにはどうすればいいだろうか。このviews.pyの中にhtmlを書いていくことも出来る・・・が、もっと簡単な方法がある。
テンプレート、templatesを使う。
さて、まずpollsのフォルダの中にtemplatesと名付けたフォルダを作ろう。
んで、ややこしいんだが、その中にpollsと言うフォルダをさらに作り、その中にindex.htmlファイルを作る。
つまりpolls/templates/polls/index.htmlといった構成になる。
なんでこんなことをするかと言うと、例えばviews.pyからindexのテンプレート(index.htmlのこと)
を求められたとき、Djangoはtemplatesと付けられたフォルダ全部から、探してしまう。
今はpollsしかアプリケーションが無いので、templatesの中にpollsフォルダを作らなくても動くだろうが、いくつもアプリケーションが増え、それぞれがindex.htmlをもっていた場合、Djangoはどのテンプレートを探せばよいかわからなくなってしまう。

さて、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.pyにindexを次の内容に書き換える。
importするライブラリも増えているので注意。

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を呼んで、そこにcontextを渡している。
さあ、またまたmanage.pyを使ってサーバーを稼働させてみよう。
/polls/にアクセスすると、こんなかんじで、What’upにリンクが貼られた感じになっている。

リンクをクリックするとこんな感じ。

テンプレートを使うときは、上でやったようにテンプレートをいったん読み込んで、
それをHttpResponseオブジェクトとしてcontext, requestと一緒にして返すのがとやるのが一般的である。
Djangoではもっと簡単に書くためにrender()というショートカットを用意している。
index()を以下のように書き直そう。

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)

これだとloaderとHttpResponseを使わなくていい。
render()は引数として最初にrequestオブジェクトを取る。二つ目にテンプレートの名前、三つ目に内容(context)を辞書形式の変数でとる。そして、render()はcontextの内容を含んだテンプレートをHttpResponseオブジェクトとして返す。

404エラーを返すこと

さて、お次はquestion detail viewを書いていく。このページは投票に当たっての質問文を表示する。以下の通り書いていく。

# Http404というのをimport
from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
	# tryしてエラーを返すようであればeceptの文を実行する例のやつ
    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が無かった時に404エラーを返すもの。
さて、detailのテンプレートだが、とりあえず次の感じで用意をしておこう。
polls/templates/polls/の中に
detail.htmlを作って次の文を入れる。

{{ question }}

とりまこれで確認できるようになった。
さて、Http404を返す前はこんな感じで、実際にないデータを要求しても画面が表示されていた。

今回の変更後は、存在しないデータを要求すると、こんな画面になることが確認できるはず。

ちなみにコマンドラインの画面でサーバーの様子を確認できるが、こちらでも404エラーを返している様子が分かる↓

Not Found: /polls/50/
[22/Dec/2018 10:57:49] "GET /polls/50/ HTTP/1.1" 404 1716

ショートカットget_object_or_404()

上で使ったように、求められたデータが存在しない時にはget()とraise Http404が良く使われる。
Djangoでは、より簡単に記述できるショートカットを用意している。detail()のviewを次のように変更しよう。

# 今回はさらにget_object_or_404もimportする
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
	# さっきのtry exceptと同じことをやってくれる
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

get_object_or_404は一番目の引数にmodelを指定する必要がる。二番目は検索するキーワードを入れる。
結果は最初にdetailで実装したのと同じような処理が行われる。
ここで、わざわざget_object_or_404をimportしなくても勝手にDjangoでやってくれればいいのに・・・・と思ったりする。しかし、Djangoの設計哲学として、各レイヤー間を疎結合に保つというのがある。404エラーの処理をDjango側で勝手にやるとなると、modelとviewのレイヤーの結合が密になってしまうので、こういう仕様になっている。

さて、get_object_or_404()と同様のものにget_list_or_404()がある。これはget()ではなくてfilter()を使うもので、リストが空の時に404エラーを返す。

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

detail()のviewに戻ろう。
questionの内容を考えると、テンプレートpolls/detail.htmlの内容はこんな感じになる。

<h1>{{ question.question_text }}</h1>
<ul>
# questionに紐づいているchoiceを全部引っ張ってくるために_set.allを使う。
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

テンプレートの中に書いてあるURLを削除するには

思い出せ。我々がquestionへのリンクに関して言ったことを。リンクは下記のように部分的にハードコードされていた。(直訳)
polls/index.htmlを見てみよう。以下の文が見つかるハズ。

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

ここでいうハードコードとはhref=”/polls/{{ question.id }}/”の部分だ。
URLをハードコードすると、いくつものテンプレートを扱うときは大変だ。URLが変わった時に全てのテンプレートを修正する必要がある。良くない。これが密結合というやつだ。
ここで、path()の中でnameを設定したことを思い出してほしい。polls/urls.pyを見返してみてくれ。こんな風にnameを指定していたハズ。

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

polls/index.htmlのさっきの箇所を以下のように修正してみよう。

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

他にも、あなたがdetailのURLを変えたいとき、たとえばpolls/specifics/12/にしたいときは、こんなかんじでpolls/urls.pyを変更するだけでOK

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

くどいようだけど、このname機能が無かったらどうだろうか。わざわざテンプレート一つ一つを全て直していかないといけない。

URLの名前空間(Namespace)

このチュートリアルではpollsという一つのアプリしか扱わない。
現実のプロジェクトでは数個、数十個、恒河沙個のアプリを扱う必要もあるだろう。
そのなかで、どのようにDjangoはアプリごとのURLを見分けるのだろうか。例えば、今回のpollsではdetailというページがあるが、同じプロジェクトでblogというアプリを作って、その中でもdetailというページをつくるかもしれない。先ほどのようにテンプレートの中でnameを使ってURLを作るとき、どうすればよいだろうか。
そのようなときは、まずpolls/url.pyにapp_nameをプラスする。
ちなみに、この後のチュートリアルでもこの設定で進める前提になっているので、みんな下記のようにしておこう。

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に次の変更を加える。いままでdetailだったのがpolls:detailになった。
逆に、polls/url.pyでapp_nameを設定した状態でpolls:detailでは無く今までのdetailだけの表記を使うとエラーになるので注意。

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

次に続く。