annotate core/paginator.py @ 661:15dbe0ccda95

Prevent exceptions when viewing downloads in the admin when the file doesn't exist on the filesystem. This is usually seen in development but can also happen in production if the file is missing.
author Brian Neal <bgneal@gmail.com>
date Tue, 14 May 2013 21:02:47 -0500
parents ee87ea74d46b
children
rev   line source
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()