初めに
Djangoでリスト表示する場合、Paginator
クラスを使用することで簡単に実装することができます。
しかし、特定の要件やビジネスロジックを満たすためにページネーションの挙動を変更しなければいけない場合があります。
そんな時にPaginator
クラスを理解していれば、容易にカスタマイズすることができると考えまとめることにしました。
全体コードはこちらから
Paginatorクラス
オブジェクトのリストをページ単位で分割する機能を提供します。
これにより、大量のデータを扱う際のユーザーインターフェイスが向上します。
class Paginator:
def __init__(self, object_list, per_page, orphans=0,
allow_empty_first_page=True):
self.object_list = object_list
self._check_object_list_is_ordered()
self.per_page = int(per_page)
self.orphans = int(orphans)
self.allow_empty_first_page = allow_empty_first_page
def __iter__(self):
for page_number in self.page_range:
yield self.page(page_number)
def validate_number(self, number):
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
raise PageNotAnInteger(_('That page number is not an integer'))
if number < 1:
raise EmptyPage(_('That page number is less than 1'))
if number > self.num_pages:
if number == 1 and self.allow_empty_first_page:
pass
else:
raise EmptyPage(_('That page contains no results'))
return number
def get_page(self, number):
try:
number = self.validate_number(number)
except PageNotAnInteger:
number = 1
except EmptyPage:
number = self.num_pages
return self.page(number)
def page(self, number):
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return self._get_page(self.object_list[bottom:top], number, self)
def _get_page(self, *args, **kwargs):
return Page(*args, **kwargs)
@cached_property
def count(self):
c = getattr(self.object_list, 'count', None)
if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
return c()
return len(self.object_list)
@cached_property
def num_pages(self):
if self.count == 0 and not self.allow_empty_first_page:
return 0
hits = max(1, self.count - self.orphans)
return ceil(hits / self.per_page)
@property
def page_range(self):
return range(1, self.num_pages + 1)
def _check_object_list_is_ordered(self):
ordered = getattr(self.object_list, 'ordered', None)
if ordered is not None and not ordered:
obj_list_repr = (
'{} {}'.format(self.object_list.model, self.object_list.__class__.__name__)
if hasattr(self.object_list, 'model')
else '{!r}'.format(self.object_list)
)
warnings.warn(
'Pagination may yield inconsistent results with an unordered '
'object_list: {}.'.format(obj_list_repr),
UnorderedObjectListWarning,
stacklevel=3
)
初期化
Paginatorは以下の引数で初期化されます。
変数名 | 説明 |
---|---|
object_list | ページネーションが適用されるオブジェクトのリストで、データベースから取得したレコードのリストや、任意のPythonのリスト等が該当します。 |
per_page | 1ページあたりに表示するオブジェクトの最大数を示します |
orphans | この変数の値より少ないアイテムが最後のページにある場合、それ等を前のページに組み込むことができます |
allow_empty_first_page | 最初のページが空であることを許可するかどうかをbool値で指定します。False に設定すると、空のリストでページネーションを試みるときに例外が発生します。 |
オブジェクトリストのチェック
初期化の一環として、_check_object_list_is_ordered
メソッドが呼び出され、object_list
が順序付けられているかどうかを確認します。
ページネーションの計算
-
count
: オブジェクトの総数を返します。 -
num_pages
: ページの総数を返します。 -
page_range
: ページ番号の範囲を返します。
ページの取得と検証
-
get_page
: 指定されたページ番号に対して検証を行い、有効なPage
オブジェクトを返します。
イテレーション
__iter__
メソッドを使用すると、Paginator
インスタンスをイテレートすることができます。これにより、self.page_range
に基づいて順番にページが生成されます。
ページオブジェクトの生成
page
メソッドは、指定されたページ番号に基づいてPage
オブジェクトを生成します。
ページのナビゲーション
Page
クラスには、次のページや前のページへのナビゲーションを可能にするメソッドが用意されています。
このメソッドにより、ページネーション対象のオブジェクトを、per_page
で指定した1ページあたりに表示する数に分割して表示することができます。
__iter__メソッド
このメソッドにより、Paginatorインスタンスをイテレートして、各ページを順に取得できます。
def __iter__(self):
for page_number in self.page_range:
yield self.page(page_number)
items = ['アイテム1', 'アイテム2', 'アイテム3', 'アイテム4', 'アイテム5', 'アイテム6', 'アイテム7', 'アイテム8', 'アイテム9', 'アイテム10', 'アイテム11', 'アイテム12']
paginator = Paginator(items, 3)
for page in paginator:
print(f"ページ {page.number}:")
for item in page:
print(item)
print("\n---\n")
ページ 1:
アイテム1
アイテム2
アイテム3
---
ページ 2:
アイテム4
アイテム5
アイテム6
---
.....
page_range
メソッドでは1から最大ページ数までの範囲を取得しており、この値を元にループを行います。
page_rangeメソッドではnum_pages
、count
メソッドが使用されています。
そしてこのループ内でpage
メソッドを呼び出して、その番号に対応するページを取得します。
pageメソッド
pageメソッドは、指定されたページ番号に基づいてPageオブジェクトを作成し、そのページに含まれるオブジェクトのサブセットを返します。
def page(self, number):
"""Return a Page object for the given 1-based page number."""
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return self._get_page(self.object_list[bottom:top], number, self)
引数のページ番号をもとにそのページのアイテムが開始されるインデックス(bottom
)と終了されるインデックス(top
)を計算して_get_page
メソッドを呼び出すことで(厳密にはPage
クラスを使用して)、その範囲にあるアイテムのスライスとページ番号、そしてPaginator自身を引数としてPageオブジェクトを生成します。
Pageクラス
Pageクラスは、特定のページに関連するオブジェクトの集合を管理します。これには、ページのナビゲーションや、ページ内のアイテムへのアクセスなどが含まれます。
class Page(collections.abc.Sequence):
def __init__(self, object_list, number, paginator):
self.object_list = object_list
self.number = number
self.paginator = paginator
def __repr__(self):
return '<Page %s of %s>' % (self.number, self.paginator.num_pages)
def __len__(self):
return len(self.object_list)
def __getitem__(self, index):
if not isinstance(index, (int, slice)):
raise TypeError(
'Page indices must be integers or slices, not %s.'
% type(index).__name__
)
# The object_list is converted to a list so that if it was a QuerySet
# it won't be a database hit per __getitem__.
if not isinstance(self.object_list, list):
self.object_list = list(self.object_list)
return self.object_list[index]
def has_next(self):
return self.number < self.paginator.num_pages
def has_previous(self):
return self.number > 1
def has_other_pages(self):
return self.has_previous() or self.has_next()
def next_page_number(self):
return self.paginator.validate_number(self.number + 1)
def previous_page_number(self):
return self.paginator.validate_number(self.number - 1)
def start_index(self):
# Special case, return zero if no items.
if self.paginator.count == 0:
return 0
return (self.paginator.per_page * (self.number - 1)) + 1
def end_index(self):
# Special case for the last page because there can be orphans.
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page
属性
変数名 | 説明 |
---|---|
object_list | ページに含まれるオブジェクトのリスト |
number | ページ番号 |
object_list | 親Paginatorオブジェクトの参照 |
ヘルパーメソッド
メソッド名 | 説明 |
---|---|
has_next | 次のページが存在するかどうかを返します |
has_previous | 前のページが存在するかどうかを返します |
has_other_pages | 他のページが存在するかどうかを返します |
next_page_number | 次のページの番号を返します |
previous_page_number | 前のページの番号を返します |
start_index | このページに含まれるオブジェクトの開始インデックス |
end_index | このページに含まれるオブジェクトの終了インデックス |
これらの機能により、Pageオブジェクトを使用することで、特定のページのコンテキストでページネーションに関連する情報を簡単に取得することができます
カスタム例
以下では、PaginatorクラスとPageクラスを継承してオーバーライドしています。
from django.core.paginator import Paginator, Page
class CustomPage(Page):
def get_highlighted_items(self):
return ["**{}**".format(item) if len(item) >= 5 else item for item in self.object_list]
class CustomPaginator(Paginator):
def _get_page(self, *args, **kwargs):
return CustomPage(*args, **kwargs)
# 使用例
objects = ['apple', 'banana', 'cherry', 'date', 'fig', 'grape']
custom_paginator = CustomPaginator(objects, 2)
page1 = custom_paginator.page(1)
# カスタムメソッドを使用
highlighted_items = page1.get_highlighted_items()
print(highlighted_items) # ['apple', '**banana**']
CustomPageクラスでは、Page
クラスを継承し、条件に基づいてアイテムにハイライトを追加する新しいメソッドget_highlighted_items
を提供します。
CustomPaginator
クラスでは_get_page
メソッドをオーバーライドして、カスタムページオブジェクト CustomPage を生成して返しています。
custom_paginator.page(1)
でpage
メソッドを使用して、ページ番号1に相当するページを取得しています。
後は追加したget_highlighted_items
メソッドを使用することでページ1の文字列長が5以上のアイテムに対してハイライトされるようになります。