| 1 |
from copy import copy |
|---|
| 2 |
from math import ceil |
|---|
| 3 |
|
|---|
| 4 |
class InvalidPage(Exception): |
|---|
| 5 |
pass |
|---|
| 6 |
|
|---|
| 7 |
class ObjectPaginator(object): |
|---|
| 8 |
""" |
|---|
| 9 |
This class makes pagination easy. Feed it a module (an object with |
|---|
| 10 |
get_count() and get_list() methods) and a dictionary of arguments |
|---|
| 11 |
to be passed to those methods, plus the number of objects you want |
|---|
| 12 |
on each page. Then read the hits and pages properties to see how |
|---|
| 13 |
many pages it involves. Call get_page with a page number (starting |
|---|
| 14 |
at 0) to get back a list of objects for that page. |
|---|
| 15 |
|
|---|
| 16 |
Finally, check if a page number has a next/prev page using |
|---|
| 17 |
has_next_page(page_number) and has_previous_page(page_number). |
|---|
| 18 |
""" |
|---|
| 19 |
def __init__(self, module, args, num_per_page, count_method='get_count', list_method='get_list'): |
|---|
| 20 |
self.module, self.args = module, args |
|---|
| 21 |
self.num_per_page = num_per_page |
|---|
| 22 |
self.count_method, self.list_method = count_method, list_method |
|---|
| 23 |
self._hits, self._pages = None, None |
|---|
| 24 |
self._has_next = {} # Caches page_number -> has_next_boolean |
|---|
| 25 |
|
|---|
| 26 |
def _normalize_page_number(self, page_number): |
|---|
| 27 |
try: |
|---|
| 28 |
page_number = int(page_number) |
|---|
| 29 |
except ValueError: |
|---|
| 30 |
raise InvalidPage |
|---|
| 31 |
if page_number < 0: |
|---|
| 32 |
raise InvalidPage |
|---|
| 33 |
return page_number |
|---|
| 34 |
|
|---|
| 35 |
def get_page(self, page_number): |
|---|
| 36 |
page_number = self._normalize_page_number(page_number) |
|---|
| 37 |
args = copy(self.args) |
|---|
| 38 |
args['offset'] = page_number * self.num_per_page |
|---|
| 39 |
# Retrieve one extra record, and check for the existence of that extra |
|---|
| 40 |
# record to determine whether there's a next page. |
|---|
| 41 |
args['limit'] = self.num_per_page + 1 |
|---|
| 42 |
object_list = getattr(self.module, self.list_method)(**args) |
|---|
| 43 |
if not object_list: |
|---|
| 44 |
raise InvalidPage |
|---|
| 45 |
self._has_next[page_number] = (len(object_list) > self.num_per_page) |
|---|
| 46 |
return object_list[:self.num_per_page] |
|---|
| 47 |
|
|---|
| 48 |
def has_next_page(self, page_number): |
|---|
| 49 |
"Does page $page_number have a 'next' page?" |
|---|
| 50 |
if not self._has_next.has_key(page_number): |
|---|
| 51 |
if self._pages is None: |
|---|
| 52 |
args = copy(self.args) |
|---|
| 53 |
args['offset'] = (page_number + 1) * self.num_per_page |
|---|
| 54 |
args['limit'] = 1 |
|---|
| 55 |
object_list = getattr(self.module, self.list_method)(**args) |
|---|
| 56 |
self._has_next[page_number] = (object_list != []) |
|---|
| 57 |
else: |
|---|
| 58 |
self._has_next[page_number] = page_number < (self.pages - 1) |
|---|
| 59 |
return self._has_next[page_number] |
|---|
| 60 |
|
|---|
| 61 |
def has_previous_page(self, page_number): |
|---|
| 62 |
return page_number > 0 |
|---|
| 63 |
|
|---|
| 64 |
def _get_hits(self): |
|---|
| 65 |
if self._hits is None: |
|---|
| 66 |
order_args = copy(self.args) |
|---|
| 67 |
if order_args.has_key('order_by'): |
|---|
| 68 |
del order_args['order_by'] |
|---|
| 69 |
if order_args.has_key('select_related'): |
|---|
| 70 |
del order_args['select_related'] |
|---|
| 71 |
self._hits = getattr(self.module, self.count_method)(**order_args) |
|---|
| 72 |
return self._hits |
|---|
| 73 |
|
|---|
| 74 |
def _get_pages(self): |
|---|
| 75 |
if self._pages is None: |
|---|
| 76 |
self._pages = int(ceil(self.hits / float(self.num_per_page))) |
|---|
| 77 |
return self._pages |
|---|
| 78 |
|
|---|
| 79 |
hits = property(_get_hits) |
|---|
| 80 |
pages = property(_get_pages) |
|---|
| 81 |
|
|---|
| 82 |
class MultiObjectPaginator(ObjectPaginator): |
|---|
| 83 |
""" |
|---|
| 84 |
A paginator for multiple object types. |
|---|
| 85 |
|
|---|
| 86 |
Has the same interface as ObjectPaginator except for __init__(). |
|---|
| 87 |
""" |
|---|
| 88 |
def __init__(self, mods_and_args, num_per_page, count_method='get_count', list_method='get_list'): |
|---|
| 89 |
""" |
|---|
| 90 |
mods_and_args = ((some_mod, 'foo__exact="bar"'), (other_mod, 'bar__startswith="hi"')) |
|---|
| 91 |
""" |
|---|
| 92 |
self.mods_and_args = mods_and_args |
|---|
| 93 |
self.num_per_page = num_per_page |
|---|
| 94 |
self.count_method, self.list_method = count_method, list_method |
|---|
| 95 |
self._hits, self._pages = None, None |
|---|
| 96 |
self._has_next = {} # Caches page_number -> has_next_boolean |
|---|
| 97 |
|
|---|
| 98 |
def get_page(self, page_number): |
|---|
| 99 |
page_number = self._normalize_page_number(page_number) |
|---|
| 100 |
raise NotImplementedError |
|---|
| 101 |
|
|---|
| 102 |
def has_next_page(self, page_number): |
|---|
| 103 |
"Does page $page_number have a 'next' page?" |
|---|
| 104 |
raise NotImplementedError |
|---|