1 | """Pagination for Collections and ORMs |
---|
2 | |
---|
3 | The Pagination module aids in the process of paging large collections of |
---|
4 | objects. It can be used macro-style for automatic fetching of large collections |
---|
5 | using one of the ORM wrappers, or handle a large collection responding to |
---|
6 | standard Python list slicing operations. These methods can also be used |
---|
7 | individually and customized to do as much or little as desired. |
---|
8 | |
---|
9 | The Paginator itself maintains pagination logic associated with each page, where |
---|
10 | it begins, what the first/last item on the page is, etc. |
---|
11 | |
---|
12 | Helper functions hook-up the Paginator in more conveinent methods for the more |
---|
13 | macro-style approach to return the Paginator and the slice of the collection |
---|
14 | desired. |
---|
15 | |
---|
16 | """ |
---|
17 | from routes import request_config |
---|
18 | from orm import get_wrapper |
---|
19 | |
---|
20 | def paginate(collection, page=None, per_page=10, item_count=None, *args, **options): |
---|
21 | """Paginate a collection of data |
---|
22 | |
---|
23 | If the collection is a list, it will return the slice of the list along |
---|
24 | with the Paginator object. If the collection is given using an ORM, the |
---|
25 | collection argument must be a partial representing the function to be |
---|
26 | used that will generate the proper query and extend properly for the |
---|
27 | limit/offset. |
---|
28 | |
---|
29 | Example:: |
---|
30 | |
---|
31 | # In this case, Person is a SQLObject class, or it could be a list/tuple |
---|
32 | person_paginator, person_set = paginate(Person, page=1) |
---|
33 | |
---|
34 | set_count = int(person_paginator.current) |
---|
35 | total_pages = len(person_paginator) |
---|
36 | |
---|
37 | Current ORM support is limited to SQLObject and SQLAlchemy. You can use any ORM |
---|
38 | you'd like with the Paginator as it will give you the offset/limit data necessary |
---|
39 | to make your own query. |
---|
40 | |
---|
41 | **WARNING:** Unless you pass in an item_count, a count will be performed on the |
---|
42 | collection every time paginate is called. If using an ORM, it's suggested that |
---|
43 | you count the items yourself and/or cache them. |
---|
44 | |
---|
45 | """ |
---|
46 | collection = get_wrapper(collection, *args, **options) |
---|
47 | if not item_count: |
---|
48 | item_count = len(collection) |
---|
49 | paginator = Paginator(item_count, per_page, page) |
---|
50 | subset = collection[paginator.current.first_item:paginator.current.last_item] |
---|
51 | |
---|
52 | return paginator, subset |
---|
53 | |
---|
54 | |
---|
55 | class Paginator(object): |
---|
56 | """Tracks paginated sets of data, and supplies common pagination operations |
---|
57 | |
---|
58 | The Paginator tracks data associated with pagination of groups of data, as well |
---|
59 | as supplying objects and methods that make dealing with paginated results easier. |
---|
60 | |
---|
61 | A Paginator supports list operations, including item fetching, length, iteration, |
---|
62 | and the 'in' operation. Each item in the Paginator is a Page object representing |
---|
63 | data about that specific page in the set of paginated data. As with the standard |
---|
64 | Python list, the Paginator list index starts at 0. |
---|
65 | |
---|
66 | """ |
---|
67 | def __init__(self, item_count, items_per_page=10, current_page=0): |
---|
68 | """Initialize a Paginator with the item count specified.""" |
---|
69 | self.item_count = item_count |
---|
70 | self.items_per_page = items_per_page |
---|
71 | self.pages = {} |
---|
72 | self.current_page = current_page |
---|
73 | |
---|
74 | def current(): |
---|
75 | doc = """\ |
---|
76 | Page object currently being displayed |
---|
77 | |
---|
78 | When assigning to the current page, it will set the page number for this page |
---|
79 | and create it if needed. If the page is a Page object and does not belong to |
---|
80 | this paginator, an AttributeError will be raised. |
---|
81 | |
---|
82 | """ |
---|
83 | def fget(self): |
---|
84 | return self[int(self.current_page)] |
---|
85 | def fset(self, page): |
---|
86 | if isinstance(page, Page) and page.paginator != self: |
---|
87 | raise AttributeError("Page/Paginator mismatch") |
---|
88 | page = int(page) |
---|
89 | self.current_page = page in self and page or 0 |
---|
90 | return locals() |
---|
91 | current = property(**current()) |
---|
92 | |
---|
93 | def __len__(self): |
---|
94 | return (self.item_count == 0) and 0 or (((self.item_count - 1)//self.items_per_page) + 1) |
---|
95 | |
---|
96 | def __iter__(self): |
---|
97 | for i in range(0, len(self)): |
---|
98 | yield self[i] |
---|
99 | |
---|
100 | def __getitem__(self, index): |
---|
101 | # Handle negative indexing like a normal list |
---|
102 | if index < 0: |
---|
103 | index = len(self) + index |
---|
104 | |
---|
105 | if index < 0: |
---|
106 | index = 0 |
---|
107 | |
---|
108 | if index not in self and index != 0: |
---|
109 | raise IndexError, "list index out of range" |
---|
110 | |
---|
111 | return self.pages.setdefault(index, Page(self, index)) |
---|
112 | |
---|
113 | def __contains__(self, value): |
---|
114 | if value >= 0 and value <= (len(self) - 1): |
---|
115 | return True |
---|
116 | return False |
---|
117 | |
---|
118 | class Page(object): |
---|
119 | """Represents a single page from a paginated set.""" |
---|
120 | def __init__(self, paginator, number): |
---|
121 | """Creates a new Page for the given ``paginator`` with the index ``number``.""" |
---|
122 | self.paginator = paginator |
---|
123 | self.number = int(number) |
---|
124 | |
---|
125 | def __int__(self): |
---|
126 | return self.number |
---|
127 | |
---|
128 | def __eq__(self, page): |
---|
129 | return self.paginator == page.paginator and self.number == page.number |
---|
130 | |
---|
131 | def __cmp__(self, page): |
---|
132 | return cmp(self.number, page.number) |
---|
133 | |
---|
134 | def offset(): |
---|
135 | doc = """Offset of the page, useful for database queries.""" |
---|
136 | def fget(self): |
---|
137 | return self.paginator.items_per_page * self.number |
---|
138 | return locals() |
---|
139 | offset = property(**offset()) |
---|
140 | |
---|
141 | def first_item(): |
---|
142 | doc = """The number of the first item in the page.""" |
---|
143 | def fget(self): |
---|
144 | return self.offset |
---|
145 | return locals() |
---|
146 | first_item = property(**first_item()) |
---|
147 | |
---|
148 | def last_item(): |
---|
149 | doc = """The number of the last item in the page.""" |
---|
150 | def fget(self): |
---|
151 | return min(self.paginator.items_per_page * (self.number + 1), |
---|
152 | self.paginator.item_count) |
---|
153 | return locals() |
---|
154 | last_item = property(**last_item()) |
---|
155 | |
---|
156 | def first(): |
---|
157 | doc = """Boolean indiciating if this page is the first.""" |
---|
158 | def fget(self): |
---|
159 | return self == self.paginator[0] |
---|
160 | return locals() |
---|
161 | first = property(**first()) |
---|
162 | |
---|
163 | def last(): |
---|
164 | doc = """Boolean indicating if this page is the last.""" |
---|
165 | def fget(self): |
---|
166 | return self == self.paginator[-1] |
---|
167 | return locals() |
---|
168 | last = property(**last()) |
---|
169 | |
---|
170 | def previous(): |
---|
171 | doc = """Previous page if it exists, None otherwise.""" |
---|
172 | def fget(self): |
---|
173 | if self.first: |
---|
174 | return None |
---|
175 | return self.paginator[self.number - 1] |
---|
176 | return locals() |
---|
177 | previous = property(**previous()) |
---|
178 | |
---|
179 | def next(): |
---|
180 | doc = """Next page if it exists, None otherwise.""" |
---|
181 | def fget(self): |
---|
182 | if self.last: |
---|
183 | return None |
---|
184 | return self.paginator[self.number + 1] |
---|
185 | return locals() |
---|
186 | next = property(**next()) |
---|
187 | |
---|
188 | def window(self, padding = 2): |
---|
189 | return Window(self, padding) |
---|
190 | |
---|
191 | def __repr__(self): |
---|
192 | return str(self.number) |
---|
193 | |
---|
194 | class Window(object): |
---|
195 | """Represents ranges around a given page.""" |
---|
196 | def __init__(self, page, padding = 2): |
---|
197 | """Creates a new Window object for the given ``page`` with the specified ``padding``.""" |
---|
198 | self.paginator = page.paginator |
---|
199 | self.page = page |
---|
200 | self.padding = padding |
---|
201 | |
---|
202 | def padding(): |
---|
203 | doc = """Sets the window's padding (the number of pages on either side of the window page).""" |
---|
204 | def fset(self, padding): |
---|
205 | self._padding = padding |
---|
206 | if padding < 0: self._padding = 0 |
---|
207 | first_page_in_window = self.page.number - self._padding |
---|
208 | self.first = first_page_in_window in self.paginator and ( |
---|
209 | self.paginator[first_page_in_window]) or self.paginator[0] |
---|
210 | last_page_in_window = self.page.number + self._padding |
---|
211 | self.last = last_page_in_window in self.paginator and ( |
---|
212 | self.paginator[last_page_in_window]) or self.paginator[-1] |
---|
213 | def fget(self): |
---|
214 | return self._padding |
---|
215 | return locals() |
---|
216 | padding = property(**padding()) |
---|
217 | |
---|
218 | def pages(): |
---|
219 | doc = """Returns a list of Page objects in the current window.""" |
---|
220 | def fget(self): |
---|
221 | return [self.paginator[page_number] for page_number in |
---|
222 | range(self.first.number, self.last.number+1)] |
---|
223 | return locals() |
---|
224 | pages = property(**pages()) |
---|
225 | |
---|
226 | def __add__(self, window): |
---|
227 | if window.paginator != self.paginator: |
---|
228 | raise AttributeError("Window/paginator mismatch") |
---|
229 | assert self.last >= window.first |
---|
230 | return Window(self.page.next, padding=self.padding+1) |
---|