LoginSignup
1
0

DjangoのPaginatorクラスを理解する

Last updated at Posted at 2023-11-06

初めに

Djangoでリスト表示する場合、Paginatorクラスを使用することで簡単に実装することができます。
しかし、特定の要件やビジネスロジックを満たすためにページネーションの挙動を変更しなければいけない場合があります。
そんな時に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インスタンスをイテレートして、各ページを順に取得できます。

__iter__メソッド
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_pagescountメソッドが使用されています。

そしてこのループ内でpageメソッドを呼び出して、その番号に対応するページを取得します。

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クラスは、特定のページに関連するオブジェクトの集合を管理します。これには、ページのナビゲーションや、ページ内のアイテムへのアクセスなどが含まれます。

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以上のアイテムに対してハイライトされるようになります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0