Djangoの 汎用クラスビューをまとめて、実装について言及する


この記事を書こうと思った経緯


  • 現在webアプリ開発を行うにあたって、Djangoを選択する場合、クラスベースビューを使うことができる

  • クラスベースビューの公式ドキュメントからだとどの汎用ビューをどんなときに使えばいいか理解しづらい


    • ちなみに多くの説明はこの公式ドキュメントを参考に書いているが、筆者の独断で、理解に必要な部分を付け足し、なくても良いと思ったところはばっさりカットしている



  • 公式ドキュメントやQiita上の日本語ドキュメントも参考にはなるが、もう少し自由に実装しようとすると、Djangoのコードを読まなければ理解しづらい部分がある

  • 今後同じような開発を行うエンジニアの助けになればと思いこの記事を書く


この記事の対象者


  • DjangoのMTVについてちょっとでも理解のある方

  • Djangoを利用するにあたって、複数あるクラスベース汎用ビューの何を選択すればいいか手っ取り早く知りたい方

  • クラスベース汎用ビューの実装に興味がある方


この記事の読み方(書かれ方)


  • 6種類のクラスベース汎用ビューについてそれぞれ以下のことを書く。


    1. どんなものであるか、どんなときに使うのか

    2. どうやって実装されているかコードをみながら確認



  • なお、すでにQiita上にわかりやすい記事のある項目については引用させていただく

  • 私もDjango完全に理解した、とは程遠い人間ですので間違いや正しくない表現等あれば教えて下さい


そもそもクラスベース汎用ビューとはなにか

Djangoにおけるクラスベース汎用ビューの入門と使い方サンプルの記事に詳しく書いてあるので参照ください


1. それぞれのクラスベース汎用ビューがどんなものであるか、どんなときに使うのか


すべてのもととなる汎用ビュー


genelic.base.View


  • 次項から解説する汎用ビューのように用途別のものではない

  • このclassを継承して実装することで一番自由度高くコードが書ける。Djangoを使いつつ、自由に書きたい方向け


表示系汎用ビューの3つの使い分け


genelic.base.TemplateView


  • 単にTemplateをレンダリングし表示する場合に利用する


genelic.list.ListView


  • オブジェクトのリストを表示するためのビュー

  • オブジェクトのリストには通常、指定した一つのmodel_class(DB上の1テーブル)の情報を保持する


genelic.detail.DetailView


  • オブジェクトのdetailリストを表示するためのビュー

  • このリストには通常、指定したmodel_classの中の一つのオブジェクト情報を保持する。


    • urls.pyにて<pk>あるいは<slug>を指定することで、指定したmodel_classの中から一つのオブジェクトのみをfilterする




使い分けについて


  • 理解のために簡単なブログ記事を例に挙げ説明する

  • ブログ記事のトップページ(/toppage)には "ようこそhogefugaブログへ!" という文字列と静的な各項目へのlinkのみがあるとする


    • この場合、利用するのはTemplateViewになる



  • ブログ記事の記事一覧ページ(/articles)には、各記事の名前と投稿日がリストで表示される


    • この場合、利用するのはListViewになる

    • 各記事の名前と投稿日を含むdictをlistでとってきて表示している



  • それぞれのブログ記事ページ(/articles/1)には、各記事の内容などの詳細が表示される


    • この場合、利用するのはDetailViewになる

    • pk=1の記事の情報をlistでとってきて表示している




更新系汎用ビュー2つの使い分け


genelic.edit.FormView


  • Viewは、HTTP method GETを受けたときにformを表示する

  • Viewは、HTTP method POSTを受けたときにリクエストのformに入っているものを受け取り、操作を実行できる(通常はリダイレクト)


    • エラー時には、バリデーションエラーとともにformを再描画する。




genelic.edit.CreateView


  • Viewは、HTTP method GETを受けたときにformを表示する

  • Viewは、HTTP method POSTを受けたときにリクエストのformに入っているものを受け取り、指定したmodelにそのままinsert処理を実行する


    • formはmodel_formという各model_classに沿った形のformを利用する

    • エラー時には、バリデーションエラーとともにformを再描画する




使い分けについて


  • FormViewとCreateViewはバリデーションの後、(validであれば)ともにform_validというメソッドが呼ばれるのだが、その時の挙動が、前者はリダイレクトで後者がmodelへのsave(insert)である

  • つまり


    • formを表示し、POSTで受け取ったあとにDBへのinsertがしたければCreateViewを使う

    • formを表示し、POSTで受け取ったあとにDBへのinsertをせずリダイレクトをしたければFormViewを使う


      • (ちなみに筆者自身はFormViewの使いみち思いついたことがなく使ったことが今の所ない)

      • (上記を満たす一番わかり易いものはlogin画面だが、それにはLoginViewという個別のものがある。(なお、本記事では詳細を記載しない))






2. それぞれのクラスベース汎用ビューがどうやって実装されているかコードをみながら確認

さて、ここからは実際にDjangoの実装をみていき、それぞれがどんな様相で書かれているかを確認する


genelic.base.View


.as_view()


  • コードをコピペする。流れは次の通り


    1. as_view()が受け取った引数が正しいかチェック

    2. 上記引数とインスタンスのrequestなどをあわせてself.dispatch()に投げる

    3. requestのHTTP method がself.http_method_namesに含まれているかを確認


      • コピペした範囲にないが http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] とクラス変数で宣言されている



    4. HTTP methodに対応した名前の関数を呼ぶ(GETならdef get, POSTなら def postといった通り )




View_class

    def as_view(cls, **initkwargs):

"""Main entry point for a request-response process."""
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))

def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.request = request
self.args = args
self.kwargs = kwargs
return self.dispatch(request, *args, **kwargs)
view.view_class = cls
view.view_initkwargs = initkwargs

# take name and docstring from class
update_wrapper(view, cls, updated=())

# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view

def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs



genelic.base.TemplateView


  • コードはここ

  • 流れは以下


    1. as_view()で呼ばれて、HTTP methodがGETの場合、TemplateViewのdef getが呼ばれる

    2. ContentMixin中のget_context_dataが呼ばれ、contextが生成される

    3. TemplateResponseMixin中のrender_to_responseが呼ばれる

    4. インスタンスのrequest, 上記のcontextとともにself.get_template_names()で呼んだtemplate_nameがdjango.template.response.TemplateResponseに渡されたものがreturnする




TemplateView

class TemplateView(TemplateResponseMixin, ContextMixin, View):

"""
Render a template. Pass keyword arguments from the URLconf to the context.
"""

def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)


TemplateResponseMixin

class TemplateResponseMixin:

"""A mixin that can be used to render a template."""
template_name = None
template_engine = None
response_class = TemplateResponse
content_type = None

def render_to_response(self, context, **response_kwargs):
"""
Return a response, using the `response_class` for this view, with a
template rendered with the given context.
Pass response_kwargs to the constructor of the response class.
"""

response_kwargs.setdefault('content_type', self.content_type)
return self.response_class(
request=self.request,
template=self.get_template_names(),
context=context,
using=self.template_engine,
**response_kwargs
)

def get_template_names(self):
"""
Return a list of template names to be used for the request. Must return
a list. May not be called if render_to_response() is overridden.
"""

if self.template_name is None:
raise ImproperlyConfigured(
"TemplateResponseMixin requires either a definition of "
"'template_name' or an implementation of 'get_template_names()'")
else:
return [self.template_name]



  • genelic.base.View以外のクラスベース汎用ビューでは複数のクラスを継承あるいは多重継承しており、複雑になってくる

  • TemplateViewを利用したViewではtemplate_nameの変数を宣言することが必須


genelic.list.ListView


  • コードはここ

  • ListViewクラス本体には何も記述されておらずクラスを継承するだけのクラスとなっておりいよいよ複雑化してくる

  • 流れは以下


    1. as_view()で呼ばれて、HTTP methodがGETの場合、BaseListViewのdef getが呼ばれる


    2. MultipleObjectMixinget_queryset()が呼ばれてqueryset = self.model._default_manager.all()がself.object_listに格納される


    3. MultipleObjectMixin中のget_context_data()が呼ばれ,querysetをもとにcontextが生成される


    4. TemplateResponseMixin中のrender_to_response()が呼ばれる


    5. MultipleObjectTemplateResponseMixin中のget_template_names()が呼ばれる


      • template_nameが存在すればtemplate_nameはそれになる

      • template_nameがない場合, self.model._meta.model_name(model classの小文字)+_list.htmlが存在すればそれがtemplate_nameとなる



    6. インスタンスのrequest, 上記のcontext,template_nameがdjango.template.response.TemplateResponseに渡されたものがreturnする




MultipleObjectMixin_class

    def get_queryset(self):

"""
Return the list of items for this view.
The return value must be an iterable and may be an instance of
`QuerySet` in which case `QuerySet` specific behavior will be enabled.
"""

if self.queryset is not None:
queryset = self.queryset
if isinstance(queryset, QuerySet):
queryset = queryset.all()
elif self.model is not None:
queryset = self.model._default_manager.all()
else:
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
ordering = self.get_ordering()
if ordering:
if isinstance(ordering, str):
ordering = (ordering,)
queryset = queryset.order_by(*ordering)

return queryset



MultipleObjectMixin_class

    def get_context_data(self, *, object_list=None, **kwargs):

"""Get the context for this view."""
queryset = object_list if object_list is not None else self.object_list
page_size = self.get_paginate_by(queryset)
context_object_name = self.get_context_object_name(queryset)
if page_size:
paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
context = {
'paginator': paginator,
'page_obj': page,
'is_paginated': is_paginated,
'object_list': queryset
}
else:
context = {
'paginator': None,
'page_obj': None,
'is_paginated': False,
'object_list': queryset
}
if context_object_name is not None:
context[context_object_name] = queryset
context.update(kwargs)
return super().get_context_data(**context)


MultipleObjectTemplateResponseMixin_class

    def get_template_names(self):

"""
Return a list of template names to be used for the request. Must return
a list. May not be called if render_to_response is overridden.
"""

try:
names = super().get_template_names()
except ImproperlyConfigured:
# If template_name isn't specified, it's not a problem --
# we just start with an empty list.
names = []

# If the list is a queryset, we'll invent a template name based on the
# app and model name. This name gets put at the end of the template
# name list so that user-supplied names override the automatically-
# generated ones.
if hasattr(self.object_list, 'model'):
opts = self.object_list.model._meta
names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix))
elif not names:
raise ImproperlyConfigured(
"%(cls)s requires either a 'template_name' attribute "
"or a get_queryset() method that returns a QuerySet." % {
'cls': self.__class__.__name__,
}
)
return names



  • ListViewを利用したViewでは



    • template_nameの変数を宣言することは任意


    • modelの変数を宣言することが必須




genelic.detail.DetailView


  • コードはここ


  • ListViewのmodelをfilterした版であるため、異なる部分だけを記述する


    • ListViewではMultipleObjectMixinを継承していたが、DetailViewではSingleObjectMixinを継承している




  • SingleObjectMixinget_queryset()が呼ばれてself.objectに代入されるところの解説


    • urls.py中に<pk>あるいは<slug>があった場合


      • pkがあった場合はqueryset = self.model_default_manager.all()に対して、queryset.filter(pk=pk).get()となる

      • slugがあった場合はqueryset.filter(**{slug_field: slug}).get()となる


        • slug_fieldはViewのクラス変数として宣言可能でデフォルトは'slug_field'である








SingleObjectMixin

class SingleObjectMixin(ContextMixin):

"""
Provide the ability to retrieve a single object for further manipulation.
"""

model = None
queryset = None
slug_field = 'slug'
context_object_name = None
slug_url_kwarg = 'slug'
pk_url_kwarg = 'pk'
query_pk_and_slug = False

def get_object(self, queryset=None):
"""
Return the object the view is displaying.
Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
Subclasses can override this to return any object.
"""

# Use a custom queryset if provided; this is required for subclasses
# like DateDetailView
if queryset is None:
queryset = self.get_queryset()

# Next, try looking up by primary key.
pk = self.kwargs.get(self.pk_url_kwarg)
slug = self.kwargs.get(self.slug_url_kwarg)
if pk is not None:
queryset = queryset.filter(pk=pk)

# Next, try looking up by slug.
if slug is not None and (pk is None or self.query_pk_and_slug):
slug_field = self.get_slug_field()
queryset = queryset.filter(**{slug_field: slug})

# If none of those are defined, it's an error.
if pk is None and slug is None:
raise AttributeError(
"Generic detail view %s must be called with either an object "
"pk or a slug in the URLconf." % self.__class__.__name__
)

try:
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name})
return obj

def get_queryset(self):
"""
Return the `QuerySet` that will be used to look up the object.
This method is called by the default implementation of get_object() and
may not be called if get_object() is overridden.
"""

if self.queryset is None:
if self.model:
return self.model._default_manager.all()
else:
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
return self.queryset.all()



  • DetailViewを利用したViewでは



    • template_nameの変数を宣言することは任意


    • modelの変数を宣言することが必須

    • urls.py中に<pk>か<slug>が必須

    • slugを使う場合、多くの場面でslug_fieldを宣言することになる




genelic.edit.FormView


  • コードはここ


    • 今までのクラスと違うのは主にFormMixinなので主にそこを解説する



  • 流れ(HTTP GET)



    1. as_view()で呼ばれて、HTTP methodがGETの場合、ProcessFormViewのdef getが呼ばれる


    2. FormMixin中のget_context_data()が呼ばれ、インスタンス変数のform_classがkwargs['form']に格納される

    3. 今までと同様、templateをreturnする



  • 流れ(HTTP POST)


    1. as_view()で呼ばれて、HTTP methodがGETの場合、ProcessFormViewのdef postが呼ばれる


    2. FormMixinget_form()が呼ばれ、値の挿入されたformを入手


    3. form.is_valid()でform classで定義されたvalidationの判定

    4. validであればFormMixinform_valid(form)が呼ばれ, インスタンス変数のsuccess_urlにリダイレクトされる

    5. invlidであればエラーと一緒にGETにリダイレクトされる




FormMixin

class FormMixin(ContextMixin):

"""Provide a way to show and handle a form in a request."""
initial = {}
form_class = None
success_url = None
prefix = None

def get_initial(self):
"""Return the initial data to use for forms on this view."""
return self.initial.copy()

def get_prefix(self):
"""Return the prefix to use for forms."""
return self.prefix

def get_form_class(self):
"""Return the form class to use."""
return self.form_class

def get_form(self, form_class=None):
"""Return an instance of the form to be used in this view."""
if form_class is None:
form_class = self.get_form_class()
return form_class(**self.get_form_kwargs())

def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
kwargs = {
'initial': self.get_initial(),
'prefix': self.get_prefix(),
}

if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
return kwargs

def get_success_url(self):
"""Return the URL to redirect to after processing a valid form."""
if not self.success_url:
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
return str(self.success_url) # success_url may be lazy

def form_valid(self, form):
"""If the form is valid, redirect to the supplied URL."""
return HttpResponseRedirect(self.get_success_url())

def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
return self.render_to_response(self.get_context_data(form=form))

def get_context_data(self, **kwargs):
"""Insert the form into the context dict."""
if 'form' not in kwargs:
kwargs['form'] = self.get_form()
return super().get_context_data(**kwargs)



  • FormViewを利用したViewでは



    • template_nameの変数を宣言することは任意


    • form_classを宣言するのは必須ではないが、これを使わないとFormViewを使う意味がない




genelic.edit.CreateView


  • コードはここ

  • ほとんどFormViewと一緒なため、違いのあるModelFormMixinについてのみ書く


  • ModelFormMixinFormMixinを継承している


    • CreateViewがModelFormMixinを利用することでFormViewと変わるのはform_valid時にリダイレクトではなくsave()されることである



class ModelFormMixin(FormMixin, SingleObjectMixin):

"""Provide a way to show and handle a ModelForm in a request."""
fields = None

def get_form_class(self):
"""Return the form class to use in this view."""
if self.fields is not None and self.form_class:
raise ImproperlyConfigured(
"Specifying both 'fields' and 'form_class' is not permitted."
)
if self.form_class:
return self.form_class
else:
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif getattr(self, 'object', None) is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
else:
# Try to get a queryset and extract the model class
# from that
model = self.get_queryset().model

if self.fields is None:
raise ImproperlyConfigured(
"Using ModelFormMixin (base class of %s) without "
"the 'fields' attribute is prohibited." % self.__class__.__name__
)

return model_forms.modelform_factory(model, fields=self.fields)

def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
kwargs = super().get_form_kwargs()
if hasattr(self, 'object'):
kwargs.update({'instance': self.object})
return kwargs

def get_success_url(self):
"""Return the URL to redirect to after processing a valid form."""
if self.success_url:
url = self.success_url.format(**self.object.__dict__)
else:
try:
url = self.object.get_absolute_url()
except AttributeError:
raise ImproperlyConfigured(
"No URL to redirect to. Either provide a url or define"
" a get_absolute_url method on the Model.")
return url

def form_valid(self, form):
"""If the form is valid, save the associated model."""
self.object = form.save()
return super().form_valid(form)


  • CreateViewを利用したViewでは



    • template_nameの変数を宣言することは任意


    • form_classを宣言するのは任意



      • form_classを宣言しなければ、指定したmodelを使っているmodel_formが使われる






あとがき


  • かなり長い記事を読んでくださってありがとうございます。

  • 実際にはクラスベース汎用ビューにももっとたくさん種類があるのですが、すべてやるとあまりにも冗長だと思ったので重要そうなものを6種類とりあげました。


    • この記事で書いてあるようなことが分かれば、他の汎用ビューで躓いてもコードを読んで解決できるはずです



  • 本当はMixinをうまく使って汎用ビューを組み合わせる方法とそのメリット・デメリットみたいな話も書きたかったのですが、長くなりそうなのでいつかの機会にとっておきます。

  • 企業のアドベントカレンダーじゃなくてDjangoアドベントカレンダーでやれよという声が聞こえます、すいません。


参考