nikkie-ftnextの日記

イベントレポートや読書メモを発信

Django Girls Tutorial修了者が投票アプリチュートリアルでDjangoの理解を深めました(その3, 4編)

この記事は、Django Advent Calendar 2018 5日目の記事です。

Django公式チュートリアル(投票アプリ作成)その3・その4に取り組んで得た気づきをアウトプットします。

はじめに

いつも心は虹色に! nikkieです。
Django公式ドキュメントのチュートリアル(投票アプリ)で手を動かした後のアウトプットが眠っていたので、アドベントカレンダーを機に消化することにしました。
その3(ビューとテンプレート)とその4(汎用ビューに書き直し)について扱います。
(この記事は、その5以降を進めるための足掛かりにする目的もあります。)

nikkieについて

アドベントカレンダーのリンクから来られた方向けに簡単に自己紹介します。

  • Pythonは始めて1年過ぎたくらい(執筆時点)
  • 趣味の開発や業務でWeb周りを触ってきた(Flask, Django
  • 2018年5月〜Django Girls Tutorialの翻訳・レビュー、2018年10月〜コーチ

チュートリアルの概要

はじめての Django アプリ作成、その 1 | Django documentation | Django から始まる7回構成のチュートリアルです。
作るアプリの名前をとって「投票アプリチュートリアル」と呼ぶことにします。
なお、私のDjangoレベルは「Django Girls Tutorial修了」です。
Django Girls Tutorialを終えてから投票アプリチュートリアルに取り組んだことで、Djangoへの理解が深まったと感じています。
また、このアウトプットでDjango Girls Tutorialと投票アプリチュートリアルを少しでも繋ぐことができれば嬉しいです。

その1・2についてのアウトプットはこちらです:
前回同様に今回も長くなってしまったので、拾い読みも推奨します。

動作環境

その3:ビューとテンプレート

はじめての Django アプリ作成、その 3 | Django documentation | Django

その3ではURLconf、テンプレート、HttpResponse(とショートカット)について、理解が深まりました。

URLconf

URLパターンは、URLを単に一般化したものです。
(中略)URLconf はURLパターンをビューにマッピングします。

プロジェクト(mysite)のsettings.pyには

ROOT_URLCONF = 'mysite.urls'

と指定されています。
これにより、mysite/urls.pyがロードされるのだと気づきました。

テンプレート

ビューにページのデザインがハードコード1されている問題を解決するために、テンプレートが導入されました。2

プロジェクト(mysite)のsettings.pyには

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True, # ここがポイント
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

と指定されています。
この指定により、INSTALLED_APPSのそれぞれのtemplatesディレクトリがテンプレートの検索対象になるそうです。

polls/templates/index.htmlではなく、polls/templates/polls/index.htmlと配置する理由は、テンプレートに名前空間を与えるためと知り、なるほどと思いました。

HttpResponse

Django Girls Tutorialでは意識してこなかったのですが、ビューはHttpResponseが肝と認識しました。

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

先のハードコードの例で、テンプレートなしでもHttpResponseを返せばいいことが分かります。
テンプレートを返す場合は、以下の2つが必要です。

  1. loader.get_template()でテンプレートを取得する
  2. テンプレートに渡す変数を辞書で定義する(コンテキストと呼ばれる)

テンプレートの返却はHttpResponse(template.render(context, request))のようになります。

これはrender()を使って簡潔に書くことができます。
Django Girls Tutorialで見かけたrender()3が裏でやっていることはHttpResponseを返すことなのですね。

▼render()を使った質問一覧画面:
f:id:nikkie-ftnext:20181203234416p:plain:w400

モデルの取得についても複数手順からなるので、ショートカットget_object_or_404()が用意されています。
裏でやっていることは、

  1. getでオブジェクトを取得してみる
  2. オブジェクトが存在しない場合、Http404エラーを上げる
  3. オブジェクトが存在する場合、取得したオブジェクトをテンプレートに渡して返却(render()関数)

です。

▼質問詳細画面:
f:id:nikkie-ftnext:20181203234552p:plain:w400
▼質問詳細画面で404エラーが返るケース:
f:id:nikkie-ftnext:20181203234606p:plain

(再度)テンプレート

question.question_textのようなドットでつなげた構文にも検索順序があることが分かりました。

  1. 辞書のキーとして検索
  2. 属性として検索
  3. リストインデックスとして検索

今度はリンクがハードコードされています。
これは{% url %}を使って解決します。
このタグで使われるのは、path()のname引数です。4
f:id:nikkie-ftnext:20181204213336p:plain:w400

index.html{% url 'detail' question.id %}を使うことで、polls/urls.pypath('specifics/<int:question_id>/', views.detail, name='detail'),と変更しても、テンプレートは変更不要となりました5
f:id:nikkie-ftnext:20181204213248p:plain:w400

(再度)URLconf

テンプレートに名前空間がありましたが、URLconfにも名前空間が登場します。
それがpolls/urls.pyapp_nameです。 (app_name = 'polls'
名前空間の指定後は、index.htmlのテンプレートタグは{% url 'polls:detail' question.id %}となります。

その3についての疑問点

Djangoが採用している「カップリング」という概念は何を意味するのか?

Django の最も大きな目標の一つは、ルーズカップリングの維持にあります。

その4:汎用ビューに書き直し

はじめての Django アプリ作成、その 4 | Django documentation | Django

その4ではフォームを扱った後に、その3以降で追加した部分を汎用ビューで書き直します。
コードの内容が非常に簡単になって、「処理を全然書いてないけど、いいの?」と衝撃を受けました。

フォーム

残っている投票機能を実装します。
フォームを使った処理のポイント(Djangoに限らない)を学びました。

  • サーバのデータを更新するフォームには、 method="post"を使う
  • 自サイト内をURLに指定したフォームは、クロスサイトリクエストフォージェリ対策をする(Djangoでは{% csrf_token %}
  • POSTされたデータの扱いが成功した後は常に HttpResponseRedirect を返す(戻るボタン押下で再度データが送られることを防ぐため)

polls/views.pyのコードについて

  • try except else 参考:https://docs.python.jp/3/tutorial/errors.html#handling-exceptions

    else 節を設ける場合、全ての except 節よりも後ろに置かなければなりません。 else 節は try 節で全く例外が送出されなかったときに実行されるコードを書くのに役立ちます。

  • selected_choiceにget_object_or_404を使わないのは、エラー時にHttp404ではなくquestionを使ってrenderをしているからと考えた

  • URLをreverseで構築している
  • vote{{ choice.votes|pluralize }}でchoice.votesの値に応じて複数形のsの出し分けをしている

ここまでで投票と結果表示が実装されました。
▼投票画面:
f:id:nikkie-ftnext:20181204213558p:plain:w400
▼結果表示画面:
f:id:nikkie-ftnext:20181204213622p:plain:w400
▼選択肢を選んでいないときのエラー表示:
f:id:nikkie-ftnext:20181204213651p:plain:w400

汎用ビュー

一通り実装したコードを汎用ビューで書き直しました。6

汎用ビューとは、よくあるパターンを抽象化して、 Python コードすら書かずにアプリケーションを書き上げられる状態にしたものです。

手順

  1. URLconfのアップデート(汎用ビューを使う上での命名規則を適用)
  2. 古いビューの削除(index, detail, results)
  3. 汎用ビューで実装(ListViewとDetailView)

polls/views.pyの変更が劇的すぎます!

polls/views.py(Before)

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse

from .models import Choice
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)

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

def vote(request, question_id):
    # 変更がないため省略

polls/views.py(After)

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.views import generic # 追加

from .models import Choice
from .models import Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    # 変更がないため省略

汎用ビューにはmodeltemplate_nameを指定しているだけです。
質問一覧画面については、コンテキスト変数の指定や、質問の取得方法の指定もしています。

終わりに

今回はここまでです。
次回、その5(テスト)からのアウトプットでお会いしましょう。
最後までお読みいただき、ありがとうございました。


  1. PyConJP 2018 の懇親会でSwall0wTech さんから聞いたハードウェアの話を思い出しました。ハードコードしたソフトウェアはハードウェアだそうです(ソースはClean Architectureと記憶)

  2. この部分、リスト内包表記をさらっと使って、output = ', '.join([q.question_text for q in latest_question_list])となっています。このあたりDjango Girls Tutorialとのギャップを感じるところです。

  3. render()Djangoビュー · Django Girls Tutorial に登場します。

  4. 次の箇所の理解が深まりました:https://docs.djangoproject.com/ja/2.1/intro/tutorial01/#path-argument-name

  5. リンク先が<a href="/polls/specifics/1/">What's up?</a>のように自動で生成されています

  6. 汎用ビューもショートカットと呼ばれるようです。render()と同じ呼ばれ方ですね。ショートカットは便利ですが、「使わずに書くとどう動くか」を知っていると、より使いこなせるんじゃないかと思います。