4.フォームと汎用ビュー#
簡単なフォームを書く#
<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><br>
{% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>
変更後以下ラジオボタンが表示される
説明:
上のテンプレートは、各質問の選択肢のラジオボタンが表示するもの。 各ラジオボタンの value は、関連する質問の選択肢のID。 各ラジオボタンの name は "choice"。つまり、投票者がラジオボタンの1つを選択し、フォームを送信すると、POSTデータ choice=# (#は選んだ選択肢のID)が送信される。これは、HTMLフォームの基本的な概念。
フォームのアクションを {% url 'polls:vote' question.id %}
に設定し、method="post を設定。
このフォームを送信するとサーバー側のデータが変更されるため、(
method="get"ではなく)
method="postを使用することは非常に重要。 サーバー側のデータを変更するフォームを作成するときは常に、
method="post"` を使用すること。 このヒントは Django に固有のものではない。 これは、一般的に Web 開発の優れた方法。
forloop.counter
は、 for タグのループが何度実行されたかを表す値。
(データを改ざんされる恐れのある) POST フォームを作成しているので、クロスサイトリクエストフォージェリを気にする必要がある。
Django がこれに対応するとても使いやすい仕組みを提供してくれているので、あまり心配する必要はない。
自サイト内を URL に指定した POST フォームには全て、 {% csrf_token %}
テンプレートタグを使うべき。
forloop.counter は、 for タグのループが何度実行されたかを表す値
(データを改ざんされる恐れのある) POST フォームを作成しているので、クロスサイトリクエストフォージェリを気にする必要があります。ありがたいことに、 Django がこれに対応するとても使いやすい仕組みを提供してくれているので、あまり心配する必要はありません。手短に言うと、自サイト内を URL に指定した POST フォームには全て、 {% csrf_token %}
テンプレートタグを使うべきです。
このとき、 vote() 関数のダミー実装も作成している。
今度は本物を実装する。以下を 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):
# Redisplay the question voting form.
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_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.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
request.POST は辞書のようなオブジェクトです。キーを指定すると、送信したデータにアクセスできます。 この場合、 request.POST['choice'] は、選択された選択肢の ID を文字列として返す。 request.POST の値は常に文字列。
Django では、同じ方法で GET データにアクセスするために request.GET も提供している。 ただし、このコードでは、POST 呼び出し以外でデータが更新されないようにするために、request.POST を明示的に使っている。
POST データに choice がなければ、 request.POST['choice'] は KeyError を送出する。 上のコードでは KeyError をチェックし、 choice がない場合にはエラーメッセージ付きの質問フォームを再表示する。
choice のカウントをインクリメントした後、このコードは、 通常の HttpResponse ではなく HttpResponseRedirect を返す。 HttpResponseRedirect はひとつの引数(リダイレクト先のURL)をとる (この場合にURLをどう構築するかについては、以下のポイントを参照してください)。
POST データを正常に処理した後は、常に HttpResponseRedirect を返す必要がある。 このヒントは Django に固有のものではなく、一般的な Web 開発の優れた手法。
この例では、 HttpResponseRedirect コンストラクタの中で reverse() 関数を使用。
この関数を使うと、ビュー関数中での URL のハードコードを防げる。
関数には、制御を渡したいビューの名前と、そのビューに与える URL パターンの位置引数を与える。
この例では、 チュートリアルその 3 で設定した URLconf を使っているので、 reverse() を呼ぶと、'/polls/3/results/'
の文字列が返ってくる。この 3 は question.id の値。 リダイレクト先の URL は 'results' ビューを呼び出し、最終的なページを表示する。
request は HttpRequest オブジェクトです。 HttpRequest オブジェクトの詳細は リクエストとレスポンスのオブジェクトを参照
次に誰かが質問の投票すると、 vote() ビューは質問の結果ページにリダイレクトするビューを書く
from django.shortcuts import get_object_or_404, render
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
また、 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>
ブラウザで /polls/1/
を表示してさきほど追加した投票機能を確認する。
票を入れるたびに、結果のページが更新されていることがわかる。
選択肢を選ばずにフォームを送信すると、エラーメッセージを表示される。
Warning
これまで作ってきた vote() ビューのコードは、小さな問題を抱えている。
最初にデータベースから selected_choice オブジェクトを取得し、そこで votes の新しい値を計算し、データベースにそれを戻して保存する。
もしウェブサイトのユーザー 2 人が まったく同時に 投票しようとすると、誤りが発生する。
votes の元の値が 42 だったとするとその時、両方のユーザーに対して新しい値として 43 が計算され保存される。
しかし 44 が本来想定される値。この問題は、「競合状態」と呼ばれ、解決はAvoiding race conditions using F()
を参照。
汎用ビューを使う#
汎用ビューとは、よくあるパターンを抽象化して、 Python コードすら書かずにアプリケーションを書き上げられる状態にしたもの。
これまで作成してきた poll アプリを汎用ビューシステムに変換して、 コードを減らす。 変換のステップは次のとおり。 1. URLconf を変換 2. 古い不要なビューを削除 3. 新しいビューに Djangoの汎用ビューを設定
なぜコード入れ換えるのか?
一般に Django アプリケーションを書く場合は、まず自分の問題を解決するために汎用ビューが適しているか考えた上で、 最初から汎用ビューを使い、途中まで書き上げたコードをリファクタすることはない。 ただ、このチュートリアルでは中核となるコンセプトに焦点を合わせるために、わざと「大変な」ビューの作成の手順が用意されている。
URLconf の修正#
まず、 URLconf の polls/urls.py
を開き、次のように変更
from django.urls import path
from . import views
app_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'),
]
Note
2つ目と3つ目のパス文字列に一致するパターンの名前が <question_id>
から <pk>
に変更されたことに注意
views の修正#
次に、古い index 、 detail 、と results のビューを削除し、代わりに Django の汎用ビューを使用します。これを行うには、 polls/views.py ファイルを開き、次のように変更します:
from django.http import 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'
def vote(request, question_id):
... # ここは変更しない
ここでは、ListView
と DetailView
を使用。
これらのビューはそれぞれ、「オブジェクトのリストを表示する」および「あるタイプのオブジェクトの詳細ページを表示する」という二つの概念を抽象化している。
各汎用ビューは自分がどのモデルに対して動作するのか知っておく必要がある。 これは、 model 属性を使用して提供される。
DetailView
汎用ビューには、 pk
という名前で URL からプライマリキーをキャプチャして渡すことになっているため、 汎用ビュー向けに question_id を pk に変更している。
デフォルトでは、 DetailView 汎用ビューは <app name>/<model name>_detail.html
という名前のテンプレートを使用。
この場合、テンプレートの名前は polls/question_detail.html
となる。
template_name
属性を指定すると、自動生成されたデフォルトのテンプレート名ではなく、
指定したテンプレート名を使うように Django に伝えることができる。
また、 results リストビューにも template_name
を指定。
これによって、 結果ビューと詳細ビューをレンダリングしたとき、(裏側ではどちらも DetailView )それぞれ違った見た目になる。
同様に、 ListView 汎用ビューは <app name>/<model name>_list.html
というデフォルトのテンプレートを使うため、
template_name を使って ListView に既存の polls/index.html
テンプレートを使用するように伝える。
このチュートリアルの前の部分では、 question や latest_question_list
といったコンテキスト変数が含まれるコンテキストをテンプレートに渡していた。
DetailView には question という変数が自動的に渡される。
なぜなら、 Django モデル (Question) を使用していて、 Django はコンテキスト変数にふさわしい名前を決めることができるから。
一方で、 ListView では、自動的に生成されるコンテキスト変数は question_list になる。
これを上書きするには、 context_object_name
属性を与え、 latest_question_list を代わりに使用すると指定する。
この代替アプローチとして、テンプレートのほうを変えて、新しいデフォルトのコンテキスト変数の名前と一致させることもできる。
しかし、使用したい変数名を Django に伝えるだけのほうが簡単。
サーバを実行して、新しく汎用ビューベースにした投票アプリケーションを使ってみると見た目は変わらないことが分かる。
汎用ビューの詳細は、 汎用ビューのドキュメントを参照。