Djangoのお勉強(５) 

[はじめての Django アプリ作成、その 4](https://docs.djangoproject.com/ja/2.0/intro/tutorial04/)の内容にそって試したことや調べた事を記載する。
前回の記事は [Djangoのお勉強(４)](https://qiita.com/ShimantoAkira/items/88db1fa16ec6ca3fa421)

# 簡単なフォームを書く

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

```html: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にセットした内容にもよるが、下記のような出力になる

```html:
<!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">ちょいす３－１</label><br />

    <input type="radio" name="choice" id="choice2" value="7" />
    <label for="choice2">ちょいす３－２</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">ちょいす３－１</label><br />

    <input type="radio" name="choice" id="choice2" value="7" />
    <label for="choice2">ちょいす３－２</label><br />
```
に、それぞれ変換されて出力されているのがわかる。

```
<h1>{{ question.question_text }}</h1>
```
の方は、既に取り上げたものと同じなので、formの中味を調べる

```
<form action="{% url 'polls:vote' question.id %}" method="post">
```
は、[テンプレート中のurlのハードコードを解消する](https://qiita.com/ShimantoAkira/items/88db1fa16ec6ca3fa421#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E4%B8%AD%E3%81%AEurl%E3%81%AE%E3%83%8F%E3%83%BC%E3%83%89%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%A7%A3%E6%B6%88%E3%81%99%E3%82%8B)でお勉強した事と同じで、urlパターン名が、urlの絶対パス名に変換されている。

```
{% csrf_token %}
```
は、[クロス サイトリクエストフォージェリ](https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA)を防止する為の仕組みで、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()が呼ばれるので、この関数を用意する。

```python: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()関数をチュートリアルに従って書き換える

```python: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テンプレートをチュートリアルに従って追加する

```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を付け加えたり、加えなかったりする。
英語において複数形は大事です。
結果的に、下記のように変換されます

```html:
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <title>views/result</title>
</head>
<body>
<h1>Question Text3</h1>

<ul>

    <li>ちょいす３－１ -- 2 votes</li>

    <li>ちょいす３－２ -- 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()](https://docs.djangoproject.com/ja/2.0/ref/models/expressions/#avoiding-race-conditions-using-f)に書いてあるけど、英語だからよくわかんない。
とりあえず、変更して動かしてみたけど動いてる。でもこの検証試験するのはしんどいのでやらない。

# ここまでのソース

```python: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,)))
```

```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>
```

```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を書き換える

```python: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を変更する

```python: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 ビュー](https://docs.djangoproject.com/ja/2.0/ref/class-based-views/generic-display/)に記載されており、ListViewとDetailViewの説明もそこに書かれている。

ListViewは、下記のように使われている

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