Help us understand the problem. What is going on with this article?

Djangoにおけるクラスベース汎用ビューの入門と使い方サンプル

間違い、勘違いなどありましたら優しくご指摘ください。

  • Django 1.9
  • python 3.5.1

なぜこの記事を書くのか

Djangoには強力なテンプレート言語があります。

テンプレートにデータを供給するのはViewです。
そして、それを手短にコーディングする方法も用意されています。
それが「汎用ビュー(Generic View)」です。

はるか昔は関数ベースの汎用ビューを使っていたようですが、現在は主にクラスベースのビューを使います。
現在のDjangoの和訳ドキュメントは1.4が最新ですが、クラスベース汎用ビューのドキュメントはちゃんとあります。

http://docs.djangoproject.jp/en/latest/topics/class-based-views.html

しかし、思ったよりもクラスベース汎用ビューを解説している記事が少なかったため、書き記そうと思いました。

この記事の内容

  • Djangoでクラスベース汎用ビューを使う方法

汎用ビューとは何か

viewとはRailsでいうところのコントローラーに当たります。DjangoはMTV(Model, Template, View)という考え方なのでviewと呼んでいるようです。

データをテンプレートに供給する役目を負っていますが、ウェブサイトのほとんどは

  • 特定の条件下のリスト
  • DBから記事を絞り込んで表示する
  • 単一の記事を表示する

という機能を持っています。何度も同じようなコードを書かなくても済むようにあらかじめDjangoに処理を定義してあります。

それらを「汎用ビュー」と呼び、関数で提供されていれば「関数ベース汎用ビュー」、クラスで提供されていれば「クラスベース汎用ビュー」と呼ぶようです。

汎用ビューでは、モデルやフォームなどに応じて適切なデフォルト値を設定しておいてくれます。
したがって、プログラマが記述する量がとても少なくなるという利点があるのです。

この記事では、「クラスベース汎用ビュー」について書き留めています。

クラスベース汎用ビューはそれぞれの同じような処理をクラスで定義しています。
私たちは、そのクラスを継承し、必要であればクラス変数を変更し、またメソッドをオーバーライドして自身の処理を挿入したりできます。

クラスベース汎用ビューの使い方

最初の一歩

最初の一歩ですので、まずはどのように記述するのかにフォーカスを当ててみます。

汎用ビューは主にviews.pyなどで使用されます。
また、urls.pyas_view()という関数でURLを紐つけます。

サンプルを書いてみましょう。
django-adminなどでプロジェクトを作成し、manager.py startapp fooとしてアプリケーションを一つ作成したものとします。

さて、書いていきます。

foo/views.py
from django.views.generic import TemplateView

class SampleTemplateView(TemplateView):
    template_name = "index.html"

これで使用可能です。
記述量がとても少ないことがわかります。

ここでは、TemplateViewが汎用ビューです。
django.views.genericなどに、それぞれの用途に応じたビューが準備されていますから、importして継承します。

URLは、

urls.py
from foo.views import SampleTemplateView

urlpatterns = [
    url(r'^sample$', SampleTemplateView.as_view()),
]

とすれば、/sampleにアクセス際にSampleTemplateViewがよしなに処理してくれます。

このように、views.pyに書き込むコードの量を激減させることができます。
TemplateViewはその名の通りテンプレートを使って描画させることを目的としています。
TemplateViewではなく他の汎用ビューを使えば、DBからデータを取得するコードを書くこと無く一覧表示ができたり、特定の条件で絞り込んだあとのデータを出力することができます。

テンプレートに自分で設定した変数を渡したい

はじめの一歩では、ただテンプレートを表示することしかできませんでした。
しかし、普通はDBから読み込んだデータを表示したくなるでしょう。

そんな時は汎用ビューで定義されているメソッドをオーバーライドします。

私がよくオーバーライドしているメソッドはget_context_dataです。

以下のようにして使います。

views.py
from django.views.generic import TemplateView


class SampleTemplate(TemplateView):

    template_name = "index.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs) # はじめに継承元のメソッドを呼び出す
        context["foo"] = "bar"
        return context

のように使います。

上記のコードの場合、テンプレートでfooという変数を使えるようにしています。
テンプレートで{{ foo }}とすればbarとなります。

テンプレートのファイル名をindex.htmlなどとすると

index.html
<div class="sample">{{ foo }}</div>

とすれば、

index.html
<div class="sample">bar</div>

となるということです。

つまり、このメソッド内に自身が挿入したい処理を記述し、get_context_dataの戻り値に入れておけば、テンプレートで使用できる、ということになります。

この章まとめ

汎用ビューにはこのようにメソッドをオーバーライドして自分が必要な処理を追加していきます。
これが基本的な使い方です。

一覧表示するためのListViewや、個別ページを作成するためのDetailViewなど便利な汎用ビューがたくさんありますので、紹介して行きます。

クラスベース汎用ビューの種類(一部)

注:ここに紹介しているものが全てではありません

https://github.com/django/django/blob/master/django/views/generic/__init__.py

を見ると、RedirectViewFormViewなどもっとたくさんの汎用ビューが定義されています。

TemplateView

TemplateViewはその名の通り、テンプレートを指定して何かを表示する汎用ビューです。
したがって、テンプレート名はかならず指定しなければなりません。

また、get_context_dataを呼び出すので、必要なデータはオーバーライドして設定します。
もちろん、汎用ビューの管理外ですので、自身で件数やフィルタリングを行う必要があります。

views.py
from django.views.generic import TemplateView


class MyTemplateView(TemplateView):

    template_name = "index.html"  # 必須

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # いろいろやる
        return context

筆者は主にサイトのトップページなどで使用しています。

ListView

https://docs.djangoproject.com/ja/1.9/ref/class-based-views/generic-display/#listview

ListViewはその名の通り、一覧ページなどを作る時に使用します。

単純に対象のモデルのリストを作成すると、

views.py
from django.views.generic import ListView
from foo.model import MyModel

class MyListView(ListView):

    model = MyModel

のように定義します。
model = MyModelはリストを作成するモデルです。

テンプレートではこのようになるでしょう。

mymodel/mymodel_list.html
<ul>
  {% for item in object_list %}
  <li>{{ item }}</li>
  {% endfor %}
</ul>

DBにたくさんのデータが入っていても、ListViewはデフォルトで10件ずつリストを分割します(コメントでの指摘により変更)全件取得しますが、後述する変数で一ページに出力する件数を制御できます。
分割する件数はpaginate_byという変数です。

つまり、5件ずつ分割したいのなら

views.py
class MyListView(ListView):
    model = MyModel
    paginate_by = 5

と定義します。

これは、100件データがあったら、一ページに5件だけ表示させる、という設定です。

また、時にはあらかじめ取得するデータのソート順を変えたり、フィルタリングして制御したい時もあるでしょう。
その時はget_querysetメソッドをオーバーライドします。

views.py
class MyListView(ListView):
    model = MyModel
    paginate_by = 5

    def get_queryset(self):
        return MyModel.objects.filter(some_column=foo)

のようにします。
そうすれば、get_querysetで定義されたクエリを発行し、データをobject_listに格納します。

もし、一覧に抽出する件数を制限したいときはここで指定します。

指定は普通のスライスです。

views.py
def get_queryset(self, queryset=None):
    return MyModel.objects.all()[:10]

これでたとえデータが100件あっても10件しか抽出されなくなります。

この書き方は、もう少し短くできます。

views.py
class MyListView(ListView):
    model = MyModel
    paginate_by = 5
    queryset = MyModel.objects.filter(some_column=foo)

どちらを使っても結果は変わりません。

動的にクエリを発行したいのなら前者を、常に同じ結果を求めるのなら後者を使えば良いと思います。

もちろん、get_context_dataメソッドをオーバーライドすれば、他のテーブルからのデータをテンプレートに渡すことができます。

views.py
class MyListView(ListView):
    model = MyModel
    paginate_by = 5

    def get_queryset(self):
        return MyModel.objects.filter(some_column=foo)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**data)
        context["bar"] = OtherModel.objects.all()  # 他のモデルからデータを取得
        return context

ただ、この場合、OtherModelは汎用ビューの制御下にはありませんので、context["bar"]にはOtherModelの全件数が格納されます。

DetailView

https://docs.djangoproject.com/ja/1.9/ref/class-based-views/generic-display/#detailview

DetailViewはその名の通り、個別詳細ページを対象とした汎用ビューです。
レコード一列を対象としてデータを取得します。

views.py
from django.views.generic import DetailView
from mymodel.model import MyModel


class MyDetailView(DetailView):
    model = MyModel

URLは

urls.py
urlpatterns = [
    url(r'^(?P<pk>\d+)$', MyDetailView.as_view()),
]

とします。
pkはプライマリーキーのことですが、DetailViewはこの名前を使ってレコードを特定します。
変更することもできますが、必要がなければこのままでよいでしょう。

CreateView・UpdateView

https://docs.djangoproject.com/ja/1.9/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView

CreateViewは新たにレコードを追加するフォームを提供するビューです。
UpdateViewはお察しの通り、すでにあるデータを更新するフォームを提供するビューです。

views.py
from django.views.generic.edit import CreateView

class MyCreateView(CreateView):
    model = MyModel
    fields = ("name", )  # リストもしくはタプル

このように定義します。他の汎用ビューとさほど変わりません。
ただ、CreateViewUpdateViewにはfieldsという変数を定義する必要があります。
これは、フォームからデータが入力されても、fieldsに定義したフィールド以外は無視するというものです。
RailsのStrongParametersのようなものでしょうか。

CreateViewUpdateViewが他の汎用ビューと異なる点は、「フォームを作成」するということです。
つまり、objectobject_listではなく、formという変数を生成します。

form変数にはフォームを作成するためのデータが格納されていて、フォームを簡単に作成できます。

form変数を使ったテンプレートはこうなります。

<form method="post">
    {% csrf_token %}
    <table>
        {{ form }}
    </table>
    <input type="submit">
</form>

HTMLタグの<form></form>は自分で書かなければなりませんが、フォームフィールドは生成してくれます。
{{ form }}はデフォルトでテーブルを使ったフィールドを出力します。
<p></p>タグを使ったフィールドにしたいときは{{ form.as_p }}とます。

{% csrf_token %}はCSRF対策です。DjangoでPOSTフォームなどを作った場合は必須です。

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

http://docs.djangoproject.jp/en/latest/ref/contrib/csrf.html

また、ModelFormなどのフォームオブジェクトを利用することもできます。

このようにして、簡単に既存のフォームクラスを利用することができます。

forms.py
from django import forms
from myapp.model import MyModel

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ("name", )
views.py
from forms import MyModelForm

class MyCreateView(CreateView):
    form_class = MyModelForm

また、これらのフォームで重要な機能としてバリデーションがあります。
せっかくフォームを送信しても、バリデーションでエラーになり、追加・更新されない場合、ユーザーに通知しなければ混乱するかもしれません。

そこで、これらの汎用ビューにはform_validform_invalidというメソッドが定義されていて、バリデーションが通ったか否かで自身が必要な処理を実行できます。

例えば、送信したフォームが上手く保存されたかどうかをユーザーに通知することは良くあります。
そんな時Djangoではメッセージフレームワークを使います。

http://docs.djangoproject.jp/en/latest/ref/contrib/messages.html

views.py
from django.views.generic.edit import CreateView
from django.contrib import messages  # メッセージフレームワーク

from myapp.models import MyModel


class MyCreateView(CreateView):
    model = MyModel

    def form_valid(self, form):
        ''' バリデーションを通った時 '''
        messages.success(self.request, "保存しました")
        return super().form_valid(form)

    def form_invalid(self, form):
        ''' バリデーションに失敗した時 '''
        message.warning(self.request, "保存できませんでした")
        return super().form_invalid(form)

ここでは、バリデーションに成功した時に呼ばれるform_validメソッドと、バリデーションに失敗した時に呼ばれるform_invalidに独自のメッセージを追加しています。

こうすることによって独自の処理を追加できるので、例えば上記の例のほか、エラーが発生した時にメールで管理者に通知するなどの処理を挿入することができます。

独自の処理を終えたら、継承元のメソッドを呼び出して、戻り値を返します。

ちなみに、メッセージを表示するには以下のようにします。
ドキュメントの内容そのままですが。

{% if messages %}
<ul class="messages">
    {% for message in messages %}
    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
    {% endfor %}
</ul>
{% endif %}

DeleteView

DeleteViewはその名の通り、レコードを削除します。
テンプレートは(AppName)/(AppName)_confirm_delete.htmlです。

views.py
from django.views.generic.edit import DeleteView
from myapp.models import MyModel


class MyDeleteView(DeleteView):
    model = MyModel

この場合のテンプレートは「本当に削除してもよいですか?」という確認のフォームになるでしょう。

もしかしたら本当に削除するのではなく、削除フラグをONにしたいだけの時もあると思います。
そんな時はdeleteメソッドをオーバーライドするのが良いと思います。
ただ、筆者は素直に削除してしまうので試したことはないです。

共通項目

クラスベース汎用ビューは前述したそれぞれの処理はもちろん、テンプレートのファイル名、変数名についても自由にオーバーライドできます。
また、その方法も共通していますので、いくつか紹介します。
普段はデフォルト値を使い、必要なときだけオーバーライドするのが良いと思います。

デフォルト変数

すでにお気づきかと思いますが、汎用ビューにはある程度の規則性があります。
get_context_dataをオーバーライドし、必要な処理を行い、変数にセットすることは初めの一歩で紹介しました。
テンプレートにデータを渡したいときはほとんどこれで事足りると思います。

ではその反対に、汎用ビューが自動的にデータを取得するとき、どんな変数にデータにどんなデータがセットされるのでしょうか。

クラスベース汎用ビューでは、その用途(継承するクラス)に応じて自動的にDBからデータを取得します。
どのテーブルからデータを取得するのかはmodel = MyModelのように設定します。

views.py
class MyListView(ListView):
    model = MyModel  # MyModelのリストを自動的に取得する

このようにすることで、MyModelからデータを取得します。
その結果などは決まった名称の変数にセットされることになっています。
個別ページなどのレコード一つだけならobject
複数であればobject_list

テンプレートでは以下のようになるでしょう。

個別ページなど

{{ object }}

複数であれば

{% for item in object_list %}
  {{ item }}
{% endfor %}

しかし、これでは何を指しているのかテンプレートを見てもわかりません。
変数名をもっとわかりやすい名前にしたいこともあると思います。
そんな時は、context_object_nameに変数名を代入します。

views.py
class MyBookList(ListView):
    model = Books
    context_object_name = "my_books"  # この行で変数名を指定

このようにすると、テンプレートでは

<ul>
    {% for book in my_books %}
    <li>{{ book.name }}</li>
    {% endfor %}
</ul>

のようにして呼び出せます。
変数名が何を指すのかはっきりしたので、テンプレートを読んだだけで変数が何を示しているかわかりやすくなります。

テンプレートとして使うファイルの指定

viewsはコントローラーです。
したがって、適切なテンプレートにデータを渡します。
テンプレートは明示的に指定することもできますが、デフォルトのテンプレート名を持っています。
それは、

(ModelName)/(ModelName)_(ViewName).html

というものです。

それらは、

  • AppName
    • manage.py startappなどで作成したアプリケーションの名前
  • ViewName
    • ListViewであればlistDetailViewであればdetailなど、固有の名称

という事になっています。
つまり、myappListViewを使うと、デフォルトでmyapp/myapp_list.htmlを探します。

これを変更したいときは、

views.py
class MyListView(ListView):
    model = MyModel
    template_name = "my_list_view.html"  # この行でテンプレート指定

のようにします。

余談

汎用ビューを使っていると、同じ変数をすべてのビューで定義したくなります。
たとえば、「サイト名」です。
すべてのビューでそれを定義するのはイマイチなので、共通のクラスを作って継承しようかなぁと思ったりします。

base_view.py
from django.views.generic import ListView

from myapp.models import MyModel


# Mixinなどを継承せず、objectを継承してください。
class MyCommonView(object):

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["SITE_NAME"] = "SITE-NAME"
        return context


class MyListView(ListView, MyCommonView):
    model = MyModel

これで、サイト名を変更したいときは一箇所変更するだけで済みます・・・か?
それも一つの方法でしょうが、私は素直にContextProcessorを使ったほうがいいと思います。

あとがき

疲れました。

いくらかでもDjangoを使う人の参考になればと思います。

一応、

  • Django 1.9
  • python 3.5.1

で動作を確認していますが、おかしな所がありましたら優しく教えて下さい。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした