はじめに
ListViewはとても簡単にリスト表示やページネーション機能を実装することができます。
基本的に属性を指定すれば動くのですが、コードを理解しないと柔軟に対応できない恐れがあります。
本記事で理解いただければ幸いです。
ListViewの全体コードはこちら
ListViewの処理の流れ
ListViewはMultipleObjectTemplateResponseMixin
とBaseListView
を継承しています。
継承順により、MultipleObjectTemplateResponseMixin
が先に実行され、その後にBaseListView
の処理が行われます。
class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
継承① MultipleObjectTemplateResponseMixin
このMixinはTemplateResponseMixinを継承し、テンプレートの指定やHTTPレスポンスの生成を行います。
継承② BaseListView
MultipleObjectMixin
とView
を継承しており、ここでリストの取得やページネーション、コンテキストデータの生成が行われます。
Viewクラスのas_view()
メソッドにより、適切なHTTPメソッドへのマッピングが行われます。
MultipleObjectMixinクラス
このクラスでは、リストの取得やページネーション、コンテキストデータの生成などが行われます。
class MultipleObjectMixin(ContextMixin):
allow_empty = True
queryset = None
model = None
paginate_by = None
paginate_orphans = 0
context_object_name = None
paginator_class = Paginator
page_kwarg = 'page'
ordering = None
def get_queryset(self):
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
def get_ordering(self):
return self.ordering
def paginate_queryset(self, queryset, page_size):
paginator = self.get_paginator(
queryset, page_size, orphans=self.get_paginate_orphans(),
allow_empty_first_page=self.get_allow_empty())
page_kwarg = self.page_kwarg
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
try:
page_number = int(page)
except ValueError:
if page == 'last':
page_number = paginator.num_pages
else:
raise Http404(_('Page is not “last”, nor can it be converted to an int.'))
try:
page = paginator.page(page_number)
return (paginator, page, page.object_list, page.has_other_pages())
except InvalidPage as e:
raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
'page_number': page_number,
'message': str(e)
})
def get_paginate_by(self, queryset):
return self.paginate_by
def get_paginator(self, queryset, per_page, orphans=0,
allow_empty_first_page=True, **kwargs):
return self.paginator_class(
queryset, per_page, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, **kwargs)
def get_paginate_orphans(self):
return self.paginate_orphans
def get_allow_empty(self):
return self.allow_empty
def get_context_object_name(self, object_list):
if self.context_object_name:
return self.context_object_name
elif hasattr(object_list, 'model'):
return '%s_list' % object_list.model._meta.model_name
else:
return None
def get_context_data(self, *, object_list=None, **kwargs):
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)
クラス変数
変数名 | 役割 |
---|---|
allow_empty | クエリセットが空の場合にエラーを出すかどうかを決める変数。デフォルトのTrueならエラーを発生させません |
queryset | ビューが操作するクエリセットを定義します。設定されていない場合はmodel属性が使われます。 |
model | querysetが設定されていない場合、このモデルからクエリセットが自動的に作成されます。 |
paginate_by | 最後のページに表示するオブジェクトの最小数を指定します。この数未満の場合、それらは前のページに結合されます。 |
context_object_name | テンプレートでクエリセットにアクセスするために使用されるコンテキスト変数の名前を指定します。設定されていない場合、object_listが使用されます。 |
paginator_class | デフォルトはPaginatorクラスで、ページネーションに使用されるクラスを指定します。カスタムページネーターを使用する場合は、この属性をオーバーライドします。 |
page_kwarg | URLからページ番号を取得するために使用されるキーワード引数の名前を指定します。 |
ordering | クエリセットの並び順を指定します。この属性を設定することで、オブジェクトのデフォルトの並び順をオーバーライドできます。 |
get_querysetメソッド
クエリセットを取得するためのメソッドで、データベースからのデータ取得、フィルタリング、並べ替えを行います。
クエリセットとはデータベースからのデータの取得やフィルタリング、並べ替えなどの操作を抽象的に行うためのものであり、SQLクエリに変換されるので、Pythonのコードとして直感的に操作できるのがクエリセットの大きなメリットです。
all_objects = MyModel.objects.all()
filtered_objects = MyModel.objects.filter(name="John")
first_five = MyModel.objects.all()[:5]
コードでは初めにqueryset
の存在を確認し、それがQuerySet
インスタンスである場合、queryset.all()
を使って新しいクエリセットを複製します。
all()メソッドは新しいクエリセットを返し、元のクエリセットに影響を与えないため、ビュー外でqueryset属性が変更されることを防ぎ、ビュー内でのみクエリセットを操作するために行われます。
queryset
はQuerySet
のメソッド(.filter()、.all()など)を持っており、データベースへのクエリが「遅延評価」されることを意味します。
反対に、querysetがリストや他のイテラブルである場合はデータベースの遅延評価のメリットを享受できません。使い道としては特定のリスト操作やクエリセットからデータを即座に取得する必要がある場合に使用する必要があります。
ただし、一般的な使用ではQuerySetインスタンスを使用することを推奨しており。これによりデータベースの効率的な操作や、クエリの遅延評価、およびDjangoの多くの組み込み機能を利用することができます。
# クエリセット
queryset = Article.objects.filter(published=True)
# リスト
queryset = list(Article.objects.filter(published=True))
querysetが存在しない場合はmodel
が存在しているかを確認し、存在していればself.model._default_manager.all()
でモデルからすべてのオブジェクトを取得します。
いずれも存在しない場合はエラーを出力します。
リストをソートしたい場合は、orderingに値をセットすることで容易に実現することができます。
#昇順
class BookListView(ListView):
model = Book
ordering = 'title'
#降順
class BookListView(ListView):
model = Book
ordering = '-publish_date'
#動的にソートする
class BookListView(ListView):
model = Book
def get_ordering(self):
user_ordering = self.request.GET.get('order')
if user_ordering == 'desc':
return '-title'
return 'title'
paginate_querysetメソッド
クエリセットをページネーションするためのメソッドで、与えられたクエリセットをページネーションすることができます。
まずはget_paginator
メソッドを使用して新しいPaginatorクラスのインスタンスを作成します。このインスタンスは、クエリセットのページネーションに使用されます。(Paginatorクラスについてはこちら)
取得したいページ番号はpage = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
で取得しており、3つの異なるソースからページ番号を取得することができます。
self.kwargs
はビュー関数に渡されるキーワード引数を含む辞書で、URLconfの<int:page>
のようなURLのパターンマッチングから得られるキーワード引数を含んでいます。一つ目はそこからpage_kwarg(page)に一致するkeyの値(ページ番号を取得しています。)
self.request.GET.get(page_kwarg)
は/items/?page=2
のようなクエリパラメータからページ番号を取得します。
そして上記二つに当てはまらない場合は1
を取得します。
デフォルトのpage_kwargはpage
という名前ですが、変更したい場合はビュー内で属性をオーバーライドすることができます。
class MyListView(ListView):
model = MyModel
paginate_by = 10
page_kwarg = 'mypage'
path('items/page/<int:mypage>/', MyListView.as_view(), name='item-list-page'),
次に取得したpage(ページ番号)を元に実際にデータを取得していきます。
int型にキャストして、もしエラーになればlast
という文字列か確認します。lastの場合、ページ番号はページネータの最後のページとして(paginator.num_pages)として設定されます。num_pages
は、Paginatorクラスのメソッドで、オブジェクトの総数をページあたりのオブジェクト数で割り、その結果を切り上げることで必要な総ページ数を求めることができます。なので、pageがlastの場合、page_number = paginator.num_pages
によってpage_numberに必要なページ数が格納されることになります。
数値にキャストされるか必要なページ数を取得した後は、paginatorクラスのpage
メソッドで指定されたページ番号に対応するページオブジェクトを取得します。
そして最終的に(paginator, page, page.object_list, page.has_other_pages())
を返します。
paginator
はクエリセット全体をページネーションするためのオブジェクトです。
paginator.count: #全オブジェクトの総数を取得します。
paginator.num_pages: #全ページ数を取得します。
paginator.page_range: #使用可能なページ番号のリストを取得します。
page
は現在のページに関連する情報やオブジェクトのサブセットへのアクセスを提供します。
page.number: #現在のページ番号を取得します。
page.has_previous(): #前のページが存在する場合はTrueを返します。
page.has_next(): #次のページが存在する場合はTrueを返します。
page.previous_page_number(): #前のページの番号を取得します。
page.next_page_number(): #次のページの番号を取得します。
page.object_list
は現在のページに表示されるオブジェクトのサブセットで、ループさせることでページ上の各アイテムを操作または表示します
for item in page.object_list:
print(item)
page.has_other_pages
はbool型で前または次に他のページが存在する場合はTrueを返し、存在しない場合はFalseを返します。
get_context_dataメソッド
テンプレートに渡されるコンテキストデータを取得するためのメソッドです。
基本的にはビュー内で get_querysetメソッドをオーバーライドすることでリストを操作しますが、例えば、あるビューの結果を基に、その結果を表示するための別のビューにリダイレクトする場合には引数のobject_list
を使うことになります。
class MyCustomView(View):
template_name = 'my_template.html'
def get(self, request, *args, **kwargs):
# 何らかの処理を行い、オブジェクトリストを生成
object_list = MyModel.objects.filter(some_field='some_value')
# オブジェクトリストと共に別のビューにリダイレクトする
return render(request, self.template_name, self.get_context_data(object_list=object_list))
def get_context_data(self, *, object_list=None, **kwargs):
# object_listが提供されていない場合は、デフォルトのクエリセットを使用
if object_list is None:
object_list = MyModel.objects.all()
# コンテキストデータを生成
context = {
'object_list': object_list,
# 追加のコンテキストデータを設定
'additional_data': 'これは追加のデータです'
}
return context
上記の場合はobject_listにオブジェクトリストが引数に指定されているので、get_context_data
内で指定されたオブジェクトリストをcontextのobject_listキーに追加しています。
get_context_dataメソッドでは最初にqueryset = object_list if object_list is not None else self.object_list
でobject_listがあればそれをqueryset
にセットし、そうでなければデフォルトのクエリセットをセットします。
page_sizeはget_paginate_byから取得し、値があればpaginate_queryset
メソッドの引数にpage_sizeを入れてリストデータをテンプレートで使用するcontextデータに格納します。
値がなければデフォルトのobject_listを返します。
つまり、ページネーションが指定されていれば(ページサイズ)区切り、そうでなければデフォルトのobject_list(queryset)をcontextに入れているということです。
最後にcontext_object_nameが存在すれば、querysetの名前を指定した名前にして、最終的なcontextを親スーパーのget_context_dataにcontextを渡します。
BaseListViewクラス
MultipleObjectMixin
とView
クラスを継承し、ListView
の基本機能を提供します。
カスタムレスポンスの生成やリスト表示以外の処理を組み込みたい場合にはこのクラスが使用されます。
以下は、BaseListViewを使用して独自のHTTPレスポンスを生成する例です。
class BookListView(BaseListView):
model = Book
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
context = self.get_context_data()
# contextを使って独自のHttpResponseを生成
book_titles = ', '.join([book.title for book in context['object_list']])
return HttpResponse(f'Books: {book_titles}')
def get_context_data(self, **kwargs):
# ベースクラスのコンテキストデータを取得
context = super().get_context_data(**kwargs)
# 必要に応じてコンテキストデータをカスタマイズ
context['extra_data'] = 'This is extra data for the view.'
return context
getメソッドで取得されたオブジェクトのリストを使用して独自のテキストベースのHTTPレスポンスを生成しています。ビューにアクセスすると、すべての書籍のタイトルがコンマ区切りで表示されるレスポンスが返されます。
独自のレンダリング方法や特定のHTTPレスポンスのフォーマットが必要な場合に有効ですが、HTMLテンプレートを使用して通常のWebページをレンダリングする場合は、ListViewのような派生クラスを使用する方が効率的です。
MultipleObjectTemplateResponseMixinクラス
複数のオブジェクトをテンプレートに渡すために使用され、テンプレート名の生成などを行います。
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
"""Mixin for responding with a template and list of objects."""
template_name_suffix = '_list'
def get_template_names(self):
try:
names = super().get_template_names()
except ImproperlyConfigured:
names = []
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
get_template_namesメソッド
まずは継承したTemplateResponseMixin
のget_template_names
メソッドを使用してnamesを取得します。もしnamesが存在しなければ、空の配列をセットします。
object_listにmodel
属性が存在していればそのモデルのメタデータオブジェクトをopts
に格納し、names
にoptsのapp_label
、model_name
、template_name_suffixを使用してtemplate_nameを作成し返却します。
ListViewクラス
上記のMixinとクラスを組み合わせ、リスト表示に関連する一連のタスクを容易にします。
例えば、モデルの指定やテンプレート名、ページネーションの設定などが可能です。
class BookListView(ListView):
model = Book # 表示するモデルを指定
template_name = 'book_list.html' # 使用するテンプレートを指定
context_object_name = 'books' # テンプレートで使用するコンテキスト変数の名前を指定
paginate_by = 10 # 1ページに表示するオブジェクトの数を指定
<h1>Book List</h1>
<ul>
{% for book in books %}
<li>{{ book.title }} by {{ book.author }}</li>
{% endfor %}
</ul>
以下のmodelとtemplate_nameを指定するだけで、テンプレートでobject_listを使用することができます。
class IndexListView(ListView):
model = Item
template_name = 'pages/index.html'