From d67208f5bea85d731c8171070381cd80173dffd4 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 18 Mar 2008 21:13:48 +0000 Subject: [PATCH] Added a new and improved Paginator class, which allows you to pass a Page object to the template instead of 5 or 6 separate variables. ObjectPaginator still exists for backwards compatibility but issues a DeprecationWarning git-svn-id: http://code.djangoproject.com/svn/django/trunk@7306 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/paginator.py | 202 ++++++++++++++++++-------- tests/modeltests/pagination/models.py | 201 +++++++++++++++++++++---- 2 files changed, 315 insertions(+), 88 deletions(-) diff --git a/django/core/paginator.py b/django/core/paginator.py index 71a5479fd5..dabd20dfc0 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -1,46 +1,149 @@ class InvalidPage(Exception): pass -class ObjectPaginator(object): - """ - This class makes pagination easy. Feed it a QuerySet or list, plus the number - of objects you want on each page. Then read the hits and pages properties to - see how many pages it involves. Call get_page with a page number (starting - at 0) to get back a list of objects for that page. +class Paginator(object): + def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True): + self.object_list = object_list + self.per_page = per_page + self.orphans = orphans + self.allow_empty_first_page = allow_empty_first_page + self._num_pages = self._count = None - Finally, check if a page number has a next/prev page using - has_next_page(page_number) and has_previous_page(page_number). - - Use orphans to avoid small final pages. For example: - 13 records, num_per_page=10, orphans=2 --> pages==2, len(self.get_page(0))==10 - 12 records, num_per_page=10, orphans=2 --> pages==1, len(self.get_page(0))==12 + def validate_number(self, number): + "Validates the given 1-based page number." + try: + number = int(number) + except ValueError: + raise InvalidPage('That page number is not an integer') + if number < 1: + raise InvalidPage('That page number is less than 1') + if number > self.num_pages: + if number == 1 and self.allow_empty_first_page: + pass + else: + raise InvalidPage('That page contains no results') + return number + + def page(self, number): + "Returns 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 Page(self.object_list[bottom:top], number, self) + + def _get_count(self): + "Returns the total number of objects, across all pages." + if self._count is None: + self._count = len(self.object_list) + return self._count + count = property(_get_count) + + def _get_num_pages(self): + "Returns the total number of pages." + if self._num_pages is None: + hits = self.count - 1 - self.orphans + if hits < 1: + hits = 0 + if hits == 0 and not self.allow_empty_first_page: + self._num_pages = 0 + else: + self._num_pages = hits // self.per_page + 1 + return self._num_pages + num_pages = property(_get_num_pages) + + def _get_page_range(self): + """ + Returns a 1-based range of pages for iterating through within + a template for loop. + """ + return range(1, self.num_pages + 1) + page_range = property(_get_page_range) + +class QuerySetPaginator(Paginator): + """ + Like Paginator, but works on QuerySets. + """ + def _get_count(self): + if self._count is None: + self._count = self.object_list.count() + return self._count + count = property(_get_count) + +class Page(object): + def __init__(self, object_list, number, paginator): + self.object_list = object_list + self.number = number + self.paginator = paginator + + def __repr__(self): + return '' % (self.number, self.paginator.num_pages) + + 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.number + 1 + + def previous_page_number(self): + return self.number - 1 + + def start_index(self): + """ + Returns the 1-based index of the first object on this page, + relative to total objects in the paginator. + """ + return (self.paginator.per_page * (self.number - 1)) + 1 + + def end_index(self): + """ + Returns the 1-based index of the last object on this page, + relative to total objects found (hits). + """ + if self.number == self.paginator.num_pages: + return self.paginator.count + return self.number * self.paginator.per_page + +class ObjectPaginator(Paginator): + """ + Legacy ObjectPaginator class, for backwards compatibility. + + Note that each method on this class that takes page_number expects a + zero-based page number, whereas the new API (Paginator/Page) uses one-based + page numbers. """ def __init__(self, query_set, num_per_page, orphans=0): + Paginator.__init__(self, query_set, num_per_page, orphans) + import warnings + warnings.warn("The ObjectPaginator is deprecated. Use django.core.paginator.Paginator instead.", DeprecationWarning) + + # Keep these attributes around for backwards compatibility. self.query_set = query_set self.num_per_page = num_per_page - self.orphans = orphans self._hits = self._pages = None - self._page_range = None def validate_page_number(self, page_number): try: - page_number = int(page_number) + page_number = int(page_number) + 1 except ValueError: raise InvalidPage - if page_number < 0 or page_number > self.pages - 1: - raise InvalidPage - return page_number + return self.validate_number(page_number) def get_page(self, page_number): - page_number = self.validate_page_number(page_number) - bottom = page_number * self.num_per_page - top = bottom + self.num_per_page - if top + self.orphans >= self.hits: - top = self.hits - return self.query_set[bottom:top] + try: + page_number = int(page_number) + 1 + except ValueError: + raise InvalidPage + return self.page(page_number).object_list def has_next_page(self, page_number): - "Does page $page_number have a 'next' page?" return page_number < self.pages - 1 def has_previous_page(self, page_number): @@ -52,7 +155,7 @@ class ObjectPaginator(object): relative to total objects found (hits). """ page_number = self.validate_page_number(page_number) - return (self.num_per_page * page_number) + 1 + return (self.num_per_page * (page_number - 1)) + 1 def last_on_page(self, page_number): """ @@ -60,40 +163,23 @@ class ObjectPaginator(object): relative to total objects found (hits). """ page_number = self.validate_page_number(page_number) - page_number += 1 # 1-base - if page_number == self.pages: - return self.hits + if page_number == self.num_pages: + return self.count return page_number * self.num_per_page - def _get_hits(self): - if self._hits is None: - # Try .count() or fall back to len(). + def _get_count(self): + # The old API allowed for self.object_list to be either a QuerySet or a + # list. Here, we handle both. + if self._count is None: try: - self._hits = int(self.query_set.count()) - except (AttributeError, TypeError, ValueError): - # AttributeError if query_set has no object count. - # TypeError if query_set.count() required arguments. - # ValueError if int() fails. - self._hits = len(self.query_set) - return self._hits + self._count = self.object_list.count() + except AttributeError: + self._count = len(self.object_list) + return self._count + count = property(_get_count) - def _get_pages(self): - if self._pages is None: - hits = (self.hits - 1 - self.orphans) - if hits < 1: - hits = 0 - self._pages = hits // self.num_per_page + 1 - return self._pages - - def _get_page_range(self): - """ - Returns a 1-based range of pages for iterating through within - a template for loop. - """ - if self._page_range is None: - self._page_range = range(1, self.pages + 1) - return self._page_range + # The old API called it "hits" instead of "count". + hits = count - hits = property(_get_hits) - pages = property(_get_pages) - page_range = property(_get_page_range) + # The old API called it "pages" instead of "num_pages". + pages = Paginator.num_pages diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index f44c67a139..ca03e38573 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -4,6 +4,11 @@ Django provides a framework for paginating a list of objects in a few lines of code. This is often useful for dividing search results or long lists of objects into easily readable pages. + +In Django 0.96 and earlier, a single ObjectPaginator class implemented this +functionality. In the Django development version, the behavior is split across +two classes -- Paginator and Page -- that are more easier to use. The legacy +ObjectPaginator class is deprecated. """ from django.db import models @@ -16,70 +21,206 @@ class Article(models.Model): return self.headline __test__ = {'API_TESTS':""" -# prepare a list of objects for pagination +# Prepare a list of objects for pagination. >>> from datetime import datetime >>> for x in range(1, 10): ... a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) ... a.save() -# create a basic paginator, 5 articles per page +#################################### +# New/current API (Paginator/Page) # +#################################### + +>>> from django.core.paginator import Paginator, InvalidPage +>>> paginator = Paginator(Article.objects.all(), 5) +>>> paginator.count +9 +>>> paginator.num_pages +2 +>>> paginator.page_range +[1, 2] + +# Get the first page. +>>> p = paginator.page(1) +>>> p + +>>> p.object_list +[, , , , ] +>>> p.has_next() +True +>>> p.has_previous() +False +>>> p.has_other_pages() +True +>>> p.next_page_number() +2 +>>> p.previous_page_number() +0 +>>> p.start_index() +1 +>>> p.end_index() +5 + +# Get the second page. +>>> p = paginator.page(2) +>>> p + +>>> p.object_list +[, , , ] +>>> p.has_next() +False +>>> p.has_previous() +True +>>> p.has_other_pages() +True +>>> p.next_page_number() +3 +>>> p.previous_page_number() +1 +>>> p.start_index() +6 +>>> p.end_index() +9 + +# Invalid pages raise InvalidPage. +>>> paginator.page(0) +Traceback (most recent call last): +... +InvalidPage: ... +>>> paginator.page(3) +Traceback (most recent call last): +... +InvalidPage: ... + +# Empty paginators with allow_empty_first_page=True. +>>> paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True) +>>> paginator.count +0 +>>> paginator.num_pages +1 +>>> paginator.page_range +[1] + +# Empty paginators with allow_empty_first_page=False. +>>> paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False) +>>> paginator.count +0 +>>> paginator.num_pages +0 +>>> paginator.page_range +[] + +# Paginators work with regular lists/tuples, too -- not just with QuerySets. +>>> paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 5) +>>> paginator.count +9 +>>> paginator.num_pages +2 +>>> paginator.page_range +[1, 2] + +# Get the first page. +>>> p = paginator.page(1) +>>> p + +>>> p.object_list +[1, 2, 3, 4, 5] +>>> p.has_next() +True +>>> p.has_previous() +False +>>> p.has_other_pages() +True +>>> p.next_page_number() +2 +>>> p.previous_page_number() +0 +>>> p.start_index() +1 +>>> p.end_index() +5 + +################################ +# Legacy API (ObjectPaginator) # +################################ + >>> from django.core.paginator import ObjectPaginator, InvalidPage >>> paginator = ObjectPaginator(Article.objects.all(), 5) - -# the paginator knows how many hits and pages it contains >>> paginator.hits 9 - >>> paginator.pages 2 +>>> paginator.page_range +[1, 2] -# get the first page (zero-based) +# Get the first page. >>> paginator.get_page(0) [, , , , ] - -# get the second page ->>> paginator.get_page(1) -[, , , ] - -# does the first page have a next or previous page? >>> paginator.has_next_page(0) True - >>> paginator.has_previous_page(0) False - -# check the second page ->>> paginator.has_next_page(1) -False - ->>> paginator.has_previous_page(1) -True - >>> paginator.first_on_page(0) 1 ->>> paginator.first_on_page(1) -6 >>> paginator.last_on_page(0) 5 + +# Get the second page. +>>> paginator.get_page(1) +[, , , ] +>>> paginator.has_next_page(1) +False +>>> paginator.has_previous_page(1) +True +>>> paginator.first_on_page(1) +6 >>> paginator.last_on_page(1) 9 +# Invalid pages raise InvalidPage. +>>> paginator.get_page(-1) +Traceback (most recent call last): +... +InvalidPage: ... +>>> paginator.get_page(2) +Traceback (most recent call last): +... +InvalidPage: ... + +# Empty paginators with allow_empty_first_page=True. +>>> paginator = ObjectPaginator(Article.objects.filter(id=0), 5) +>>> paginator.count +0 +>>> paginator.num_pages +1 +>>> paginator.page_range +[1] + +################## +# Orphan support # +################## + # Add a few more records to test out the orphans feature. >>> for x in range(10, 13): ... Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() -# With orphans set to 3 and 10 items per page, we should get all 12 items on a single page: +# With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. +>>> paginator = Paginator(Article.objects.all(), 10, orphans=3) +>>> paginator.num_pages +1 + +# With orphans only set to 1, we should get two pages. +>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1) +>>> paginator.num_pages +2 + +# LEGACY: With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. >>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=3) >>> paginator.pages 1 -# With orphans only set to 1, we should get two pages: +# LEGACY: With orphans only set to 1, we should get two pages. >>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1) >>> paginator.pages 2 - -# The paginator can provide a list of all available pages. ->>> paginator = ObjectPaginator(Article.objects.all(), 10) ->>> paginator.page_range -[1, 2] """}