Help us understand the problem. What is going on with this article?

Djangoのお勉強(5)

More than 1 year has passed since last update.

はじめての Django アプリ作成、その 4の内容にそって試したことや調べた事を記載する。
前回の記事は Djangoのお勉強(4)

簡単なフォームを書く

、、、というわけで、チュートリアルのソースを試してみる

polls/template/polls/detail.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>

この変更の後

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()が呼ばれるので、この関数を用意する。

polls/views.py
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()関数をチュートリアルに従って書き換える

polls/view.py
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テンプレートをチュートリアルに従って追加する

polls/templates/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()に書いてあるけど、英語だからよくわかんない。
とりあえず、変更して動かしてみたけど動いてる。でもこの検証試験するのはしんどいのでやらない。

ここまでのソース

polls/views.py
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,)))
polls/templates/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>
polls/templates/polls/detail.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を書き換える

pools/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'),
]

index,detail,resultsのurlパターンが変更されている。
引数question_idがpkに変更されている。
汎用ビューでは、pkという名前の引数にプライマリキーを設定する事になっている。

また、今まではpolls/views.pyの中のindex(),detail(),result()の各関数が呼び出されていたが、汎用ビューでは、汎用ビューのクラスの中のas_view()を呼び出す。

、、、というわけで、polls/view.pyを変更する

pools/views.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 に、それぞれモデルクラスと、テンプレートのパス名を設定する

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away