はじめての Django アプリ作成、その 4の内容にそって試したことや調べた事を記載する。
前回の記事は Djangoのお勉強(4)
簡単なフォームを書く
、、、というわけで、チュートリアルのソースを試してみる
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Detail</title>
</head>
<body>
<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>
</body>
</html>
この変更の後
python manage.py runserver
を実行して、http://127.0.0.1:8000/polls/3/ にアクセスすると、DBにセットした内容にもよるが、下記のような出力になる
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Detail</title>
</head>
<body>
<h1>Question Text3</h1>
<form action="/polls/3/vote/" method="post">
<input type='hidden' name='csrfmiddlewaretoken' value='WZJQnlp0x1DNpnLyfolOPKVja2CbUA5SGeSPNTG8Zp7JNhQJL0AQpg4swpLLZac5' />
<input type="radio" name="choice" id="choice1" value="3" />
<label for="choice1">ちょいす3-1</label><br />
<input type="radio" name="choice" id="choice2" value="7" />
<label for="choice2">ちょいす3-2</label><br />
<input type="submit" value="Vote" />
</form>
</body>
</html>
<h1>{{ question.question_text }}</h1>
は
<h1>Question Text3</h1>
に、
<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 %}
は
<form action="/polls/3/vote/" method="post">
<input type='hidden' name='csrfmiddlewaretoken' value='WZJQnlp0x1DNpnLyfolOPKVja2CbUA5SGeSPNTG8Zp7JNhQJL0AQpg4swpLLZac5' />
<input type="radio" name="choice" id="choice1" value="3" />
<label for="choice1">ちょいす3-1</label><br />
<input type="radio" name="choice" id="choice2" value="7" />
<label for="choice2">ちょいす3-2</label><br />
に、それぞれ変換されて出力されているのがわかる。
<h1>{{ question.question_text }}</h1>
の方は、既に取り上げたものと同じなので、formの中味を調べる
<form action="{% url 'polls:vote' question.id %}" method="post">
は、テンプレート中のurlのハードコードを解消するでお勉強した事と同じで、urlパターン名が、urlの絶対パス名に変換されている。
{% csrf_token %}
は、クロス サイトリクエストフォージェリを防止する為の仕組みで、formの中に、{% 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 %}
では、選択されたQuestionに紐付けられているChoiceの分だけ、ラジオボタンを作っている。
choice{{ forloop.counter }}
のforloop.counterは、[組み込みタグfor]http://djangoproject.jp/doc/ja/1.0/ref/templates/builtins.html の説明内に記載されていて、現在のループカウンタ番号を示す。
viewを準備する
今の状態でVoteと表示されているSubmitボタンを押された時 http://127.0.0.1:8000/polls/3/vote/ にアクセスする。
このurlに対しては、 pools/urls.pyの中での
path('<int:question_id>/vote/', views.vote, name='vote'),
の部分が対応し、polls/views.pyのvote()が呼ばれるので、この関数を用意する。
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,)))
この処理で、
question = get_object_or_404(Question, pk=question_id)
は、question_id(http://127.0.0.1:8000/polls/3/vote/ のうちの3)をkeyにして、Questionテーブルからレコードを読み込む。
もし、該当するレコードが無い場合は、404エラーを表示する
selected_choice = question.choice_set.get(pk=request.POST['choice'])
は、name=choiceと名付けられているラジオボタンのvalue値を取り出してkeyとし、Choiseテーブルから現在のquestionに紐付けられているレコードを読み出す。
該当するレコードが無い場合は、
# Redisplay the question voting form.
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "You didn't select a choice.",
})
の処理になり、questionとerror_messageを指定してdetailのテンプレートを表示する。
正常な場合は、下記の処理を行う。
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,)))
選択されたchoiseのvotesフィールド値をインクリメントして保存する。
polls/urls.pyの
path('<int:question_id>/results/', views.results, name='results'),
のurlパターンをリダイレクトし、polls/views.pyのresults()関数が呼び出される。
、、、というわけで、polls/views.pyのresults()関数をチュートリアルに従って書き換える
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
この処理は、question_idをキーにしてquestion テーブルを読み出し、polls/results.htmlテンプレートにquestionを渡して表示している。
、、、というわけで、polls/results.htmlテンプレートをチュートリアルに従って追加する
<!DOCTYPE html>
<html lang="jp">
<head>
<meta charset="UTF-8">
<title>views/result</title>
</head>
<body>
<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>
</body>
</html>
このテンプレートには、目新しいことは{{ choice.votes|pluralize }}くらいである。
|pluralizeは、choice.votesの値を見て、sを付け加えたり、加えなかったりする。
英語において複数形は大事です。
結果的に、下記のように変換されます
<!DOCTYPE html>
<html lang="jp">
<head>
<meta charset="UTF-8">
<title>views/result</title>
</head>
<body>
<h1>Question Text3</h1>
<ul>
<li>ちょいす3-1 -- 2 votes</li>
<li>ちょいす3-2 -- 3 votes</li>
</ul>
<a href="/polls/3/">Vote again?</a>
</body>
</html>
複数アクセスによるDBへの競合の解消
polls/view.pyのvote()関数は、複数ユーザーから同時にアクセスされた時に、競合する可能性がある。
selected_choice.votes += 1
selected_choice.save()
の部分で、selected_choice.votesをインクリメントする時に、他者がおなじ処理をすると、selected_choice.votesの値が、正しくインクリメントされない可能性がある。
これを回避するためには、
selected_choice.votes =F('votes')+1
selected_choice.save()
と、書き換える、、、、らしい。Avoiding race conditions using F()に書いてあるけど、英語だからよくわかんない。
とりあえず、変更して動かしてみたけど動いてる。でもこの検証試験するのはしんどいのでやらない。
ここまでのソース
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.http import Http404
from .models import Choice, Question
from django.urls import reverse
from django.db.models import F
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)
return render(request, 'polls/detail.html', {'question': question})
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': 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.votes =F('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,)))
<!DOCTYPE html>
<html lang="jp">
<head>
<meta charset="UTF-8">
<title>views/result</title>
</head>
<body>
<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>
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Detail</title>
</head>
<body>
<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>
</body>
</html>
汎用ビュー
「URL を介して渡されたパラメータに従ってデータベースからデータを取り出し、テンプレートをロードして、レンダリングしたテンプレートを返す」という処理を一個にまとめたショートカットがあるそうだ。
その為に、polls/urls.pyとpolls/views.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'),
]
index,detail,resultsのurlパターンが変更されている。
引数question_idがpkに変更されている。
汎用ビューでは、pkという名前の引数にプライマリキーを設定する事になっている。
また、今まではpolls/views.pyの中のindex(),detail(),result()の各関数が呼び出されていたが、汎用ビューでは、汎用ビューのクラスの中のas_view()を呼び出す。
、、、というわけで、polls/view.pyを変更する
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
from django.db.models import F
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):
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 =F('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,)))
表示系の汎用ビューについては、Generic display ビューに記載されており、ListViewとDetailViewの説明もそこに書かれている。
ListViewは、下記のように使われている
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]
template_nameにテンプレートのパス名を指定する
context_object_nameに、テンプレート上、リストのデータを得るイテレータの名前を指定する。
そのcontext_object_nameには、get_queryset()が返した値がセットされる。
DetailViewでは、modelとtemplate_name に、それぞれモデルクラスと、テンプレートのパス名を設定する