クラスベースのビューでミックスイン (mixin) を使用する¶
注意
これは発展的なトピックです。これらのテクニックについて詳しく読む前に、Django のクラスベースビュー で動作のしくみを知っておくことをおすすめします。
Django のビルトインのクラスベースビューではたくさんの機能が準備されていますが、個別に使いたい機能もあるかもしれません。たとえば、HTTP レスポンスを生成するテンプレートをレンダリングするビューを記述したいとき、TemplateView
は使えない状況もあります; POST
ではテンプレートをレンダリングするだけで、GET
のときはまったく異なる処理がしたいときなどです。この場合、TemplateResponse
を直接使えますが、コードが重複する結果となってしまいます。
この理由から、Django は個別の機能を提供する多くの mixin を用意しています。たとえば、テンプレートのレンダリングは TemplateResponseMixin
でカプセル化されています。Django のリファレンスドキュメントには すべてのミックスインの完全なドキュメント があります。
コンテキストとテンプレートのレスポンス¶
2 つの中心的なミックスイン (mixin) が用意されており、クラスベースビュー内のテンプレートを扱うインターフェースに一貫性を保ちやすくなっています。
TemplateResponseMixin
TemplateResponse
を返すビルトインビューは全て、TemplateResponseMixin
が提供するrender_to_response()
メソッドを呼び出します。ほとんどの場合、これはあなたによって呼び出されます(たとえば、TemplateView
とDetailView
)の両方で実装されているget()
メソッドによって)。同様に、これをオーバーライドする必要はほとんどありませんが、Django テンプレートでレンダリングされていないものを返したい場合は、オーバーライドする必要があるでしょう。この例については、 JSONResponseMixin の例 を参照してください。render_to_response()
自体はget_template_names()
を呼び出しますが、デフォルトではクラスベースのビューでtemplate_name
を検索します。他の2つのミックスイン (SingleObjectTemplateResponseMixin
とMultipleObjectTemplateResponseMixin
) は、実際のオブジェクトを扱うときに、より柔軟なデフォルトを提供するためにこれをオーバーライドします。ContextMixin
コンテキストデータが必要なすべての組み込みビュー、たとえばテンプレート (上述の
TemplateResponseMixin
を含む) をレンダリングするには、get_context_data()
を呼び出し、そこに含めたいデータをキーワード引数として渡してください。get_context_data()
は辞書を返します。ContextMixin
ではキーワード引数を返しますが、辞書にさらにメンバーを追加するためにこれをオーバーライドすることはよく行われます。また、extra_context
属性も使用できます。
Django の一般的なクラスベースのビューを構築する¶
Djangoのクラスベースのジェネリックビューが、個々の機能を提供するミックスインからどのように構築されているか見てみましょう。オブジェクトの「詳細」ビューをレンダリングする DetailView
と、クエリセットから典型的にオブジェクトのリストをレンダリングし、オプションでページ分割する ListView
を考えます。これにより、単一のDjangoオブジェクトや複数のオブジェクトを扱う際に便利な機能を提供する4つのミックスインについて紹介します。
ジェネリック編集ビュー (FormView
と、モデル固有のビュー CreateView
, UpdateView
, DeleteView
) と日付ベースのジェネリックビューにもミックスインがあります。これらは ミックスインのリファレンス で紹介されています。
DetailView
: Django の1つのオブジェクトを対象とするビュー¶
オブジェクトの詳細を表示するためには、基本的に2つの作業が必要です。まずはオブジェクトを検索し、それから適切なテンプレートと、そのオブジェクトをコンテキストとして、 TemplateResponse
を作成します。
オブジェクトを取得するために、 DetailView
は SingleObjectMixin
に依存しています。このクラスではリクエストURLに基づいてオブジェクトを見つけ出すメソッド get_object()
を提供しています(このメソッドでは URLConfで宣言されている pk
および slug
キーワード引数を検索し、ビューの model
属性、あるいは提供されている場合は queryset
属性からオブジェクトを検索します)。 SingleObjectMixin
はまた get_context_data()
メソッドも提供しています。このメソッドでは、テンプレートレンダーに渡すコンテキストデータを提供するために、Django組み込みのクラスベースビュー全てで使われています。
TemplateResponse
を作成するために、DetailView
は SingleObjectTemplateResponseMixin
を使用します。これは TemplateResponseMixin
を拡張し、上記のように get_template_names()
をオーバーライドします。実際にはかなり洗練されたオプションのセットを提供していますが、ほとんどの人が使用する主なものは <app_label>/<model_name>_detail.html
です。_detail
部分は、サブクラス上の template_name_suffix
を別のものに設定することで変更できます。(例えば、ジェネリック編集ビュー は、作成および更新ビューに _form
を、削除ビューには _confirm_delete
を使用します。)
ListView
: Django の複数のオブジェクトを対象とするビュー¶
オブジェクトのリストもおおよそ同じパターンに従います。オブジェクトの (おそらくページ分割された) リスト、典型的には QuerySet
が必要で、次にそのオブジェクトのリストを使って、適切なテンプレートで TemplateResponse
を作る必要があります。
オブジェクトを取得するために、 ListView
は MultipleObjectMixin
を使用します。このミックスインは、 get_queryset()
と paginate_queryset()
の両方を提供します。 SingleObjectMixin
とは異なり、URL の一部をキーにしてクエリセットを特定する必要はありません。そのため、デフォルトではビュークラスの queryset
または model
属性が使用されます。ここで get_queryset()
をオーバーライドする一般的な理由は、現在のユーザーに依存するなど、オブジェクトを動的に変化させることで、ブログの場合は将来の投稿を除外することなどが理由となるでしょう。
MultipleObjectMixin
も get_context_data()
をオーバーライドして、ページ分割に適切なコンテキスト変数を含めます(ページ分割が無効な場合はダミーを提供します)。これは object_list
がキーワード引数として渡される動作に依存しており、 ListView
がそれを調整します。
TemplateResponse
を作成するために、 ListView
は MultipleObjectTemplateResponseMixin
を使用します。上記の SingleObjectTemplateResponseMixin
と同様に、これは get_template_names()
をオーバーライドして、さまざまなオプション
を提供します。最もよく使われるのは <app_label>/<model_name>_list.html
で、_list
の部分は template_name_suffix
属性から取得されます。(日付ベースのジェネリックビューでは、 _archive
や _archive_year
などの接尾辞を使用して、様々な用途に特化した日付ベースのリストビューで、異なるテンプレートを使用します)
Django のクラスベースのビューのミックスイン (mixin) を使用する¶
Django のクラスベースのジェネリックビューが、提供されたミックスインをどのように使うか見てきたので、それらを組み合わせる他の方法を見てみましょう。組み込みのクラスベースのビューや、他のクラスベースのジェネリックビューと組み合わせることに変わりはありませんが、Django を「箱から出してすぐに」使えるものよりも、もっとレアな問題をカバーしています。
警告
すべてのミックスインを一緒に使用することができるわけではなく、すべてのクラスベースのジェネリックビューをすべての他のミックスインと一緒に使用することもできません。ここではいくつかの動作する例を紹介します。他の機能を組み合わせる場合は、使用するさまざまなクラス間で重複する属性やメソッドの相互作用、および method resolution order (メソッド解決順序: MRO)が、どのバージョンのメソッドがどんな順番で呼び出されるかに影響することを考慮する必要があります。
Django の クラスベースビュー および クラスベースビュー ミックスイン のリファレンスドキュメントは、異なるクラスやミックスイン間でよく競合を引き起こす属性やメソッドを理解するのに役立ちます。
もし迷ったら、 View
や TemplateView
をベースにして、 SingleObjectMixin
や MultipleObjectMixin
を使うのが良いでしょう。おそらく、より多くのコードを書くことになるでしょうが、後でそのコードに辿り着いた他の誰かが明確に理解できる可能性が高くなります。(もちろん、Django のクラスベースのジェネリックビューの実装を見て、問題への取り組み方のヒントを得ることもできます)。
ビューで SingleObjectMixin
を使用する¶
もし POST
にのみ反応するクラスベースのビューを書きたいのであれば、 View
をサブクラス化し、その中に post()
メソッドを書きます。しかし、URLから特定したオブジェクトに対して処理を行いたい場合、 SingleObjectMixin
が提供する機能が必要になります。
クラスベースのジェネリックビューのイントロダクション で使用した Author
モデルを使って、このデモンストレーションを行います。
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.urls import reverse
from django.views import View
from django.views.generic.detail import SingleObjectMixin
from books.models import Author
class RecordInterestView(SingleObjectMixin, View):
"""Records the current user's interest in an author."""
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
# Look up the author we're interested in.
self.object = self.get_object()
# Actually record interest somehow here!
return HttpResponseRedirect(
reverse("author-detail", kwargs={"pk": self.object.pk})
)
実際には、リレーショナルデータベースではなく、キーバリューストアに関心事を記録したいと思うでしょう。その部分は省略しました。 SingleObjectMixin
を使用するビューで興味があるのは作者を調べるところだけで、これは self.get_object()
を呼び出すことで行います。それ以外は全てミックスインが行ってくれます。
これをURLにフックするのは簡単です:
from django.urls import path
from books.views import RecordInterestView
urlpatterns = [
# ...
path(
"author/<int:pk>/interest/",
RecordInterestView.as_view(),
name="author-interest",
),
]
pk
という名前のグループに注目してください。 get_object()
は Author
のインスタンスを検索するために使用します。スラグを使うこともできますし、 SingleObjectMixin
の他の機能を使うこともできます。
ListView
で SingleObjectMixin
を使用する¶
ListView
は組み込みのページ分割を提供しますが、別のオブジェクトに(外部キーで)リンクされたオブジェクトのリストをページ分割したいかもしれません。出版の例では、特定の出版社のすべての本をページ分割したいかもしれません。
これを行う1つの方法は、 ListView
と SingleObjectMixin
を組み合わせることで、ページ分割された本のリストのクエリセットを、見つかった出版社から単一のオブジェクトとしてぶら下げることができます。これを行うには、2つの異なるクエリセットを用意する必要があります:
ListView
で使用するためのBook
の クエリセットリストアップしたい本の
Publisher
にアクセスできるので、get_queryset()
をオーバーライドして、Publisher
の 逆方向の外部キーマネージャ を使用します。get_object()
で使用するPublisher
クエリセットです。正しい
Publisher
オブジェクトを取得するには、デフォルトのget_object()
の実装に頼りましょう。ただし、明示的にqueryset
引数を渡さなければなりません。なぜなら、デフォルトのget_object()
の実装ではget_queryset()
を呼び出しますが、これはPublisher
の代わりにBook
オブジェクトを返すようにオーバーライドしているからです。
注釈
get_context_data()
については慎重に考える必要があります。 context_object_name
がセットされている場合、 SingleObjectMixin
と ListView
の両方がその値のもとのコンテキストデータに格納されます。そのため、代わりに Publisher
が明示的にコンテキストデータに含まれるようにする必要があります。 ListView
は super()
を呼び出すことを忘れなければ、適切な page_obj
と paginator
を自動で追加してくれます。
これで新しい PublisherDetailView
を書くことができます:
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from books.models import Publisher
class PublisherDetailView(SingleObjectMixin, ListView):
paginate_by = 2
template_name = "books/publisher_detail.html"
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=Publisher.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["publisher"] = self.object
return context
def get_queryset(self):
return self.object.book_set.all()
get()
内で self.object
を設定していることに注目してください。これにより、後で get_context_data()
や get_queryset()
で再利用できます。 template_name
を指定しない場合、テンプレートは通常の ListView
の選択肢にデフォルトで設定されます。この場合、それは "books/book_list.html"
になります。なぜなら、これは本のリストだからです。 ListView
は SingleObjectMixin
について何も知らないため、このビューが Publisher
と関連していることを全く理解していません。
この例では paginate_by
を意図的に小さくしているので、ページ分割の動作を確認するためにたくさんのブックを作成する必要はありません!以下が使いたいテンプレートです:
{% extends "base.html" %}
{% block content %}
<h2>Publisher {{ publisher.name }}</h2>
<ol>
{% for book in page_obj %}
<li>{{ book.title }}</li>
{% endfor %}
</ol>
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endblock %}
これより複雑なことはやめておきましょう¶
一般的に必要な機能は、 TemplateResponseMixin
や SingleObjectMixin
で実現できます。上記のように、少し注意すれば、SingleObjectMixin
を ListView
と組み合わせることもできます。しかし、そうするとますます複雑になります。適切な判断基準は以下の通りです:
ヒント
各ビューでは、 detail, list, editing, date のようなクラスベースのジェネリックビューのグループのミックスイン/ビューのうち、それぞれ1つだけを使うようにしてください。たとえば、 TemplateView
(組み込みのビュー) と MultipleObjectMixin
(ジェネリックリスト) の組み合わせは問題ありませんが、 SingleObjectMixin
(ジェネリック詳細ビュー) と MultipleObjectMixin
(ジェネリックリストビュー) の組み合わせは問題が発生しやすいでしょう。
より洗練されたものにしようとするとどうなるかを示すために、より単純な解決策がある場合に可読性と保守性を犠牲にする例を示します。まず、 DetailView
と FormMixin
を組み合わせて、 DetailView
を使ってオブジェクトを表示しているのと同じ URL に Django Form
を POST
できるようにしようという素朴な試みを見てみましょう。
DetailView
で FormMixin
を使用する¶
先ほどの View
と SingleObjectMixin
を一緒に使う例を思い出してください。ユーザが特定の作者に興味を持ったことを記録していました。ここで、なぜその作者が好きなのか、メッセージを残させたいとします。ここではリレーショナルデータベースではなく、より難解な方法で情報を保存することになると仮定しましょう。その詳細についてはここでは考慮しません。
この時点で、ユーザのブラウザから Django に送られる情報をカプセル化する Form
に手を伸ばすのは自然なことです。また、私たちは REST に重きを置いているので、ユーザからのメッセージを取得するのと同じように、作者を表示するのにも同じ URL を使いたいとします。そのために AuthorDetailView
を書き換えてみましょう。
GET
の処理は DetailView
から引き継ぎますが、コンテキストデータに Form
を追加して、テンプレートでレンダリングできるようにします。また、 FormMixin
からフォーム処理を取り込みます。そして、 POST
時にフォームが適切に呼び出されるようにコードを少し書きましょう。
注釈
FormMixin
を使用し、自分で post()
を実装します。 FormView
(すでに適切な post()
を提供している)と DetailView
は、両方のビューが get()
を実装しているため、混在させるとより混乱する可能性があるからです。
新しい AuthorDetailView
は次のようになります:
# CAUTION: you almost certainly do not want to do this.
# It is provided as part of a discussion of problems you can
# run into when combining different generic class-based view
# functionality that is not designed to be used together.
from django import forms
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import FormMixin
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
# Here, we would record the user's interest using the message
# passed in form.cleaned_data['message']
return super().form_valid(form)
get_success_url()
はリダイレクト先を提供し、デフォルト実装の form_valid()
で使用されます。前述のように、独自の post()
を用意する必要があります。
より良い解決策¶
FormMixin
と DetailView
の間の微妙な相互作用の数は、すでに私たちの管理能力を試しています。このようなクラスを自分で書こうとは思わないでしょう。
この場合、 post()
メソッドを自分で書くことはできますが、 DetailView
を唯一の汎用機能として保持し、 Form
を処理するコードを自分で書くことは多くの重複を伴います。
代わりに、フォームを処理するための別々のビューを持つ方が、上記のアプローチよりも作業量が少ないでしょう。その場合、 DetailView
とは別の FormView
を使うことになるでしょう。
もう一つのより良い解決策¶
ここで本当にやろうとしていることは、同じURLから2つの異なるクラスベースのビューを使うことです。では、なぜそうしないのでしょうか?ここでは非常に明確な区分があります。 GET
リクエストは DetailView
(コンテキストデータに Form
を追加したもの) を取得し、 POST
リクエストは FormView
を取得します。まずはこれらのビューを設定しましょう。
AuthorDetailView
ビューは、最初に紹介した AuthorDetailView とほとんど同じです。 AuthorInterestForm
をテンプレートで利用できるようにするために、独自の get_context_data()
を書く必要があります。わかりやすくするために、先ほどの get_object()
のオーバーライドは省略します:
from django import forms
from django.views.generic import DetailView
from books.models import Author
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetailView(DetailView):
model = Author
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = AuthorInterestForm()
return context
次に AuthorInterestFormView
は FormView
ですが、 SingleObjectMixin
を持ってきて、今話題にしている著者を見つけられるようにしなければなりません。また、 AuthorDetailView
が GET
で使用しているのと同じテンプレートをフォームエラーでレンダリングできるように template_name
を忘れずに指定しなければなりません:
from django.http import HttpResponseForbidden
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
class AuthorInterestFormView(SingleObjectMixin, FormView):
template_name = "books/author_detail.html"
form_class = AuthorInterestForm
model = Author
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def get_success_url(self):
return reverse("author-detail", kwargs={"pk": self.object.pk})
最後に、これを新しい AuthorView
ビューにまとめます。クラスベースのビューで as_view()
を呼び出すと、関数ベースのビューと全く同じように動作するものが得られることは既に知っています。そのため、2つのサブビューのどちらかを選択する時点でこれを行うことができます。
キーワード引数を as_view()
に渡すには、 URLconf と同じようにします。例えば、 AuthorInterestFormView
の動作を別の URL にも表示させたいが、別のテンプレートを使いたい場合などです:
from django.views import View
class AuthorView(View):
def get(self, request, *args, **kwargs):
view = AuthorDetailView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = AuthorInterestFormView.as_view()
return view(request, *args, **kwargs)
この方法は、他のクラスベースのジェネリックビューや、 View
や TemplateView
を直接継承した独自のクラスベースのビューでも使用できます。異なるビューを可能な限り分離した状態に保つことができるからです。
単純な HTML を超える¶
クラスベースビューが役に立つのは、同じことを何度もしたい場合です。たとえば API を作成している場合、全てのビューはレンダリングされた HTML ではなく JSON を返す必要があります。
全てのビューで利用するために、 JSONへの変換処理をするmixinクラスを作成できます。
たとえば、JSONのmixinは以下のようになるでしょう。
from django.http import JsonResponse
class JSONResponseMixin:
"""
A mixin that can be used to render a JSON response.
"""
def render_to_json_response(self, context, **response_kwargs):
"""
Returns a JSON response, transforming 'context' to make the payload.
"""
return JsonResponse(self.get_data(context), **response_kwargs)
def get_data(self, context):
"""
Returns an object that will be serialized as JSON by json.dumps().
"""
# Note: This is *EXTREMELY* naive; in reality, you'll need
# to do much more complex handling to ensure that arbitrary
# objects -- such as Django model instances or querysets
# -- can be serialized as JSON.
return context
注釈
Djangoモデルやクエリセットを正しくJSONに変換する方法についての詳細は Django オブジェクトのシリアライズ ドキュメントを参照してください。
このmixinは render_to_response()
と同じ引数をもつ render_to_json_response()
メソッドを提供します。これを使用するには、たとえば TemplateVIew
へ取り入れ、 render_to_response()
をオーバーライドして代わりに render_to_json_response()
を呼び出す必要があります。
from django.views.generic import TemplateView
class JSONView(JSONResponseMixin, TemplateView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
同様に、このミックスインをジェネリックビューで使用することもできます。 JSONResponseMixin
と BaseDetailView
(テンプレートレンダリングの動作が混ざる前の DetailView
) をミックスインすることで、 DetailView
の独自のバージョンを作ることができます:
from django.views.generic.detail import BaseDetailView
class JSONDetailView(JSONResponseMixin, BaseDetailView):
def render_to_response(self, context, **response_kwargs):
return self.render_to_json_response(context, **response_kwargs)
このビューはレスポンスの作成を除いて、他の DetailView
と同じように記述され、同じように動作します。
本当に冒険をしたいのであれば、クエリ引数や HTTP ヘッダなどの HTTP リクエストのプロパティに応じて、HTML と JSON の 両方 のコンテンツを返すことができる DetailView
サブクラスを混ぜることもできます。 JSONResponseMixin
と SingleObjectTemplateResponseMixin
の両方をミックスインして、 render_to_response()
の実装をオーバーライドし、ユーザが要求したレスポンスの種類に応じて適切なレンダリングメソッドに振り分けます:
from django.views.generic.detail import SingleObjectTemplateResponseMixin
class HybridDetailView(
JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView
):
def render_to_response(self, context):
# Look for a 'format=json' GET argument
if self.request.GET.get("format") == "json":
return self.render_to_json_response(context)
else:
return super().render_to_response(context)
Pythonのメソッド多重定義に関する仕様により、 super().render_to_response(context)
の呼び出しは TemplateResponseMixin
に実装された render_to_response()
メソッドを呼び出します。