2018-03-10

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

DjangoのGeneric View(クラスベースのビュー)について理解したくてまとめました。

今回は割と適当です。適当というか投げやり。

warning
  • この記事は結構古いです。

準備

インストール
  • とりあえず必要なものを用意します。
  • 当たり前ですがジャンゴくんとmodelを使いたいのでsqlite3あたり。
  • $ pip install django $ sudo yum -y install sqlite3 $ django-admin startproject djangotest $ cd djangotest $ ./manage.py startapp testapp
settings.py
  • settings にはこんな感じのことを追記。

  • 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)
テンプレート
  • 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>

実装

クラスベースのViewを使うメリットの一つとして、 簡単な動作であれば処理を書く必要がないということが挙げられます。

ここでいう簡単な処理とは、例えば特定のモデルを参照してテンプレートを描画するだけといったケースです。 例えば上記で定義したTestモデルを参照して、上記で書いたテンプレートを描画する例を書いてみました。

  • views.py
  • urls.py
  • 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にアクセスして表示された結果です。

/a/
  • ListViewの結果

    5 e 6 f 7 g 8 h 9 i 10 j
/b/1
  • DetailViewの結果

    id: 1 a: a b: 0 c: True
/c/
  • TemplateViewの結果

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

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

実は前のセクションで利用した ListView, DetailView, TemplateView は View という基底クラスから作られており、 View 自体を使うこともできます。 というか上記3つのビューで対応できないような複雑なビューを定義する場合は View を継承して自分でビューを書くことになるでしょう。

この View はHTTPメソッドに対応したメソッド(小文字)を持つため、 GET/POSTで処理を分けたい場合であっても関数ビューのように request.method によって分岐する必要がありません。

実際に書いてみましょう。

from django.http import HttpResponse from django.views import View class DView(View): # GETリクエスト時の処理内容 def get(self, request): return HttpResponse("get response") # POSTリクエスト時の処理内容 def post(self, request): return HttpResponse("post response")

これらのメソッドはHttpResponseの返却を期待されていて、 コンテキスト(描画パラメータ)を返却するわけではないということに注意しましょう。

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 View from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt class SomeView(View): @method_decorator(csrf_exempt) @method_decorator(login_required) def dispatch(self, *args, **kwargs): return super(SomeView, self).dispatch(*args, **kwargs)

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

参考