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()