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
This commit is contained in:
Adrian Holovaty 2008-03-18 21:13:48 +00:00
parent 855bc7b09d
commit d67208f5be
2 changed files with 315 additions and 88 deletions

View File

@ -1,46 +1,149 @@
class InvalidPage(Exception): class InvalidPage(Exception):
pass pass
class ObjectPaginator(object): class Paginator(object):
""" def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
This class makes pagination easy. Feed it a QuerySet or list, plus the number self.object_list = object_list
of objects you want on each page. Then read the hits and pages properties to self.per_page = per_page
see how many pages it involves. Call get_page with a page number (starting self.orphans = orphans
at 0) to get back a list of objects for that page. 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 def validate_number(self, number):
has_next_page(page_number) and has_previous_page(page_number). "Validates the given 1-based page number."
try:
Use orphans to avoid small final pages. For example: number = int(number)
13 records, num_per_page=10, orphans=2 --> pages==2, len(self.get_page(0))==10 except ValueError:
12 records, num_per_page=10, orphans=2 --> pages==1, len(self.get_page(0))==12 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 '<Page %s of %s>' % (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): 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.query_set = query_set
self.num_per_page = num_per_page self.num_per_page = num_per_page
self.orphans = orphans
self._hits = self._pages = None self._hits = self._pages = None
self._page_range = None
def validate_page_number(self, page_number): def validate_page_number(self, page_number):
try: try:
page_number = int(page_number) page_number = int(page_number) + 1
except ValueError: except ValueError:
raise InvalidPage raise InvalidPage
if page_number < 0 or page_number > self.pages - 1: return self.validate_number(page_number)
raise InvalidPage
return page_number
def get_page(self, page_number): def get_page(self, page_number):
page_number = self.validate_page_number(page_number) try:
bottom = page_number * self.num_per_page page_number = int(page_number) + 1
top = bottom + self.num_per_page except ValueError:
if top + self.orphans >= self.hits: raise InvalidPage
top = self.hits return self.page(page_number).object_list
return self.query_set[bottom:top]
def has_next_page(self, page_number): def has_next_page(self, page_number):
"Does page $page_number have a 'next' page?"
return page_number < self.pages - 1 return page_number < self.pages - 1
def has_previous_page(self, page_number): def has_previous_page(self, page_number):
@ -52,7 +155,7 @@ class ObjectPaginator(object):
relative to total objects found (hits). relative to total objects found (hits).
""" """
page_number = self.validate_page_number(page_number) 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): def last_on_page(self, page_number):
""" """
@ -60,40 +163,23 @@ class ObjectPaginator(object):
relative to total objects found (hits). relative to total objects found (hits).
""" """
page_number = self.validate_page_number(page_number) page_number = self.validate_page_number(page_number)
page_number += 1 # 1-base if page_number == self.num_pages:
if page_number == self.pages: return self.count
return self.hits
return page_number * self.num_per_page return page_number * self.num_per_page
def _get_hits(self): def _get_count(self):
if self._hits is None: # The old API allowed for self.object_list to be either a QuerySet or a
# Try .count() or fall back to len(). # list. Here, we handle both.
if self._count is None:
try: try:
self._hits = int(self.query_set.count()) self._count = self.object_list.count()
except (AttributeError, TypeError, ValueError): except AttributeError:
# AttributeError if query_set has no object count. self._count = len(self.object_list)
# TypeError if query_set.count() required arguments. return self._count
# ValueError if int() fails. count = property(_get_count)
self._hits = len(self.query_set)
return self._hits
def _get_pages(self): # The old API called it "hits" instead of "count".
if self._pages is None: hits = count
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
hits = property(_get_hits) # The old API called it "pages" instead of "num_pages".
pages = property(_get_pages) pages = Paginator.num_pages
page_range = property(_get_page_range)

View File

@ -4,6 +4,11 @@
Django provides a framework for paginating a list of objects in a few lines 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 of code. This is often useful for dividing search results or long lists of
objects into easily readable pages. 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 from django.db import models
@ -16,70 +21,206 @@ class Article(models.Model):
return self.headline return self.headline
__test__ = {'API_TESTS':""" __test__ = {'API_TESTS':"""
# prepare a list of objects for pagination # Prepare a list of objects for pagination.
>>> from datetime import datetime >>> from datetime import datetime
>>> for x in range(1, 10): >>> for x in range(1, 10):
... a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) ... a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29))
... a.save() ... 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
<Page 1 of 2>
>>> p.object_list
[<Article: Article 1>, <Article: Article 2>, <Article: Article 3>, <Article: Article 4>, <Article: Article 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
# Get the second page.
>>> p = paginator.page(2)
>>> p
<Page 2 of 2>
>>> p.object_list
[<Article: Article 6>, <Article: Article 7>, <Article: Article 8>, <Article: Article 9>]
>>> 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
<Page 1 of 2>
>>> 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 >>> from django.core.paginator import ObjectPaginator, InvalidPage
>>> paginator = ObjectPaginator(Article.objects.all(), 5) >>> paginator = ObjectPaginator(Article.objects.all(), 5)
# the paginator knows how many hits and pages it contains
>>> paginator.hits >>> paginator.hits
9 9
>>> paginator.pages >>> paginator.pages
2 2
>>> paginator.page_range
[1, 2]
# get the first page (zero-based) # Get the first page.
>>> paginator.get_page(0) >>> paginator.get_page(0)
[<Article: Article 1>, <Article: Article 2>, <Article: Article 3>, <Article: Article 4>, <Article: Article 5>] [<Article: Article 1>, <Article: Article 2>, <Article: Article 3>, <Article: Article 4>, <Article: Article 5>]
# get the second page
>>> paginator.get_page(1)
[<Article: Article 6>, <Article: Article 7>, <Article: Article 8>, <Article: Article 9>]
# does the first page have a next or previous page?
>>> paginator.has_next_page(0) >>> paginator.has_next_page(0)
True True
>>> paginator.has_previous_page(0) >>> paginator.has_previous_page(0)
False False
# check the second page
>>> paginator.has_next_page(1)
False
>>> paginator.has_previous_page(1)
True
>>> paginator.first_on_page(0) >>> paginator.first_on_page(0)
1 1
>>> paginator.first_on_page(1)
6
>>> paginator.last_on_page(0) >>> paginator.last_on_page(0)
5 5
# Get the second page.
>>> paginator.get_page(1)
[<Article: Article 6>, <Article: Article 7>, <Article: Article 8>, <Article: Article 9>]
>>> paginator.has_next_page(1)
False
>>> paginator.has_previous_page(1)
True
>>> paginator.first_on_page(1)
6
>>> paginator.last_on_page(1) >>> paginator.last_on_page(1)
9 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. # Add a few more records to test out the orphans feature.
>>> for x in range(10, 13): >>> for x in range(10, 13):
... Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() ... 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 = ObjectPaginator(Article.objects.all(), 10, orphans=3)
>>> paginator.pages >>> paginator.pages
1 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 = ObjectPaginator(Article.objects.all(), 10, orphans=1)
>>> paginator.pages >>> paginator.pages
2 2
# The paginator can provide a list of all available pages.
>>> paginator = ObjectPaginator(Article.objects.all(), 10)
>>> paginator.page_range
[1, 2]
"""} """}