Help us understand the problem. What is going on with this article?

Djangoを始めよう! 〜チュートリアル④〜

More than 3 years have passed since last update.

概要

前回の続きです。
今回は、Djangoによるフォーム処理とコードの縮小化を中心に解説します。

フォームの作成

前回のチュートリアルで作成したpolls/detail.htmlを更新して、HTMLの<form>要素を入れましょう。

polls/templates/polls/detail.html
<h1>{{question.question_text}}</h1>

{% if error_message %}
    <p>
        <strong>{{error_message}}</strong>
    </p>
{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
    {% csrf_token %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{forloop.counter}}" value="{{choice.id}}" />
        <label for="choice{{forloop.counter}}">{{choice.choice_text}}</label><br />
    {% endfor %}
    <input type="submit" value="Vote" />
</form>

ポイントは以下です。

  • csrf_tokenはDjangoが用意してくれているクロスサイトリクエスト対策です。
  • forloop.counterはDjangoのforループ内でカウント数を表します。
  • あとは、Web開発の王道通りで、submitすると選択されたnameとvalue値が指定のaction先に送信されます。

次に送信先であるpolls/voteを修正していきましょう。
viewの修正になります。

polls/views.py
from django.shortcuts import render, get_object_or_404
from .models import Question, Choice  # Choiceモデルを追加
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse  # reverseを追加


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)
    context = {
        'question': question
    }
    return render(request, 'polls/detail.html', context)


def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

# 今回修正したviewAction
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice"
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()

        return HttpResponseRedirect(
            reverse('polls:results', args=(question.id))
        )

voteアクションを修正しました。
これまでの解説に出てきていない部分を解説します。

request.POST

アクションの第1引数には、これまでもrequestを指定してきました。
やっとここで使う時がやってきます。

request.POSTで送信してきたPOSTデータにアクセスできる辞書型の様なオブジェクトです。
今回の場合は、questionオブジェクトに紐付くchoiceをchoice_set.getし、getのfilterとしてpkを指定しています。

その時に、request.POST['choice']としていすればPOSTデータのchoiceにアクセスできるといった流れです。
(choiceはname属性のchoice)

同様にrequest.GETも存在しますが、今回はmethod=POSTでPOSTデータを送っていますので、POSTで取得します。

POSTのchoiceがなければ、KeyErrorを送出します。
上記のコードでは、KeyErrorをチェックし、choiceがない場合は、エラーメッセージ付きの質問フォームを再表示します。

POSTデータ処理が成功した後

これはDjangoに限った事ではありませんが、Web開発のベストプラクティスとしてPOSTデータの処理に成功した後は、リダイレクトで目的ページへ遷移させます。

今回であれば、Djangoが提供してくれている、HttpResponseRedirectを使って、resultに遷移させています。

HttpResponseRedirectの中では、reverse関数を利用します。

reverse関数の第1引数では、URLconfのパターンを指定します。
第2引数では、遷移に必要なパラメータを指定します。

結果ページの作成

最後にresultページを作成します。

polls/views/py
from django.shortcuts import render, get_object_or_404
from .models import Question, Choice
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse


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)
    context = {
        'question': question
    }
    return render(request, 'polls/detail.html', context)

# 今回追加したView
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {
        'question': question
    }
    return render(request, 'polls/results.html', context)


def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice"
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()

        return HttpResponseRedirect(
            reverse('polls:results', args=(question.id,))
        )

まずはViewを追加します。
内容を非常にシンプルです。

続いてテンプレートを作成します。

polls/templates/polls/results.html
<h1>{{question.question_text}}</h1>

<ul>
    {% for choice in question.choice_set.all %}
        <li>{{choice.choice_text}}&nbsp;--&nbsp;{{choice.votes}}&nbsp;vote{{choice.votes|pluralize}}</li>
    {% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>
<br />
<a href="{% url 'polls:index' %}">To TOP</a>

choiceの分だけループを回して、投票の結果を表示しています。

これで一通りのアプリの機能は完成しました。

汎用ビューを使う

Pythonの概念の1つに「少ないコードはいいことだ」という概念があります。
Djangoも勿論この考え方を採用しています。

今まで作ったviewに少し手をいれて、よりシンプルなviewを作成してみましょう。

今までで開発してきたviewはThe Web開発というやり方でした。

URLを介して渡されたパラメータに従ってデータベースからデータを取り出す。

テンプレートをロードする。

レンダリングしてテンプレートを返す。

上記の処理は極めて一般的な処理の為、Djangoでは汎用ビュー(generic view)としてショートカットが存在します。

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

一般的なビューを汎用ビューにするのは、以下のステップです。

① URLconfを変換する。
② 古い不要なビューを削除する。
③ 新しいビューにDjangoの汎用ビューを設定する。

まずは、URLconfの設定を変換します。

polls/urls.py
from django.conf.urls import url

from . import views

app_name = 'polls'

urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),  # IndexViewクラスを介する様に修正
    url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),  # DetailViewクラスを介する様に修正
    url(
        r'^(?P<pk>[0-9]+)/results/$',
        views.ResultsView.as_view(),
        name='results'
    ),   # DetailViewクラスを介する様に修正
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]

2つ目と3つ目の正規表現でマッチしたパターンの名前は、に変更しています。

次にViewの修正を行います。

polls/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic

from .models import Question, Choice


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):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice"
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()

        return HttpResponseRedirect(
            reverse('polls:results', args=(question.id,))
        )

ここでは、テンプレートを表示する為のViewをクラス化しています。
クラスの親は新規にimportしたgeneric(汎用)の子クラスを継承しています。

generic.DetailView

詳細ページ用のビューです。

各ビューは自分がどのモデルに対して動作するのかを知っておく必要があります。これはmodelプロティとして定義しています。

また、DetailView汎用ビューには、 pkという名前でURLからプライマリーキーをキャプチャして渡すことになっています。
先ほどURLconfでpkに修正したのはその為です。

DetailView汎用ビューはデフォルトで<appName>/<modelName>_detail.htmlというテンプレートを参照する様になっています。

今回は独自のテンプレート名なので、template_nameプロパティでテンプレートを明示的に知らせる必要があります。
(もし命名規則に従って作成している場合は、template_nameは必須ではありません。)

また、コンテキスト変数はモデル名を利用します。

generic.ListView

ListViewのデフォルトテンプレートの命名規則は、<appName>/<modelName>_list.htmlになります。

今回は独自テンプレートなので、template_nameプロパティを利用します。

また、デフォルトでの取得したデータ一覧は、<modelName>_listというコンテキスト変数に格納されます。

今回はアプリ全体で、latest_question_listというコンテキスト変数を利用しています。

その為、context_objext_nameプロパティでコンテキスト変数の名前を上書きしています。

最後に、コンテキスト変数に値を格納する、get_queryset()メソッドを定義します。

まとめ

今回は、

  • フォームの作成
  • 汎用ビューの利用

を解説しました。

フォームの作成は他の言語のフレームワークとさして変わりませんでしたが、汎用ビューは少し癖がありました。

覚えると、チュートリアルにある通りほとんどコードを書かずにURLからビューを選択して、ビューからテンプレートを選択して、レンダリングして、HttpResponseとして返すという処理が出来てしまいました。

慣れる必要はありますが、覚えておいて損はなさそうです。
(最終的にはReactで表は作るので、意味ないかも。。。)

次回は、作成した投票アプリを使って自動テストを導入してみたいと思います。

GitHub

シリーズ

tfrcm
React / ReactNative / Go / TypeScript / AWS / Docker / k8s
https://gemcook.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした