[Django]ジェネリックビューってなんなんだよ

DjangoのGeneric View(クラスベースのビュー)について理解したくてまとめました。
今回は割と適当です。適当というか投げやり。

準備

とりあえず必要なものを用意します。
当たり前ですがジャンゴくんとmodelを使いたいのでsqlite3あたり。

$ pip install django
$ sudo yum -y install sqlite3
$ django-admin startproject djangotest
$ cd djangotest
$ ./manage.py startapp testapp

settings.pyにはこんな感じのことを追記。

TEMPLATE_DIRS = ('templates/',)
INSTALLED_APPS += ('testapp',)

モデル

適当に定義。これをViewから参照します。

1
2
3
4
5
6
from django.db import models
 
class Test(models.Model):
    a = models.CharField(max_length=20)
    b = models.IntegerField()
    c = models.BooleanField(default=False)
マイグレーション

モデルに定義したテーブルを作ります。
1.7からはマイグレーションで作ることが推奨されているのでmigrateで作ります。(こまけぇこたぁいいんだよ

$ ./manage.py makemigrations
$ ./manage.py migrate
テストレコード

適当に10レコードほど追加。

>>> from testapp.models import Test
>>> for params in (
...     {'a': 'a', 'b': 0, 'c': True},
...     {'a': 'b', 'b': 1, 'c': False},
...     {'a': 'c', 'b': 2, 'c': True},
...     {'a': 'd', 'b': 3, 'c': True},
...     {'a': 'e', 'b': 4, 'c': True},
...     {'a': 'f', 'b': 5, 'c': True},
...     {'a': 'g', 'b': 6, 'c': False},
...     {'a': 'h', 'b': 7, 'c': True},
...     {'a': 'i', 'b': 8, 'c': False},
...     {'a': 'j', 'b': 9, 'c': True},
...
... ):
...     Test.objects.create(**params)

テンプレート

Viewから参照されるテンプレートを3つほど書きます。

a.html
<html><head><title>a view</title></head>
<body>
<table>{% for test in object_list %}
<tr><td>{{test.id}}</td><td>{{test.a}}</td></tr>
{% endfor %}</table>
</body>
</html>
b.html
<html><head><title>b</title></head>
<body>
<table>
<tr><td>id:</td><td>{{test.id}}</td></tr>
<tr><td>a:</td><td>{{test.a}}</td></tr>
<tr><td>b:</td><td>{{test.b}}</td></tr>
<tr><td>c:</td><td>{{test.c}}</td></tr>
</table>
</body>
</html>
c.html
<html><head><title>c view</title></head>
<body>
<table>{% for test in tests %}
<tr><td>{{test.id}}</td><td>{{test.a}}</td></tr>
{% endfor %}</table>
</body>
</html>

長い..準備だけで読む気なくしたらごめん

ルーティング(urls.py)

今回のViewのメインとなるルーティングです。クラスベースのViewを使うメリットの一つとして、簡単な動作であれば処理を書く必要がないということが挙げられます。
ここでいう簡単な処理とは、例えば特定のモデルを参照してテンプレートを描画するだけといったケースが考えられます。

例えば上記で定義したTestモデルを参照して、上記で書いたテンプレートを描画する例を書いてみました。

from django.conf.urls import patterns, include, url
from django.views.generic import TemplateView, ListView, DetailView
 
from testapp.models import Test
 
# レコード一覧用のビュー
class AView(ListView):
    template_name = 'a.html'
    context_object_name = 'tests'
    queryset = Test.objects.filter(id__gte=5)
 
class BView(DetailView):
    context_object_name = 'test'
    queryset = Test.objects.all()
    template_name = 'b.html'
 
class CView(TemplateView):
    template_name = 'c.html'
    # もちろん普通に処理を書くこともできる
    def get_context_data(self, **kwargs):
        return {
            'tests': Test.objects.filter(c=True)
        }
 
# as_viewで関数を渡す
urlpatterns = patterns('',
    url(r'^a/$', AView.as_view()),
    url(r'^b/(?P<pk>\d+)$', BView.as_view()),
    url(r'^c/$', CView.as_view()),
)

ListView

ListViewはレコード一覧を表現するためのViewクラスです。
特定のモデルをクエリセットとして受け取ります。テンプレートからは「object_list」という名前(デフォルト)のほか、「context_object_name」で指定した名前でも参照できます。
modelかquerysetのどちらかの要素が必須で、指定しないと以下のようなエラーが発生します。

ImproperlyConfigured at /a/
AView is missing a QuerySet. Define AView.model, AView.queryset, or override AView.get_queryset().

modelだけを指定した場合はすべてのレコードが抽出されテンプレートに渡されるようです。

DetailView

DetailViewはレコード詳細を表現するためのViewクラスです。
DetailViewを使用する際はurl中で「pk」を抽出するような正規表現を記述する必要があります。
抽出したpk(プライマリキー)によってレコードを一意に特定します。
ListViewと同じようにmodelかquerysetのどちらかが必須です。
querysetを指定した場合は指定したクエリセットからレコードを一意に絞り込むという動作になるようです。

TemplateView

TemplateViewは前2つのViewと違いモデルに依存しないため汎用的に使うことができます。
ただListViewやDetailViewと違い自動的にテンプレートにパラメータが渡されないことに注意しましょう。

上記の例で使っているものの中で特別な意味を持つクラス属性は以下です。

template_name 使用するテンプレート
model 使用するモデル
context_object_name テンプレート内でレコードを参照する際の変数名
queryset レコードの検索条件

get_context_data

get_context_dataも特別な意味を持つ属性です。
パラメータを渡すには「get_context_data」というインスタンスメソッドでパラメータを返却します。
これはTemplateViewだけでなくListViewやDetailViewでも使用可能です。返却値は単なる辞書でもいいのですが、テンプレートで使用できるパラメータがその場で生成したものに限られてしまいます。以下のようにすれば、querysetなど自動的にパラメータが指定される場合でも同時に利用することができます。contextは辞書と同じように代入可能です。

def get_context_data(self, **kwargs):
    context = super(BView, self).get_context_data(**kwargs)
    context['test'] = 1
    return context

他にもget_querysetなどあるようですが、あとは自分で調べてください。(投げやり

定義の省略

簡単なビューの場合はいちいちクラスとして定義したくないこともあります。その場合はurl関数の中に直接書いてしまいましょう。たとえば、上記のBView(レコード詳細)は次のようにもかけます。

url('^b/(?P<pk>\d+)$', DetailView.as_view(
    context_object_name='test',
    queryset=Test.objects.all(),
    template_name='b.html'
)),

アクセスしてみる

それぞれのURLにアクセスして表示された結果です。

http://localhost:8000/a/

5	e
6	f
7	g
8	h
9	i
10	j

http://localhost:8000/b/1

id:	1
a:	a
b:	0
c:	True

http://localhost:8000/c/

1	a
3	c
4	d
5	e
6	f
8	h
10	j

HTTPメソッドで処理を分けたい

HTTPメソッドのタイプによって処理を分けたい場合、同じ名前(小文字)のメソッドを定義します。

class DView(TemplateView):
    template_name = 'd.html'
 
    # GETリクエスト時の処理内容
    def get(self, request):
        return self.render_to_response({'tests': Test.objects.filter(c=True)})
 
    # POSTリクエスト時の処理内容
    def post(self, request):
        return self.render_to_response({'tests': Test.objects.filter(c=False)})

これらのメソッドはHttpResponseの返却を期待されていて、コンテキスト(描画パラメータ)を返却するわけではないということに注意しましょう。ショートカットしてrender_to_responseというインスタンスメソッドが用意されています。テンプレートはクラス変数の「tempalte_name」で自動的に設定されるため描画パラメータだけを指定すれば大丈夫です。

csrf_exempt

POSTリクエストでCSRFトークンのチェックを行いたくない時、普通のview関数であればcsrf_exemptをデコレータとして設置してると思いますが、Generic Viewではpostメソッドに設置しても意味がありません。なんでだよ
でもクラスに設置するか?といったらそうでもありません。

@csrf_exempt
class DView(TemplateView)
    def post(self):
        pass

上記のようにするとエラーが出てくれます。


'function' object has no attribute 'as_view'

csrf_exemptが修飾できるのは関数のみのようです。
じゃあどうするか。こうでしょ!

csrf_exempt(CView.as_view())

as_view()の結果(関数)をcsrf_exempt関数で括ってあげるってことですね。
なるほど、これはわからんかった。

[追記] dispatch

ジェネリックビューにデコレータを設置する場合はdispatchメソッドに対して行うそうです。
このとき、method_decoratorで対象デコレータを覆う必要があります。

from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
 
class SomeView(TemplateView):
    template_name = 'some.html'
 
    @method_decorator(csrf_exempt)
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        return super(SomeView, self).dispatch(*args, **kwargs)

複数のデコレータを設置する場合は上記のように連ねて書けば問題ありません。

参考リンク:
クラスベース汎用ビュー
Django1.3のClass-based generic viewsを使う