概要
前回の続きです。
今回は、Djangoによるフォーム処理とコードの縮小化を中心に解説します。
フォームの作成
前回のチュートリアルで作成したpolls/detail.htmlを更新して、HTMLの<form>要素を入れましょう。
<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の修正になります。
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ページを作成します。
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を追加します。
内容を非常にシンプルです。
続いてテンプレートを作成します。
<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>
<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の設定を変換します。
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の修正を行います。
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で表は作るので、意味ないかも。。。)
次回は、作成した投票アプリを使って自動テストを導入してみたいと思います。