Merge pull request #11 from stephane/master
[django-pagination.git] / linaro_django_pagination / paginator.py
1 # Copyright (c) 2008, Eric Florenzano
2 # Copyright (c) 2010, 2011 Linaro Limited
3 # All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions are
7 # met:
8 #
9 #     * Redistributions of source code must retain the above copyright
10 #       notice, this list of conditions and the following disclaimer.
11 #     * Redistributions in binary form must reproduce the above
12 #       copyright notice, this list of conditions and the following
13 #       disclaimer in the documentation and/or other materials provided
14 #       with the distribution.
15 #     * Neither the name of the author nor the names of other
16 #       contributors may be used to endorse or promote products derived
17 #       from this software without specific prior written permission.
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31
32 from django.core.paginator import Paginator, Page, PageNotAnInteger, EmptyPage
33
34
35 class InfinitePaginator(Paginator):
36     """
37     Paginator designed for cases when it's not important to know how many total
38     pages.  This is useful for any object_list that has no count() method or
39     can be used to improve performance for MySQL by removing counts.
40
41     The orphans parameter has been removed for simplicity and there's a link
42     template string for creating the links to the next and previous pages.
43     """
44
45     def __init__(self, object_list, per_page, allow_empty_first_page=True,
46         link_template='/page/%d/'):
47         orphans = 0  # no orphans
48         super(InfinitePaginator, self).__init__(object_list, per_page, orphans,
49             allow_empty_first_page)
50         # no count or num pages
51         del self._num_pages, self._count
52         # bonus links
53         self.link_template = link_template
54
55     def validate_number(self, number):
56         """
57         Validates the given 1-based page number.
58         """
59         try:
60             number = int(number)
61         except ValueError:
62             raise PageNotAnInteger('That page number is not an integer')
63         if number < 1:
64             raise EmptyPage('That page number is less than 1')
65         return number
66
67     def page(self, number):
68         """
69         Returns a Page object for the given 1-based page number.
70         """
71         number = self.validate_number(number)
72         bottom = (number - 1) * self.per_page
73         top = bottom + self.per_page
74         page_items = self.object_list[bottom:top]
75         # check moved from validate_number
76         if not page_items:
77             if number == 1 and self.allow_empty_first_page:
78                 pass
79             else:
80                 raise EmptyPage('That page contains no results')
81         return InfinitePage(page_items, number, self)
82
83     def _get_count(self):
84         """
85         Returns the total number of objects, across all pages.
86         """
87         raise NotImplementedError
88     count = property(_get_count)
89
90     def _get_num_pages(self):
91         """
92         Returns the total number of pages.
93         """
94         raise NotImplementedError
95     num_pages = property(_get_num_pages)
96
97     def _get_page_range(self):
98         """
99         Returns a 1-based range of pages for iterating through within
100         a template for loop.
101         """
102         raise NotImplementedError
103     page_range = property(_get_page_range)
104
105
106 class InfinitePage(Page):
107
108     def __repr__(self):
109         return '<Page %s>' % self.number
110
111     def has_next(self):
112         """
113         Checks for one more item than last on this page.
114         """
115         try:
116             self.paginator.object_list[self.number * self.paginator.per_page]
117         except IndexError:
118             return False
119         return True
120
121     def end_index(self):
122         """
123         Returns the 1-based index of the last object on this page,
124         relative to total objects found (hits).
125         """
126         return ((self.number - 1) * self.paginator.per_page +
127             len(self.object_list))
128
129     #Bonus methods for creating links
130
131     def next_link(self):
132         if self.has_next():
133             return self.paginator.link_template % (self.number + 1)
134         return None
135
136     def previous_link(self):
137         if self.has_previous():
138             return self.paginator.link_template % (self.number - 1)
139         return None
140
141
142 class FinitePaginator(InfinitePaginator):
143     """
144     Paginator for cases when the list of items is already finite.
145
146     A good example is a list generated from an API call. This is a subclass
147     of InfinitePaginator because we have no idea how many items exist in the
148     full collection.
149
150     To accurately determine if the next page exists, a FinitePaginator MUST be
151     created with an object_list_plus that may contain more items than the
152     per_page count.  Typically, you'll have an object_list_plus with one extra
153     item (if there's a next page).  You'll also need to supply the offset from
154     the full collection in order to get the page start_index.
155
156     This is a very silly class but useful if you love the Django pagination
157     conventions.
158     """
159
160     def __init__(self, object_list_plus, per_page, offset=None,
161         allow_empty_first_page=True, link_template='/page/%d/'):
162         super(FinitePaginator, self).__init__(object_list_plus, per_page,
163             allow_empty_first_page, link_template)
164         self.offset = offset
165
166     def validate_number(self, number):
167         super(FinitePaginator, self).validate_number(number)
168         # check for an empty list to see if the page exists
169         if not self.object_list:
170             if number == 1 and self.allow_empty_first_page:
171                 pass
172             else:
173                 raise EmptyPage('That page contains no results')
174         return number
175
176     def page(self, number):
177         """
178         Returns a Page object for the given 1-based page number.
179         """
180         number = self.validate_number(number)
181         # remove the extra item(s) when creating the page
182         page_items = self.object_list[:self.per_page]
183         return FinitePage(page_items, number, self)
184
185
186 class FinitePage(InfinitePage):
187
188     def has_next(self):
189         """
190         Checks for one more item than last on this page.
191         """
192         try:
193             self.paginator.object_list[self.paginator.per_page]
194         except IndexError:
195             return False
196         return True
197
198     def start_index(self):
199         """
200         Returns the 1-based index of the first object on this page,
201         relative to total objects in the paginator.
202         """
203         ## TODO should this holler if you haven't defined the offset?
204         return self.paginator.offset