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