Python
Django

DjangoでWebサービスを作る - 2.アプリを作ってみる その5

前回の記事「アプリを作ってみる その4」ではあたらしくtemplateが登場し、MVTモデルの役割分担を考えながらpollsアプリをよりdjangoアプリらしいものになってきた。今回はチュートリアル04に沿ってフォーム処理とGeneric Viewを見ていきたい。

記事一覧と開発環境

こちら

アプリを作ってみる その5

初回の手順で仮想環境を立ち上げておく

detailテンプレートに投票用のフォームを追加する
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 %}Cross Site Request Forgery対策をやってくれるdjangoの機能で、サイト内のURLをターゲットとしているPOSTフォームには必ず入れる。外部サイトにPOSTするようなフォームには絶対に使ってはならない。使うとcsrf tokenが流出することとなりセキュリティ上の問題になりかねない。またこの機能を有効にするにはresponseをrenderするのにRequestContextを使わなくてはならない。詳細はdjango documentation - Cross Site Request Forgery protectionを参照。

forloop.counterは名前の通り、for内で何回目のループかを示す整数値。

投票を処理して結果画面に遷移するためのviewを作る
polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from .models import Choice, Question
...
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,)))

detailフォームからpostされた選択肢はrequest.POSTから取得。このようにして明示的にPOSTでしかデータを更新できないような仕組みにする。選択肢が取得で着ないときはエラーメッセージを追加してdetail画面を再表示する。

取得できた場合は該当する選択肢の投票回数をincrementして、保存する。
最後に結果ページにredirectする。POSTのデータを処理したらredirectするのはgood practiceなんだそう。

reverseは要はURLConfsからURLを取得するfunction。これまではURLを受け取ってそこから関連するviewを呼ぶような流れだったが、viewから関連するURLを逆サーチする。だからreverse。

結果画面を作る
polls/views.py
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

viewはいたってシンプル。

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

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

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

結果画面も比較的シンプル。pluralizeは対応するデータが1ではないときに複数形のsを追加する機能。上記の例だとchoice.votesが1の時はvoteと表示、それ以外の時はvotesと表示。

このvotesビューはデータベースからデータを取得し、それに1を足してセーブするような動きとなっている。同時に複数アクセスがあるといわゆるrace conditionの問題が起こるが、これはF()という機能で対応できる。詳細はdjango documentationを要確認だが、これは追って別テーマとして書こうと思う。

Generic Viewを使って書き換える

Generic Viewはごく一般的なWebアプリのviewの建てつけをDjangoが勝手にやってくれる機能。つまり、『受け取ったパラメータをもとにデータベースからデータを取得して、テンプレートに従ってrenderしてページを作る』というのをコードを書かずにやってくれる機能。大部分のビューはこの機能で(あるいはこれをもとに)実装できそう。では早速、以下のステップで書き換えていく。

  1. URLConfを書き換える
  2. いらないviewを消し、Generic Viewを使って新規のviewを作る
URLConfを書き換える
mysite/polls/urls.py
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

voteビュー以外はGeneric Viewで書き換えられている。第二引数のviews.IndexView.as_view()等はこれからviews.pyに作成する。

いらないviewを消し、Generic Viewを使って新規のviewを作る
mysite/polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question

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

   def get_queryset(self):
        """Return the last five published questions."""
        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'
...

Generic Viewはdjango.views.genericに含まれるクラスのサブクラスとして実装される。DetailViewはパラメータとしてpkを受ける前提で、最低限modelを設定するだけでできてしまう。ListViewはリストを返すget_querysetを実装する必要がある。
template_nameは任意。指定しないと<app name>/<model name>_detail.htmlを見に行く。
同様にListViewのcontext_object_nameも任意で、デフォルトは<model name>_listになる。チュートリアルにも書かれているが、このあたりは無理にデフォルトに合わせてtemplateを作るより、view側で指定するほうが間違いが少ないと思う。

Djangoアプリの基本的な構造は今回までで一応終わり。次回以降はテスト、スタイルシートの利用、そのたもろもろカスタマイズする方法をさらっと見ていき、チュートリアルをとりあえずおわらせたい。

参考文献

[1]Writing your first Django app, part 4