Test up to Django 3.2.
[django-pagination.git] / fnp_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         try:
52             del self._num_pages, self._count
53         except AttributeError:
54             # These are just cached properties anyway.
55             pass
56         # bonus links
57         self.link_template = link_template
58
59     def validate_number(self, number):
60         """
61         Validates the given 1-based page number.
62         """
63         try:
64             number = int(number)
65         except ValueError:
66             raise PageNotAnInteger('That page number is not an integer')
67         if number < 1:
68             raise EmptyPage('That page number is less than 1')
69         return number
70
71     def page(self, number):
72         """
73         Returns a Page object for the given 1-based page number.
74         """
75         number = self.validate_number(number)
76         bottom = (number - 1) * self.per_page
77         top = bottom + self.per_page
78         page_items = self.object_list[bottom:top]
79         # check moved from validate_number
80         if not page_items:
81             if number == 1 and self.allow_empty_first_page:
82                 pass
83             else:
84                 raise EmptyPage('That page contains no results')
85         return InfinitePage(page_items, number, self)
86
87     def _get_count(self):
88         """
89         Returns the total number of objects, across all pages.
90         """
91         raise NotImplementedError
92     count = property(_get_count)
93
94     def _get_num_pages(self):
95         """
96         Returns the total number of pages.
97         """
98         raise NotImplementedError
99     num_pages = property(_get_num_pages)
100
101     def _get_page_range(self):
102         """
103         Returns a 1-based range of pages for iterating through within
104         a template for loop.
105         """
106         raise NotImplementedError
107     page_range = property(_get_page_range)
108
109
110 class InfinitePage(Page):
111
112     def __repr__(self):
113         return '<Page %s>' % self.number
114
115     def has_next(self):
116         """
117         Checks for one more item than last on this page.
118         """
119         try:
120             self.paginator.object_list[self.number * self.paginator.per_page]
121         except IndexError:
122             return False
123         return True
124
125     def end_index(self):
126         """
127         Returns the 1-based index of the last object on this page,
128         relative to total objects found (hits).
129         """
130         return ((self.number - 1) * self.paginator.per_page +
131             len(self.object_list))
132
133     #Bonus methods for creating links
134
135     def next_link(self):
136         if self.has_next():
137             return self.paginator.link_template % (self.number + 1)
138         return None
139
140     def previous_link(self):
141         if self.has_previous():
142             return self.paginator.link_template % (self.number - 1)
143         return None
144
145
146 class FinitePaginator(InfinitePaginator):
147     """
148     Paginator for cases when the list of items is already finite.
149
150     A good example is a list generated from an API call. This is a subclass
151     of InfinitePaginator because we have no idea how many items exist in the
152     full collection.
153
154     To accurately determine if the next page exists, a FinitePaginator MUST be
155     created with an object_list_plus that may contain more items than the
156     per_page count.  Typically, you'll have an object_list_plus with one extra
157     item (if there's a next page).  You'll also need to supply the offset from
158     the full collection in order to get the page start_index.
159
160     This is a very silly class but useful if you love the Django pagination
161     conventions.
162     """
163
164     def __init__(self, object_list_plus, per_page, offset=None,
165         allow_empty_first_page=True, link_template='/page/%d/'):
166         super(FinitePaginator, self).__init__(object_list_plus, per_page,
167             allow_empty_first_page, link_template)
168         self.offset = offset
169
170     def validate_number(self, number):
171         super(FinitePaginator, self).validate_number(number)
172         # check for an empty list to see if the page exists
173         if not self.object_list:
174             if number == 1 and self.allow_empty_first_page:
175                 pass
176             else:
177                 raise EmptyPage('That page contains no results')
178         return number
179
180     def page(self, number):
181         """
182         Returns a Page object for the given 1-based page number.
183         """
184         number = self.validate_number(number)
185         # remove the extra item(s) when creating the page
186         page_items = self.object_list[:self.per_page]
187         return FinitePage(page_items, number, self)
188
189
190 class FinitePage(InfinitePage):
191
192     def has_next(self):
193         """
194         Checks for one more item than last on this page.
195         """
196         try:
197             self.paginator.object_list[self.paginator.per_page]
198         except IndexError:
199             return False
200         return True
201
202     def start_index(self):
203         """
204         Returns the 1-based index of the first object on this page,
205         relative to total objects in the paginator.
206         """
207         ## TODO should this holler if you haven't defined the offset?
208         return self.paginator.offset