silly FinitePaginator for finite lists of items
[django-pagination.git] / pagination / paginator.py
1 from django.core.paginator import Paginator, Page, PageNotAnInteger, EmptyPage
2
3 class InfinitePaginator(Paginator):
4     '''
5         Paginator designed for cases when it's not important to know how many total pages.
6         This is useful for any object_list that has no count() method or can be used to
7         improve performance for MySQL by removing counts.
8
9         The orphans parameter has been removed for simplicity and there's a link template string
10         for creating the links to the next and previous pages.
11
12         Class name is pronounced verbally in a deep tone.
13     '''
14
15     def __init__(self, object_list, per_page, allow_empty_first_page=True, link_template='/page/%d/'):
16         orphans = 0 # no orphans
17         super(InfinitePaginator, self).__init__(object_list, per_page, orphans, allow_empty_first_page)
18         # no count or num pages
19         del self._num_pages, self._count
20         # bonus links
21         self.link_template = link_template
22
23     def validate_number(self, number):
24         "Validates the given 1-based page number."
25         try:
26             number = int(number)
27         except ValueError:
28             raise PageNotAnInteger('That page number is not an integer')
29         if number < 1:
30             raise EmptyPage('That page number is less than 1')
31         return number
32
33     def page(self, number):
34         "Returns a Page object for the given 1-based page number."
35         number = self.validate_number(number)
36         bottom = (number - 1) * self.per_page
37         top = bottom + self.per_page
38         page_items = self.object_list[bottom:top]
39         # check moved from validate_number
40         if not page_items:
41             if number == 1 and self.allow_empty_first_page:
42                 pass
43             else:
44                 raise EmptyPage('That page contains no results')
45         return InfinitePage(page_items, number, self)
46
47     def _get_count(self):
48         "Returns the total number of objects, across all pages."
49         raise NotImplementedError
50     count = property(_get_count)
51
52     def _get_num_pages(self):
53         "Returns the total number of pages."
54         raise NotImplementedError
55     num_pages = property(_get_num_pages)
56
57     def _get_page_range(self):
58         """
59         Returns a 1-based range of pages for iterating through within
60         a template for loop.
61         """
62         raise NotImplementedError
63     page_range = property(_get_page_range)
64
65 class InfinitePage(Page):
66
67     def __repr__(self):
68         return '<Page %s>' % self.number
69
70     def has_next(self):
71         "Checks for one more item than last on this page."
72         try:
73             next_item = self.paginator.object_list[self.number * self.paginator.per_page]
74         except IndexError:
75             return False
76         return True
77
78     def end_index(self):
79         """
80         Returns the 1-based index of the last object on this page,
81         relative to total objects found (hits).
82         """
83         return (self.number - 1) * self.paginator.per_page + len(self.object_list)
84
85     '''Bonus methods for creating links'''
86
87     def next_link(self):
88         if self.has_next():
89             return self.paginator.link_template % (self.number + 1)
90         return None
91
92     def previous_link(self):
93         if self.has_previous():
94             return self.paginator.link_template % (self.number - 1)
95         return None
96
97 class FinitePaginator(InfinitePaginator):
98     '''
99         Paginator for cases when the list of items is already finite.
100
101         A good example is a list generated from an API call. This is a subclass
102         of InfinitePaginator because we have no idea how many items exist in the
103         full collection.
104
105         To accurately determine if the next page exists, a FinitePaginator MUST be created
106         with an object_list_plus that may contain more items than the per_page count.
107         Typically, you'll have an object_list_plus with one extra item (if there's a next page).
108         You'll also need to supply the offset from the full collection in order to get the
109         page start_index.
110
111         This is a very silly class but useful if you love the Django pagination conventions.
112     '''
113
114     def __init__(self, object_list_plus, per_page, offset=None, allow_empty_first_page=True, link_template='/page/%d/'):
115         super(FinitePaginator, self).__init__(object_list_plus, per_page, allow_empty_first_page, link_template)
116         self.offset = offset
117
118     def validate_number(self, number):
119         super(FinitePaginator, self).validate_number(number)
120         # check for an empty list to see if the page exists
121         if not self.object_list:
122             if number == 1 and self.allow_empty_first_page:
123                 pass
124             else:
125                 raise EmptyPage('That page contains no results')
126         return number
127
128     def page(self, number):
129         "Returns a Page object for the given 1-based page number."
130         number = self.validate_number(number)
131         # remove the extra item(s) when creating the page
132         page_items = self.object_list[:self.per_page]
133         return FinitePage(page_items, number, self)
134
135 class FinitePage(InfinitePage):
136
137     def has_next(self):
138         "Checks for one more item than last on this page."
139         try:
140             next_item = self.paginator.object_list[self.paginator.per_page]
141         except IndexError:
142             return False
143         return True
144
145     def start_index(self):
146         """
147         Returns the 1-based index of the first object on this page,
148         relative to total objects in the paginator.
149         """
150         ## TODO should this holler if you haven't defined the offset?
151         return self.paginator.offset