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

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

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

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

モデル

適当に定義。

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\d+)$', BView.as_view()),
    url(r'^c/$', CView.as_view()),
)

ListView

ListViewはレコード一覧を表現するためのViewクラスです。

特定のモデルをクエリセットとして受け取ります。 テンプレートからは object_list という名前(デフォルト)のほか、 context_object_name で指定した名前でも参照できます。

modelqueryset のどちらかの要素が必須で、指定しないと以下のようなエラーが発生します。

Error

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と同じように modelqueryset のどちらかが 必須 です。

queryset を指定した場合は指定したクエリセットからレコードを一意に絞り込むという動作になるようです。

TemplateView

TemplateViewは前2つのViewと違いモデルに依存しないため汎用的に使うことができます。

ただListViewやDetailViewと違い自動的にテンプレートにパラメータが渡されないことに注意しましょう。

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

template_name

使用するテンプレート

model

使用するモデル

context_object_name

テンプレート内でレコードを参照する際の変数名

queryset

レコードの検索条件

get_context_data

get_context_dataも特別な意味を持つ属性です。

パラメータを渡すには get_context_data というインスタンスメソッドでパラメータを返却します。

これは TemplateView だけでなく ListViewDetailView でも使用可能です。 返却値は単なる辞書でもいいのですが、テンプレートで使用できるパラメータがその場で生成したものに限られてしまいます。

以下のようにすれば、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\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をデコレータとして設置してると思いますが GenericView では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)

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

参考