フォームの配置
このページは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) |