LoginSignup
1
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-16

はじめに

前回 から引き続き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だとするとvotevotesになるということになります。

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

さて、ここまで色々と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.pypkまたは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変数が定義されているのがわかると思います。
こちらは双方のビューどちらも使用時に必ず設定する項目で、指定したフィールド以外はフォームからのデータ入力を無視するということになります。
つまり、上記の例では例えばaddressTelなどに相当するフォームの入力項目がいくらあろうと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の汎用クラスビューをまとめて、実装について言及する

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2