Stateful paginator, digg style

  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
280
281
282
283
284
285
import math
from django.core.paginator import \
    ObjectPaginator as DjangoPaginator,\
    InvalidPage
from django.http import Http404

class Paginator(object):
    """
    Much like Django's ``ObjectPaginator`` (which it uses), but always
    represents a specific current page instead of just providing an interface
    to the data. Based on the active page it makes a number of properties
    available which mostly work exactly like the context that the
    ``object_list`` generic view provides.

    ``__init__`` takes the same arguments as ``ObjectPaginator``, plus an
    additional parameter ``page`` to initialize the active page number. It does
    not need to be an int, i.e. it can come directly from ``request.GET``; if
    conversion to an integer fails, a ``Http404`` exception is raised.
    
    You can also later assign to to ``page`` attribute.
    
    >>> items = range(1,6789)
    >>> paginator = Paginator(items, num_per_page=10, page=3)
    >>> paginator.items
    [21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
    >>> paginator.is_paginated
    True
    >>> paginator.results_per_page
    10
    >>> paginator.has_next
    True
    >>> paginator.has_previous
    True
    >>> paginator.page
    3
    >>> paginator.page0
    2
    >>> paginator.next
    4
    >>> paginator.previous
    2
    >>> paginator.last_on_page
    30
    >>> paginator.first_on_page
    21
    >>> paginator.pages
    679
    >>> paginator.hits
    6788
    """
    def __init__(self, *args, **kwargs):
        page = kwargs.pop('page', 1)
        self._paginator = DjangoPaginator(*args, **kwargs)
        
        # Resolve the page number; this is similar to what the Django
        # ``object_list`` generic view is doing.
        try:
            self.page = int(page)
        except ValueError:
            if page == 'last': self.page = self._paginator.pages
            else: raise Http404
        
    def _set_page(self, new_page):
        self.__page = new_page
        try:
            self.object_list = self._paginator.get_page(new_page-1)
        except InvalidPage:
            if new_page != -1: raise Http404
            else: self.object_list = []   # allow empty
        self.items = self.object_list # alias
        # update all exposed info for the new page number
        self.update_attrs()
    def _get_page(self): return self.__page
    page = property(_get_page, _set_page)

    def update_attrs(self):
        """Override in descendants to set custom fields."""
        self.page0 = self.page-1
        self.is_paginated = self._paginator.pages > 1
        self.results_per_page = self._paginator.num_per_page
        self.has_next = self._paginator.has_next_page(self.page-1)
        self.has_previous = self._paginator.has_previous_page(self.page-1)
        self.next = self.page+1
        self.previous = self.page-1
        self.last_on_page = self._paginator.last_on_page(self.page-1)
        self.first_on_page = self._paginator.first_on_page(self.page-1)
        self.pages = self._paginator.pages
        self.hits = self._paginator.hits
        self.page_range = self._paginator.page_range
    
class DiggPaginator(Paginator):
    """
    Adds attributes to enable Digg-style formatting, 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, to be used in the same manner
    as the default:
    
    {% for page in paginator.leading_range %} ...
    {% for page in paginator.main_range %} ...
    {% for page in paginator.trailing_range %} ...
    
    Additionally, ``page_range`` contains a nun-numeric ``False`` element
    for every transition between two ranges.
    
    {% for page in paginator.page_range %}
        {% if not page %} ...
        {% else %}{{ page }}
        {% 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.
    
    # 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
    
    # 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.margin = kwargs.pop('margin', 4)  # todo: make 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 update_attrs(self):
        super(DiggPaginator, self).update_attrs()
        # easier access
        page, pages, body, tail, padding, margin = \
            self.page, self.pages, self.body, self.tail, self.padding,\
            self.margin

        # put active page in middle of main range
        main_range = map(int, [
            math.floor(page-body/2.0)+1,  # +1 = shift odd body to right
            math.floor(page+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] > pages:
            main_range = map((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(page+padding, main_range[1]))]
            main_range[0] = 1
        else:
            leading = range(1, tail+1)
        # basically same for trailing range...
        if main_range[1] >= 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, pages]
            else:
                main_range = [min(pages-body+1, max(page-padding, main_range[0])), pages]
        else:
            trailing = range(pages-tail+1, 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], pages)]
            
        # set attributes
        self.main_range = range(main_range[0], main_range[1]+1)
        self.leading_range = leading
        self.trailing_range = trailing
        self.page_range = reduce(lambda x, y: x+((x and y) and [False])+y,
            [self.leading_range, self.main_range, self.trailing_range])
        
    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))]))

if __name__ == "__main__":
    import doctest
    doctest.testmod()

More like this

  1. yet another digg style paginator by akonsu 4 years, 6 months ago
  2. Page numbers with ... like in Digg by Ciantic 5 years ago
  3. Improved many-page pagination by dokterbob 3 years, 7 months ago
  4. Pretty Paginator Tag by sbaechler 2 years, 2 months ago
  5. Digg-style pagination, and retain GET params by hanson2010 1 year, 10 months ago

Comments

mamat (on March 17, 2008):

quite nice!

i got used to using generic views so it'd be quite nice to add a 'list_detail' function... this way all that would be needed to switch over would be to change the import statement.

#

duncanm (on April 22, 2008):

I get the error: SyntaxError at /menu/ invalid syntax (views.py, line 18)

Line 18 is: class Paginator(object):

How do I fix this please?

Thanks Duncan

#

duncanm (on April 22, 2008):

Sorry scratch the above it was a missing )

I have correctly implemented this but I now get Page no found at /menu/? when it was working correctly before?

#

(Forgotten your password?)