Pythonでのチームアプリ開発に参画するための学習履歴~Djangoチュートリアル4~

# はじめに

[前回](https://qiita.com/shitikakei/items/753ba603b5bca272c69c) から引き続きDjangoチュートリアルを進めていきます。
今回はDjangoを使う上で重要なポイントであるクラスベースビューについてです。

# フォームについて

```django

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


```

例えばこういうフォームを作りたいとします。
ある質問に対してラジオボタン付きの回答を用意してチェックを入れたらPOSTで送信するというフォームですね。
`{{ forloop.counter }}`は`{% for choice in question.choice_set.all %}`のループ回数です。
実際の表記になると例えば`<input type="radio" name="choice" id="choice1" value="1" >`といったものになります。
ではこれに対するviewを見てみましょう。

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

        # 上記のコードだと競合問題が起きるのでF(カラム名)メソッドを使う。
        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,)))


```

前回と比べて変わったところは`HttpResposeRedirect`及び`reverse`オブジェクトがインポートがされているところです。
前者はリダイレクトのためのメソッドで、後者の`reverse()`メソッドはURLを返すメソッドでurls.pyで設定した名前付きパスからURLを呼び出すものです。例えば上記の場合は本来なら
`/polls/results`というURLが返ってきますが引数にquestion.idを指定することで`/polls/1/results`といったURLを返すことができます。

vote関数の処理自体は前回までの内容を理解しているのなら何をしているのか理解はそこまで難しくないと思います。
まず、変数にインスタンスを代入します。`get_object_or_404`メソッドなので`Question`モデルから該当する主キーのデータのインスタンスですね。
それを`try-catch-else`のパターンで例外処理していきます。
まずフォームから送信されたPOSTデータを元に`get()`メソッドでデータベースからデータを取得してきます。
`request.POST`は指定したキーで送信したデータにアクセスできるというもの。
例えば`request.POST['choice']`という今回の場合は`name`属性が`choice`のデータにアクセスするということになります。

```html

<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">

```
この部分になりますね。

なので、今回はラジオボタンでのフォームなので選択肢自体は`for`のループでいくらかありますが、送信されるデータは1つなのでその選択肢の`value`の値を取り出して`pk`とするという処理になります。
ちなみにチェックボックスなどで同じ`name`属性が複数ある場合は、`get()`ではなく`getlist()`で取得するようにしてください。
閑話休題、これで例えば`question.choice_set.get(pk=request.POST['choice'])`が`question.choice_set.get(pk=1)`となり、つまり主キーが1のデータを`choice`モデルから検索して変数にインスタンスとして代入するということになります。
無事、取得できれば`else`へ飛び、できなければ`KeyError`の例外として処理され、エラーメッセージを`render()`メソッドで返しフォーム画面に戻されることになります。
`else`の処理は投票数のカウントを増やし、それをデータベースへ更新処理し、投票結果画面へリダイレクトするという処理になりますね。
では投票結果のテンプレートを作成します。

```django

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


```

`vote{{ choice.votes|pluralize }}`の部分は`pluiralize`の左側の値が複数形であった場合、複数形を表す接尾辞を返すという表現です。
例えばこの場合は`choice.votes`が2だとすると`vote`は`votes`になるということになります。


# 汎用ビュー(クラスベースビュー)

さて、ここまで色々と`view(コントローラー)`を書いてきましたが、`Django`にはクラスベースビューといういわゆる汎用的なビューが組み込みで備わっています。
`view`はテンプレートにデータや`URL`を橋渡しするような役割を持っていますがここまでやってきた中でも

・ `filter()`でリストを作って表示する
・ データベースからデータを取得してそれをテンプレートに反映させる
・ `polls/1/results`のような数あるページの中の1ページを表示する

といった処理は幾度となく出てきてその都度`view`を書いていったと思います。
そういった処理を何度も書かなくていいように予めモデルやフォームなどに応じて適切なデフォルト値を設定しておいてくれるのが汎用ビューと呼ばれるものです。
ちなみにクラスベースということからわかるように`Django`での汎用ビューはクラスで提供されているということになります。
他には関数べースというのもあるみたいですね。
では、改めてこれまでの`view`を見てみましょう。

```python

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

# Create your views here.
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):
	response = "You`re looking at the results of question %s."
	return HttpResponse(response % question_id)

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,)))

```

次にこれから書く新しい`view`がこちら

```python

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic

from .models import Question, Choice

# Create your views here.
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:
		# POSTデータにアクセスする。今回はフォームのラジオボタンで選択された選択肢のIDを文字列として返され変数に代入される。
		selected_choice = question.choice_set.get(pk=request.POST['choice'])
	# 例外指定にKeyエラーを指定。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 = F('votes') + 1
		selected_choice.save()
		# Djangoにおけるリダイレクト処理。reverse()でurls.pyで設定した名前付きパスからURLを呼び出す。引数の指定でさらに特定のページを指定できる。
		return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

```

かなり違うのがわかると思います。
まず`django.views`モジュールから`generic`オブジェクトをインポートします。
これは`from django.views.generic import ListView`などとして各クラスベースビュー毎にインポートして使うこともできます。
その場合は、引数は`generic.ListView`ではなく`ListView`になります。
そして先述の通り、クラスベースビューということなのでこれまで関数として書いていたところをクラスに書き直し、各クラスベースビューを継承して使っていきます。
例を挙げると`class IndexView(generic.ListView):`のような部分ですね。継承するので引数に使いたい汎用ビューを指定します。
もちろん、継承するということはオーバーライドもできます。
では、1つずつ見ていきます。

#### Listview

`Listview`はオブジェクトのリストを表示するためのビューです。もっと言うと一覧ページを作るときに利用できるものです。

```python

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]

```

実際に使うと以上のような書き方になります。
通常クラスベースビューを用いるときはモデルの指定が不可欠ですが、`queryset()`のメソッドを指定することもできます。
例えば上記は単にモデルの一覧を表示するのではなく、5件刻みの一覧の表示にしたいのでモデルの指定ではなく`queryset()`での指定をしているわけですね。
あとは`template_name`で橋渡し先のテンプレートを指定し、`context_object_name`で橋渡し先でのオブジェクトの変数名を決めてあげます。
前者はともかく後者は決めておかないと

```django
{% for question in latest_question_list %}

```

こういう書き方をしていたところが

```django
{% for question in object_list %}

```

という書き方をしないと動作しなくなります。
一見したときにどちらのがわかりやすいかは言うまでもないですよね。
ちなみに`Context Processor`という仕組みを使うことで別ファイルにテンプレート変数を定義して、それをインポートすることで特にここで個別に指定しなくても
テンプレートにおいてグローバル変数のように利用することもできます。
[参考:DjangoにGoogle Analyticsを導入する](https://djangobrothers.com/blogs/django_google_analytics/)

#### DetailView

`DetailView`は個別詳細ページに用いることができるクラスベースビューになります。
`polls/1/detail`のようなページに使いたいビューです。


```python

class DetailView(generic.DetailView):

	model = Question
	template_name = 'polls/detail.html'
```

モデルの指定とテンプレートの指定をしているのがわかると思います。
これの他に`urls.py`で`pk`または`slug`の指定を行わないといけないのですが、今回はこのあとに`pk`の指定を行います。


このチュートリアルで使うクラスベースビューは以上2つですが、他にもいくつかあるので今回は簡単にどんなものがあるかだけ見てみます。

#### generic.base.View

クラスベースビューの大本のクラスになります。
各クラスベースはこれを継承したものとなり、さらにそれを継承することで私達はクラスベースを使っているということになるわけですね。

```python:使用例

from django.http import HttpResponse
from django.views import View

class MyView(View):

    def get(self, request, *args, **kwargs):
        return HttpResponse('Hello, World!')
```


#### generic.edit.FormViewとgeneric.edit.CreateViewとgeneric.edit.UpdateView

いずれもフォームに対して用いることができ、更新処理を伴うビューになります。
`FormView`の役割としては`HTTP method GET`を受けるとフォームを表示します、つまり`path`で設定した`URL`が叩かれたらフォームを表示するということですね。
そして、`HTTP method POST`を受けるとフォームから受けた`request`をもとに処理を実行できます。
データベースの追加処理をせずにリダイレクト処理を行うようなフォームに使うようですが、
例えばその例として挙げられるログインフォームには`LoginView`という認証ビューと呼ばれる個別のビューが用意されているので後述する後者の方が使用されることが多いようです。

```python:使用例

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

    def send_email(self):
        # send email using the self.cleaned_data dictionary
        pass
```

`CreateView`に関しては`HTTP method POST`を受けたときにフォームから受けた`request`をもとに指定したモデルに`insert`処理を実行します。
つまり、新しいレコード(データ)を追加するようなフォームに使えるということですね。


```python:使用例

from django.views.generic.edit import CreateView
from myapp.models import Author

class AuthorCreate(CreateView):
    model = Author
    fields = ['name']
```


`UpdateView`は追加ではなく、既存のレコードの更新を伴うフォームに使うビューになります。


```python:使用例

from django.views.generic.edit import UpdateView
from myapp.models import Author

class AuthorUpdate(UpdateView):
    model = Author
    fields = ['name']
    template_name_suffix = '_update_form'
```

さて`UpdateView`及び`CreateView`には`fields`変数が定義されているのがわかると思います。
こちらは双方のビューどちらも使用時に必ず設定する項目で、指定したフィールド以外はフォームからのデータ入力を無視するということになります。
つまり、上記の例では例えば`address`や`Tel`などに相当するフォームの入力項目がいくらあろうと`name`フィールド以外は追加・更新されないということになります。
この指定はリストまたはタプルで設定できるので複数指定することもできます。

#### URLconfの修正

さて各クラスベースについて役割を簡単に押さえ、定義の仕方を確認したところで次は`urls.py`を汎用ビューの使用ができるように修正します。

```python

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'),

]
```

先程、`DetailView`を使うには`pk`の指定を行わないといけないといった部分ですね。
`<int:question_id>`が`<int:pk>`になっていることを確認してください。


#### views.pyの設定

最後に`views.py`をこの項目の冒頭に書いたように修正します。


```python

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic

from .models import Question, Choice

# Create your views here.
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:
		# POSTデータにアクセスする。今回はフォームのラジオボタンで選択された選択肢のIDを文字列として返され変数に代入される。
		selected_choice = question.choice_set.get(pk=request.POST['choice'])
	# 例外指定にKeyエラーを指定。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 = F('votes') + 1
		selected_choice.save()
		# Djangoにおけるリダイレクト処理。reverse()でurls.pyで設定した名前付きパスからURLを呼び出す。引数の指定でさらに特定のページを指定できる。
		return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
```

# 最後に

Djangoを扱うにはここの理解は避けては通れないものを感じました。

# 参考

[DjangoにGoogle Analyticsを導入する](https://djangobrothers.com/blogs/django_google_analytics/)
[Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル](https://qiita.com/felyce/items/7d0187485cad4418c073)
[Djangoの汎用クラスビューをまとめて、実装について言及する](https://qiita.com/renjikari/items/af3e8958d2653e6f8d46)