Mercurial > public > sg101
comparison core/paginator.py @ 581:ee87ea74d46b
For Django 1.4, rearranged project structure for new manage.py.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Sat, 05 May 2012 17:10:48 -0500 |
parents | gpp/core/paginator.py@dbd703f7d63a |
children |
comparison
equal
deleted
inserted
replaced
580:c525f3e0b5d0 | 581:ee87ea74d46b |
---|---|
1 """ | |
2 Digg.com style paginator. | |
3 References: | |
4 http://www.djangosnippets.org/snippets/773/ | |
5 http://blog.elsdoerfer.name/2008/05/26/diggpaginator-update/ | |
6 http://blog.elsdoerfer.name/2008/03/06/yet-another-paginator-digg-style/ | |
7 """ | |
8 import math | |
9 from django.core.paginator import \ | |
10 Paginator, QuerySetPaginator, Page, InvalidPage | |
11 | |
12 __all__ = ( | |
13 'InvalidPage', | |
14 'ExPaginator', | |
15 'DiggPaginator', | |
16 'QuerySetDiggPaginator', | |
17 ) | |
18 | |
19 class ExPaginator(Paginator): | |
20 """Adds a ``softlimit`` option to ``page()``. If True, querying a | |
21 page number larger than max. will not fail, but instead return the | |
22 last available page. | |
23 | |
24 This is useful when the data source can not provide an exact count | |
25 at all times (like some search engines), meaning the user could | |
26 possibly see links to invalid pages at some point which we wouldn't | |
27 want to fail as 404s. | |
28 | |
29 >>> items = range(1, 1000) | |
30 >>> paginator = ExPaginator(items, 10) | |
31 >>> paginator.page(1000) | |
32 Traceback (most recent call last): | |
33 InvalidPage: That page contains no results | |
34 >>> paginator.page(1000, softlimit=True) | |
35 <Page 100 of 100> | |
36 | |
37 # [bug] graceful handling of non-int args | |
38 >>> paginator.page("str") | |
39 Traceback (most recent call last): | |
40 InvalidPage: That page number is not an integer | |
41 """ | |
42 def _ensure_int(self, num, e): | |
43 # see Django #7307 | |
44 try: | |
45 return int(num) | |
46 except ValueError: | |
47 raise e | |
48 | |
49 def page(self, number, softlimit=False): | |
50 try: | |
51 return super(ExPaginator, self).page(number) | |
52 except InvalidPage, e: | |
53 number = self._ensure_int(number, e) | |
54 if number > self.num_pages and softlimit: | |
55 return self.page(self.num_pages, softlimit=False) | |
56 else: | |
57 raise e | |
58 | |
59 class DiggPaginator(ExPaginator): | |
60 """ | |
61 Based on Django's default paginator, it adds "Digg-style" page ranges | |
62 with a leading block of pages, an optional middle block, and another | |
63 block at the end of the page range. They are available as attributes | |
64 on the page: | |
65 | |
66 {# with: page = digg_paginator.page(1) #} | |
67 {% for num in page.leading_range %} ... | |
68 {% for num in page.main_range %} ... | |
69 {% for num in page.trailing_range %} ... | |
70 | |
71 Additionally, ``page_range`` contains a nun-numeric ``False`` element | |
72 for every transition between two ranges. | |
73 | |
74 {% for num in page.page_range %} | |
75 {% if not num %} ... {# literally output dots #} | |
76 {% else %}{{ num }} | |
77 {% endif %} | |
78 {% endfor %} | |
79 | |
80 Additional arguments passed to the constructor allow customization of | |
81 how those bocks are constructed: | |
82 | |
83 body=5, tail=2 | |
84 | |
85 [1] 2 3 4 5 ... 91 92 | |
86 |_________| |___| | |
87 body tail | |
88 |_____| | |
89 margin | |
90 | |
91 body=5, tail=2, padding=2 | |
92 | |
93 1 2 ... 6 7 [8] 9 10 ... 91 92 | |
94 |_| |__| | |
95 ^padding^ | |
96 |_| |__________| |___| | |
97 tail body tail | |
98 | |
99 ``margin`` is the minimum number of pages required between two ranges; if | |
100 there are less, they are combined into one. | |
101 | |
102 When ``align_left`` is set to ``True``, the paginator operates in a | |
103 special mode that always skips the right tail, e.g. does not display the | |
104 end block unless necessary. This is useful for situations in which the | |
105 exact number of items/pages is not actually known. | |
106 | |
107 # odd body length | |
108 >>> print DiggPaginator(range(1,1000), 10, body=5).page(1) | |
109 1 2 3 4 5 ... 99 100 | |
110 >>> print DiggPaginator(range(1,1000), 10, body=5).page(100) | |
111 1 2 ... 96 97 98 99 100 | |
112 | |
113 # even body length | |
114 >>> print DiggPaginator(range(1,1000), 10, body=6).page(1) | |
115 1 2 3 4 5 6 ... 99 100 | |
116 >>> print DiggPaginator(range(1,1000), 10, body=6).page(100) | |
117 1 2 ... 95 96 97 98 99 100 | |
118 | |
119 # leading range and main range are combined when close; note how | |
120 # we have varying body and padding values, and their effect. | |
121 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2).page(3) | |
122 1 2 3 4 5 ... 99 100 | |
123 >>> print DiggPaginator(range(1,1000), 10, body=6, padding=2, margin=2).page(4) | |
124 1 2 3 4 5 6 ... 99 100 | |
125 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2).page(6) | |
126 1 2 3 4 5 6 7 ... 99 100 | |
127 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2).page(7) | |
128 1 2 ... 5 6 7 8 9 ... 99 100 | |
129 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2).page(7) | |
130 1 2 ... 5 6 7 8 9 ... 99 100 | |
131 | |
132 # the trailing range works the same | |
133 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2, ).page(98) | |
134 1 2 ... 96 97 98 99 100 | |
135 >>> print DiggPaginator(range(1,1000), 10, body=6, padding=2, margin=2, ).page(97) | |
136 1 2 ... 95 96 97 98 99 100 | |
137 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2, ).page(95) | |
138 1 2 ... 94 95 96 97 98 99 100 | |
139 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2, ).page(94) | |
140 1 2 ... 92 93 94 95 96 ... 99 100 | |
141 >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2, ).page(94) | |
142 1 2 ... 92 93 94 95 96 ... 99 100 | |
143 | |
144 # all three ranges may be combined as well | |
145 >>> print DiggPaginator(range(1,151), 10, body=6, padding=2).page(7) | |
146 1 2 3 4 5 6 7 8 9 ... 14 15 | |
147 >>> print DiggPaginator(range(1,151), 10, body=6, padding=2).page(8) | |
148 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
149 >>> print DiggPaginator(range(1,151), 10, body=6, padding=1).page(8) | |
150 1 2 3 4 5 6 7 8 9 ... 14 15 | |
151 | |
152 # no leading or trailing ranges might be required if there are only | |
153 # a very small number of pages | |
154 >>> print DiggPaginator(range(1,80), 10, body=10).page(1) | |
155 1 2 3 4 5 6 7 8 | |
156 >>> print DiggPaginator(range(1,80), 10, body=10).page(8) | |
157 1 2 3 4 5 6 7 8 | |
158 >>> print DiggPaginator(range(1,12), 10, body=5).page(1) | |
159 1 2 | |
160 | |
161 # test left align mode | |
162 >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(1) | |
163 1 2 3 4 5 | |
164 >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(50) | |
165 1 2 ... 48 49 50 51 52 | |
166 >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(97) | |
167 1 2 ... 95 96 97 98 99 | |
168 >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(100) | |
169 1 2 ... 96 97 98 99 100 | |
170 | |
171 # padding: default value | |
172 >>> DiggPaginator(range(1,1000), 10, body=10).padding | |
173 4 | |
174 | |
175 # padding: automatic reduction | |
176 >>> DiggPaginator(range(1,1000), 10, body=5).padding | |
177 2 | |
178 >>> DiggPaginator(range(1,1000), 10, body=6).padding | |
179 2 | |
180 | |
181 # padding: sanity check | |
182 >>> DiggPaginator(range(1,1000), 10, body=5, padding=3) | |
183 Traceback (most recent call last): | |
184 ValueError: padding too large for body (max 2) | |
185 """ | |
186 def __init__(self, *args, **kwargs): | |
187 self.body = kwargs.pop('body', 10) | |
188 self.tail = kwargs.pop('tail', 2) | |
189 self.align_left = kwargs.pop('align_left', False) | |
190 self.margin = kwargs.pop('margin', 4) # TODO: make the default relative to body? | |
191 # validate padding value | |
192 max_padding = int(math.ceil(self.body/2.0)-1) | |
193 self.padding = kwargs.pop('padding', min(4, max_padding)) | |
194 if self.padding > max_padding: | |
195 raise ValueError('padding too large for body (max %d)'%max_padding) | |
196 super(DiggPaginator, self).__init__(*args, **kwargs) | |
197 | |
198 def page(self, number, *args, **kwargs): | |
199 """Return a standard ``Page`` instance with custom, digg-specific | |
200 page ranges attached. | |
201 """ | |
202 | |
203 page = super(DiggPaginator, self).page(number, *args, **kwargs) | |
204 number = int(number) # we know this will work | |
205 | |
206 # easier access | |
207 num_pages, body, tail, padding, margin = \ | |
208 self.num_pages, self.body, self.tail, self.padding, self.margin | |
209 | |
210 # put active page in middle of main range | |
211 main_range = map(int, [ | |
212 math.floor(number-body/2.0)+1, # +1 = shift odd body to right | |
213 math.floor(number+body/2.0)]) | |
214 # adjust bounds | |
215 if main_range[0] < 1: | |
216 main_range = map(abs(main_range[0]-1).__add__, main_range) | |
217 if main_range[1] > num_pages: | |
218 main_range = map((num_pages-main_range[1]).__add__, main_range) | |
219 | |
220 # Determine leading and trailing ranges; if possible and appropriate, | |
221 # combine them with the main range, in which case the resulting main | |
222 # block might end up considerable larger than requested. While we | |
223 # can't guarantee the exact size in those cases, we can at least try | |
224 # to come as close as possible: we can reduce the other boundary to | |
225 # max padding, instead of using half the body size, which would | |
226 # otherwise be the case. If the padding is large enough, this will | |
227 # of course have no effect. | |
228 # Example: | |
229 # total pages=100, page=4, body=5, (default padding=2) | |
230 # 1 2 3 [4] 5 6 ... 99 100 | |
231 # total pages=100, page=4, body=5, padding=1 | |
232 # 1 2 3 [4] 5 ... 99 100 | |
233 # If it were not for this adjustment, both cases would result in the | |
234 # first output, regardless of the padding value. | |
235 if main_range[0] <= tail+margin: | |
236 leading = [] | |
237 main_range = [1, max(body, min(number+padding, main_range[1]))] | |
238 main_range[0] = 1 | |
239 else: | |
240 leading = range(1, tail+1) | |
241 # basically same for trailing range, but not in ``left_align`` mode | |
242 if self.align_left: | |
243 trailing = [] | |
244 else: | |
245 if main_range[1] >= num_pages-(tail+margin)+1: | |
246 trailing = [] | |
247 if not leading: | |
248 # ... but handle the special case of neither leading nor | |
249 # trailing ranges; otherwise, we would now modify the | |
250 # main range low bound, which we just set in the previous | |
251 # section, again. | |
252 main_range = [1, num_pages] | |
253 else: | |
254 main_range = [min(num_pages-body+1, max(number-padding, main_range[0])), num_pages] | |
255 else: | |
256 trailing = range(num_pages-tail+1, num_pages+1) | |
257 | |
258 # finally, normalize values that are out of bound; this basically | |
259 # fixes all the things the above code screwed up in the simple case | |
260 # of few enough pages where one range would suffice. | |
261 main_range = [max(main_range[0], 1), min(main_range[1], num_pages)] | |
262 | |
263 # make the result of our calculations available as custom ranges | |
264 # on the ``Page`` instance. | |
265 page.main_range = range(main_range[0], main_range[1]+1) | |
266 page.leading_range = leading | |
267 page.trailing_range = trailing | |
268 page.page_range = reduce(lambda x, y: x+((x and y) and [False])+y, | |
269 [page.leading_range, page.main_range, page.trailing_range]) | |
270 | |
271 page.__class__ = DiggPage | |
272 return page | |
273 | |
274 class DiggPage(Page): | |
275 def __str__(self): | |
276 return " ... ".join(filter(None, [ | |
277 " ".join(map(str, self.leading_range)), | |
278 " ".join(map(str, self.main_range)), | |
279 " ".join(map(str, self.trailing_range))])) | |
280 | |
281 class QuerySetDiggPaginator(DiggPaginator, QuerySetPaginator): | |
282 pass | |
283 | |
284 if __name__ == "__main__": | |
285 import doctest | |
286 doctest.testmod() |