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