フォームの配置
このページはDjangoのDocumentation、Writing your first Django app, part 4に対応している。
detailページの機能
チュートリアルのテンプレートのところで作成したindexページでは、データベースのquestionテーブルに保存されたQuestionデータの一覧が表示される。
各Questionデータのリンクをクリックすると、polls.viewsモジュールのdetail関数にルーティングされ、detailテンプレートが呼ばれてページが表示される。
現状でこのページでは、"You're looking at question 1."のようにクリックしたQuestionデータのidを含むテキストが表示されるだけ。
ここでdetailページに以下のような機能を持たせる。
- クリックした
Questionデータのquestion_textを表示する - その
Questionデータに関連付けられた(すなわちその回答選択肢となる)Choiceデータ群を、ラジオボタンとともに表示する Voteボタンを押すと、選択した回答のidが送信される
id送信後の振る舞いについては後に実装することとして、ここでは上の機能をテンプレートで実装していく。
コードの変更
pollsアプリケーションのdetailテンプレートを以下の内容に変更する。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<form action="{% url 'polls:vote' question.id %}" method="post"> {% csrf_token %} <fieldset> <legend><h1>{{ question.question_text }}</h1></legend> {% if error_message %} <p><strong>{{ error_message }}</strong></p> {% endif %} {% 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> {% endfor %} </fieldset> <input type="submit" value="Vote"> </form> |
テンプレートがビューから受け取る変数
questionオブジェクトとerror_message(テキスト)が渡されることを想定している(上記コードのL4, 6, 8)。
form要素のactionとmethod
form要素のオプション設定は以下の通り。
actionの呼び出し先はpollsアプリケーションのname='vote'ルート→vote関数で、questionデータのidをURLパラメーターで渡す- メソッドは
POST
csrf_tokenタグ
form要素内の最初にCSRF/XSRF (cross-site request forgery)対策として{% csrf_token %}を置いている。form要素にこのタグを置かないと、Djangoによりアクセス禁止のエラーとなる。
qustion_textの表示
ビューから受け取ったquestionオブジェクトのquestion_textプロパティーの内容をh1要素として表示させる。
エラー表示
ビューの処理でエラーになった場合error_message変数が定義されることを前提としている。DTLのifタグにより、error_messageがセットされているときはこれが表示され、選択肢とラジオボタンが表示される。
選択肢の表示
questionデータに対する選択肢はChoiceデータが関連付けられていて、そのコレクションはDTLのドット検索によりquestion.choice_set.allで得られる。
フォームでは、このコレクションの要素(各Choiceデータ)をDTLのforタグで1つずつ取出している。そして、各choiceについてラジオボタンとラベルテキストを生成している。
ラジオボタンの属性は以下のとおり。
- name=”choice”
- すべての選択肢に同じ
name属性を適用してグループ化している。 - id=”choice{{ forloop.counter }}”
label要素で参照するidで、先頭から連番でchoice1,choice2, …のような値となる。forloop.counterはDTLのforタグの変数で、ループカウンターの値が得られる。- value=”{{ choice.id }}”
- 選択されたラジオボタンに対応する値で、
Choiceデータのidが充てられる。このidによって、データベース上の指定したデータが得られる。
このテンプレートがレンダリングされたときのHTMLは以下のとおり。valueの値が1, 2, 4となっているが、データの削除・追加によってAUTO_INCREMENTの値は必ずしも連続にならない。一方、forloop.counterを利用したidの数字部分は連番となっている。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<form action="/polls/1/vote/" method="post"> <input type="hidden" name="csrfmiddlewaretoken" value="AkC8p5sJzHblsYxvYtn124PmXPt9u9SC8qJqa6QMinL8MzCuiMsMmAhrCfHuR1YZ"> <fieldset> <legend><h1>What's up?</h1></legend> <input type="radio" name="choice" id="choice1" value="1"> <label for="choice1">Not much</label> <input type="radio" name="choice" id="choice2" value="2"> <label for="choice2">The sky</label> <input type="radio" name="choice" id="choice3" value="4"> <label for="choice3">Just hacking again</label> </fieldset> <input type="submit" value="Vote"> </form> |
ここでindexページのQuestionデータのうち"What's up?"をクリックすると、レンダリングされたdetail.htmlにより以下のページが表示される。

この段階でVoteボタンを押すと、polls.viewsモジュールのvote関数にルーティングされるが、この段階では”You’re voting on question 1.”とだけ表示される。
|
1 2 |
def vote(request, question_id): return HttpResponse("You're voting on question %s." % question_id) |
vote関数の実装
vote関数での処理
ここではフォームのアクション先であるvote関数の処理を実装する。処理内容は、Questionの持つ選択肢のうちditailページで選択・送信されたChoiceデータのvoteカウンターを1つ増やしてデータベースを変更するというもの。
コード
vote関数の内容を以下のように変更する。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, get_object_or_404 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() # POSTデータが正常に処理された場合、HttpResponseRedirectを返す。こにより # ユーザーが戻るボタンを押しても繰り返してデータが投稿されるのを防ぐ return HttpResponseRedirect(reverse('polls:results', args=(question_id,))) |
モジュールのインポート
パッケージ先頭でHttpResponseRedirectモジュールをインポートしている。このモジュールは、URLconfで定義したルート名とパラメーターを与えてレスポンスを返す。
またChoiceモデルを扱うために、Questionに加えてChoiceモジュールもインポートしている。
Questionの取得
フォームのaction属性で、Questionデータのidをクエリーパラメーターで渡すようになっていて、vote関数の引数でこれを受け取る。
そのidでQuestionデータを取得し、存在しなければ404エラーとなる。
投票登録:try~except~elseブロック
このブロックで、Questionデータに対する選択肢をデータベースから取得し、エラーならエラー処理をし、成功したならその選択肢の得票数を1つ増やす。詳細は以下の通り。
try節
try節では、前段で得られたQuestionデータの選択肢Coiceデータのうちフォームで選択されたものを取得する。
getメソッドでpkがPOST['choice']に等しいデータを取り出しているPOST['choice']はフォームにおいてname='choice'のINPUT要素から得られるvalue属性の内容- 今回のフォームではラジオボタンのグループが
'choice'であり、POST['choice']から選択されたボタンのvalue属性の値が得られる - フォームでは
value={{ choice.id }}としているので、この値は選択された選択肢に対応するChoiceデータのidの値
except節
except節では、引数で指定したKeyErrorとChoice.DoesNotExistの2つの例外をキャッチしている。いずれかの例外が発生した場合に、現在処理中のQuestionデータを与えて元のdetailページを表示させるが、このときエラーメッセージをセットしているのでこれも表示される。
KeyErrorはPOST['choice']でデータを取り出すときに'choice'というキーが存在しない場合に発生する。このような状況は次の場合に発生する。
- 選択肢を持たない
Questionデータに対してVoteボタンを押した場合(今回の例の場合question_textが"Hou're you doing?"のデータ) - HTMLのラジオボタンの
name属性が改ざんされた場合
Choice.DoesNotExistの方は、getメソッドで条件に合うデータがデータベース上にないときに発生する。ただ今回のコードでは、DB上のデータが取得できない場合はget_object_or_404により404エラーへと飛ぶようになっているので、ここでDoesNotExist例外は発火しないのではないか。
else節
else節はtry節の処理が正常終了した場合に実行される。今回の場合、フォームで選択された選択肢のChoiceデータが無事取得できたことになるので、データベース上の得票数カウンターを1つ増やして投票結果表示のresultページにリダイレクトする。
得票数のカウントアップは簡単で、取得済みのデータについて以下を実行。
|
1 |
selected_choice.votes += 1 |
ただしこれはデータベースからメモリー上に取得されたデータを変更しただけなので、変更後のデータをデータベースに登録する必要がある。
|
1 |
selected_choice.save() |
最後に戻り値としてHttpResponseRedirectを返している。これはDjangoに特化したものではなく、HTTPでの処理の標準的な手筋。
動作確認
|
1 2 3 4 5 6 7 8 9 |
mysql> select * from choices; +----+--------------------+-------+-------------+ | id | choice_text | votes | question_id | +----+--------------------+-------+-------------+ | 1 | Not much | 1 | 1 | | 2 | The sky | 0 | 1 | | 4 | Just hacking again | 0 | 1 | +----+--------------------+-------+-------------+ 3 rows in set (0.00 sec) |


