はじめに
前回 から引き続き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を見てみましょう。
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
のデータにアクセスするということになります。
<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
の処理は投票数のカウントを増やし、それをデータベースへ更新処理し、投票結果画面へリダイレクトするという処理になりますね。
では投票結果のテンプレートを作成します。
<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
を見てみましょう。
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
がこちら
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
はオブジェクトのリストを表示するためのビューです。もっと言うと一覧ページを作るときに利用できるものです。
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
で橋渡し先でのオブジェクトの変数名を決めてあげます。
前者はともかく後者は決めておかないと
{% for question in latest_question_list %}
こういう書き方をしていたところが
{% for question in object_list %}
という書き方をしないと動作しなくなります。
一見したときにどちらのがわかりやすいかは言うまでもないですよね。
ちなみにContext Processor
という仕組みを使うことで別ファイルにテンプレート変数を定義して、それをインポートすることで特にここで個別に指定しなくても
テンプレートにおいてグローバル変数のように利用することもできます。
参考:DjangoにGoogle Analyticsを導入する
DetailView
DetailView
は個別詳細ページに用いることができるクラスベースビューになります。
polls/1/detail
のようなページに使いたいビューです。
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
モデルの指定とテンプレートの指定をしているのがわかると思います。
これの他にurls.py
でpk
またはslug
の指定を行わないといけないのですが、今回はこのあとにpk
の指定を行います。
このチュートリアルで使うクラスベースビューは以上2つですが、他にもいくつかあるので今回は簡単にどんなものがあるかだけ見てみます。
generic.base.View
クラスベースビューの大本のクラスになります。
各クラスベースはこれを継承したものとなり、さらにそれを継承することで私達はクラスベースを使っているということになるわけですね。
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
という認証ビューと呼ばれる個別のビューが用意されているので後述する後者の方が使用されることが多いようです。
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
処理を実行します。
つまり、新しいレコード(データ)を追加するようなフォームに使えるということですね。
from django.views.generic.edit import CreateView
from myapp.models import Author
class AuthorCreate(CreateView):
model = Author
fields = ['name']
UpdateView
は追加ではなく、既存のレコードの更新を伴うフォームに使うビューになります。
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
を汎用ビューの使用ができるように修正します。
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
をこの項目の冒頭に書いたように修正します。
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を導入する
Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル
Djangoの汎用クラスビューをまとめて、実装について言及する