はじめに
この記事はDjangoの公式チュートリアルを進めていくシリーズものです。
今回は4記事目「はじめての Django アプリ作成、その 4」を進めていきます。
初心者の初心者による初心者のためのDjangoチュートリアルまとめ①
初心者の初心者による初心者のためのDjangoチュートリアルまとめ②
初心者の初心者による初心者のためのDjangoチュートリアルまとめ③
はじめての Django アプリ作成、その 4
Write a minimal form
前回の記事で作成したdetail.html
内に投票のためのフォームを設置します。
<h1>{{ question.question_text }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<formaction="{% url 'polls:vote' question.id %}"method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
<inputtype="radio"name="choice"id="choice{{ forloop.counter }}"value="{{ choice.id }}"><labelfor="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<inputtype="submit"value="Vote"></form>
forloop.counter
は、 for タグのループが何度実行されたかを表す値です。
内容を見るとQuestionに関連付くchoice全てがfor文でradioボタンの選択肢として表示されることがわかります。
CSRF対策にPOSTのformを作成する際は{% csrf_token %}
を挿入する必要があります。
次にvoteがしっかり機能するようにpolls/views.py
を編集します。
fromdjango.httpimportHttpResponse,HttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefrom.modelsimportChoice,Question# ...
defvote(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):# Redisplay the question voting form.
returnrender(request,'polls/detail.html',{'question':question,'error_message':"You didn't select a choice.",})else:selected_choice.votes+=1selected_choice.save()# Always return an HttpResponseRedirect after successfully dealing
# with POST data. This prevents data from being posted twice if a
# user hits the Back button.
returnHttpResponseRedirect(reverse('polls:results',args=(question.id,)))
request.POST['choice']
により上記のFormの'choice'のnameでPOSTされた値をselected_choice
として読み取っています。
request.POST['choice']
が存在しないとき(すなわちユーザーが何も選択しなかった場合)、KeyErrorが送出されます。
その場合はエラーメッセージを添えてもう一度formがrenderされます。
処理が正常に終了するとresultsにredirectします。
(POSTなどの処理が正常に終了した際はredirectをかけるべきです(Djangoにかかわらず))
reverse
関数でURLのハードコートを防ぐことができます。
使用方法はindex.html
で使用したURLの指定と似ています。
ではresultsのviewをpolls/views.py
に追記します。
fromdjango.shortcutsimportget_object_or_404,renderdefresults(request,question_id):question=get_object_or_404(Question,pk=question_id)returnrender(request,'polls/results.html',{'question':question})
内容はdetail
のviewとほぼほぼ同じになっています。
Templateも作成しましょう。
<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><ahref="{% url 'polls:detail' question.id %}">Vote again?</a>
ここまでで一つ問題点があることに気づいたでしょうか
現在の処理の流れでは最初にselected_choice
をDBから取得し、それのvotesから新しい値を計算してDBに保存します。
votesの値がもともと42だったとして、AさんとBさんが同時に投票すると本来では44にならなくてはいけないところが43になってしまいます。
こういう問題は「競合問題」と呼ばれるそうです、解決のためのF()
の説明は以下の記事が参考になります。
JavaおじさんがPythonを使えるようになるまでの全記録(その16)
汎用ビューを使う: コードが少ないのはいいことだ
Djangoには凡用View(generic view)というショートカットが存在します
URL を介して渡されたパラメータに従ってデータベースからデータを取り出し、テンプレートをロードして、レンダリングしたテンプレートを返します。これはきわめてよくあることなので、 Django では、汎用ビュー(generic view)というショートカットを提供しています。
汎用ビューとは、よくあるパターンを抽象化して、 Python コードすら書かずにアプリケーションを書き上げられる状態にしたものです。
とのことです。
URLconf の修正
polls/urls.py
を以下のように変更します
fromdjango.urlsimportpathfrom.importviewsapp_name='polls'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'),]
違いはpathの指定するviewにviews.IndexView.as_view()
などを渡している点です。
また、question_id
がpk
として名称変更されています。(理由は後述)
view内でIndexView
クラスなどを作成します。
fromdjango.httpimportHttpResponseRedirectfromdjango.shortcutsimportget_object_or_404,renderfromdjango.urlsimportreversefromdjango.viewsimportgenericfrom.modelsimportChoice,QuestionclassIndexView(generic.ListView):template_name='polls/index.html'context_object_name='latest_question_list'defget_queryset(self):"""Return the last five published questions."""returnQuestion.objects.order_by('-pub_date')[:5]classDetailView(generic.DetailView):model=Questiontemplate_name='polls/detail.html'classResultsView(generic.DetailView):model=Questiontemplate_name='polls/results.html'defvote(request,question_id):...# same as above, no changes needed.
ListView
とDetailView
が使用されています。question_id
がpk
として名称変更されていたのはDetailView
でそのように規定されているためです。
もともとデフォルトでは、 DetailView
汎用ビューは /_detail.html という名前のテンプレートを使いますが、template_name
属性を指定することで使用するテンプレートを変更できます。(ListView
も同様です)