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