【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その4～

# はじめに

みなさん、初めまして。
Djangoを用いた投票 (poll) アプリケーションの作成過程を備忘録として公開していこうと思います。
Qiita初心者ですので読みにくい部分もあると思いますがご了承ください。

シリーズ

- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その0～](https://qiita.com/oboerarenai_user/items/4ff87b2afbad8c16ad40)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その1～](https://qiita.com/oboerarenai_user/items/4357841493832cd6ca2a)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その2～](https://qiita.com/oboerarenai_user/items/4a36c2b11e7dee74b66b)
- [【初心者】【Python/Django】駆け出しWebエンジニアがDjangoチュートリアルをやってみた～その3～](https://qiita.com/oboerarenai_user/items/db5ef43bcff25223a147)

# 作業開始

## 簡単なフォームを書く

HTMLでフォームを書いていきます。

### 投票ビュー

formタグの説明

- action：データの送信先URL
- method：データの送信方法(get,post)

inputタグの説明

- type：ラジオボタン(radio)、送信ボタン(submit)
- value：現在の値
- id：ボタンを識別するID

「{% csrf_token %}」はDjangoが用意しているクロスサイトリクエストフォージェリ対策の仕組みです。
POSTメソッドを使用しているのでcsrf_tokenを使います。

```django:polls/template/polls/detail.html
<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>
```

「request.POST['choice']」でPOSTデータのchoiceにアクセスします。
POSTデータにchoiceがない場合は、KeyErrorを返します

exceptionが発生した場合は、再度質問フォームをエラー付きで表示します。

「reverse('polls:results', args=(question.id))」はpolls/urls.py > path関数(name='results')を返します。

```python
:polls/views.py

from django.http import HttpResponse, Http404, HttpResponseRedirect
from .models import Question, Choice
from django.shortcuts import render, get_object_or_404
from django.urls import reverse

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()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
```
「http://127.0.0.1:8000/polls/<質問ID>」にアクセスしてフォーム画面が表示されればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/763383/351684de-1245-b5b1-03d3-f467c8e51e00.png)

### 結果ビュー

```django:polls/template/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>
```

```python:polls/views.py

from django.http import Http404, HttpResponseRedirect

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})
```
「http://127.0.0.1:8000/polls/<質問ID>」のフォーム画面で選択肢を選択してVoteボタンを押すと以下の画面に遷移します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/763383/687c49f3-a9f6-47d8-ce54-a3a42d5914bb.png)

## 汎用ビューを使う: コードが少ないのはいいことだ

コードの縮小化を行っていきます。特に、冗長部分を排除していきます。
>
これらのビューは基本的な Web開発の一般的なケースを表します。すなわち、 URL を介して渡されたパラメータに従ってデータベースからデータを取り出し、テンプレートをロードして、レンダリングしたテンプレートを返します。これはきわめてよくあることなので、 Django では、汎用ビュー（generic view）というショートカットを提供しています
汎用ビューとは、よくあるパターンを抽象化して、 Python コードすら書かずにアプリケーションを書き上げられる状態にしたものです。

### URLconf の修正

name=detail、resultsのみ、「<int:questiopkn_id>」→「<int:pk>」にします。これは後ほど説明するDetailViewを使用するためです。
name=index、detail、resultsは後ほどclassを作成するので「views.**View.as_view()」としましょう

```python:polls/urls.py

    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:questiopkn_id>/vote/', views.vote, name='vote'),
```
### views の修正

views.pyは大きく変わります。
defで関数を多用していましたが、classを使います。

- ListView汎用ビューは"<アプリ名>/<モデル名>_list.html"テンプレートを使い、"<モデル名>_list"コンテキスト変数を渡してレタリングを行います。
template_name属性を用いると、指定したテンプレートを使うようになります。
context_object_name属性を用いると、指定したコンテキスト変数を使うようになります。

- DetailView汎用ビューは"<アプリ名>/<モデル名>_detail.html"テンプレートを使い、"<モデル名>"コンテキスト変数を渡してレタリングを行います。
template_name属性を用いると、指定したテンプレートを使うようになります。
context_object_name属性を用いると、指定したコンテキスト変数を使うようになります。
urls.pyで指定したように、"pk" という名前で URL からプライマリキーを読み取る仕組みになっています。

```python:polls/views.py

# Create your views here.
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.views import generic

from .models import Question, Choice


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        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):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
```

今日はここまでにします。ありがとうございました。