Fixed #2575: ObjectPaginator now accepts a "orphans" option to prevent pages with only a few items. Thanks, SmileyChris.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@4041 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2006-11-07 04:44:27 +00:00
parent dabf036604
commit d4d1a22730
2 changed files with 51 additions and 37 deletions

View File

@ -1,54 +1,46 @@
from math import ceil
class InvalidPage(Exception): class InvalidPage(Exception):
pass pass
class ObjectPaginator(object): class ObjectPaginator(object):
""" """
This class makes pagination easy. Feed it a QuerySet, plus the number of This class makes pagination easy. Feed it a QuerySet or list, plus the number
objects you want on each page. Then read the hits and pages properties to 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 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. at 0) to get back a list of objects for that page.
Finally, check if a page number has a next/prev page using Finally, check if a page number has a next/prev page using
has_next_page(page_number) and has_previous_page(page_number). 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 __init__(self, query_set, num_per_page): def __init__(self, query_set, num_per_page, orphans=0):
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._hits, self._pages = None, None self.orphans = orphans
self._has_next = {} # Caches page_number -> has_next_boolean self._hits = self._pages = None
def get_page(self, page_number): def validate_page_number(self, page_number):
try: try:
page_number = int(page_number) page_number = int(page_number)
except ValueError: except ValueError:
raise InvalidPage raise InvalidPage
if page_number < 0: if page_number < 0 or page_number > self.pages - 1:
raise InvalidPage raise InvalidPage
return page_number
# Retrieve one extra record, and check for the existence of that extra def get_page(self, page_number):
# record to determine whether there's a next page. page_number = self.validate_page_number(page_number)
limit = self.num_per_page + 1 bottom = page_number * self.num_per_page
offset = page_number * self.num_per_page top = bottom + self.num_per_page
if top + self.orphans >= self.hits:
object_list = list(self.query_set[offset:offset+limit]) top = self.hits
return self.query_set[bottom:top]
if not object_list:
raise InvalidPage
self._has_next[page_number] = (len(object_list) > self.num_per_page)
return object_list[:self.num_per_page]
def has_next_page(self, page_number): def has_next_page(self, page_number):
"Does page $page_number have a 'next' page?" "Does page $page_number have a 'next' page?"
if not self._has_next.has_key(page_number): return page_number < self.pages - 1
if self._pages is None:
offset = (page_number + 1) * self.num_per_page
self._has_next[page_number] = len(self.query_set[offset:offset+1]) > 0
else:
self._has_next[page_number] = page_number < (self.pages - 1)
return self._has_next[page_number]
def has_previous_page(self, page_number): def has_previous_page(self, page_number):
return page_number > 0 return page_number > 0
@ -58,8 +50,7 @@ class ObjectPaginator(object):
Returns the 1-based index of the first object on the given page, Returns the 1-based index of the first object on the given page,
relative to total objects found (hits). relative to total objects found (hits).
""" """
if page_number == 0: page_number = self.validate_page_number(page_number)
return 1
return (self.num_per_page * page_number) + 1 return (self.num_per_page * page_number) + 1
def last_on_page(self, page_number): def last_on_page(self, page_number):
@ -67,20 +58,30 @@ class ObjectPaginator(object):
Returns the 1-based index of the last object on the given page, Returns the 1-based index of the last object on the given page,
relative to total objects found (hits). relative to total objects found (hits).
""" """
if page_number == 0 and self.num_per_page >= self._hits: page_number = self.validate_page_number(page_number)
return self._hits page_number += 1 # 1-base
elif page_number == (self._pages - 1) and (page_number + 1) * self.num_per_page > self._hits: if page_number == self.pages:
return self._hits return self.hits
return (page_number + 1) * self.num_per_page return page_number * self.num_per_page
def _get_hits(self): def _get_hits(self):
if self._hits is None: if self._hits is None:
self._hits = self.query_set.count() # Try .count() or fall back to len().
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 return self._hits
def _get_pages(self): def _get_pages(self):
if self._pages is None: if self._pages is None:
self._pages = int(ceil(self.hits / float(self.num_per_page))) hits = (self.hits - 1 - self.orphans)
if hits < 1:
hits = 0
self._pages = hits // self.num_per_page + 1
return self._pages return self._pages
hits = property(_get_hits) hits = property(_get_hits)

View File

@ -64,4 +64,17 @@ True
>>> paginator.last_on_page(1) >>> paginator.last_on_page(1)
9 9
# 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:
>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=3)
>>> paginator.pages
1
# With orphans only set to 1, we should get two pages:
>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1)
>>> paginator.pages
2
"""} """}