4 from sets import Set as set
6 from django import template
7 from django.http import Http404
8 from django.core.paginator import Paginator, InvalidPage
9 from django.conf import settings
11 register = template.Library()
13 DEFAULT_PAGINATION = getattr(settings, 'PAGINATION_DEFAULT_PAGINATION', 20)
14 DEFAULT_WINDOW = getattr(settings, 'PAGINATION_DEFAULT_WINDOW', 4)
15 DEFAULT_ORPHANS = getattr(settings, 'PAGINATION_DEFAULT_ORPHANS', 0)
16 INVALID_PAGE_RAISES_404 = getattr(settings,
17 'PAGINATION_INVALID_PAGE_RAISES_404', False)
18 DISPLAY_PAGE_LINKS = getattr(settings, 'PAGINATION_DISPLAY_PAGE_LINKS', True)
19 PREVIOUS_LINK_DECORATOR = getattr(settings, 'PAGINATION_PREVIOUS_LINK_DECORATOR', "‹‹ ")
20 NEXT_LINK_DECORATOR = getattr(settings, 'PAGINATION_NEXT_LINK_DECORATOR', " ››")
21 DISPLAY_DISABLED_PREVIOUS_LINK = getattr(settings, 'PAGINATION_DISPLAY_DISABLED_PREVIOUS_LINK', True)
22 DISPLAY_DISABLED_NEXT_LINK = getattr(settings, 'PAGINATION_DISPLAY_DISABLED_NEXT_LINK', True)
24 def do_autopaginate(parser, token):
26 Splits the arguments to the autopaginate tag and formats them correctly.
28 split = token.split_contents()
31 for i, bit in enumerate(split):
35 if as_index is not None:
37 context_var = split[as_index + 1]
39 raise template.TemplateSyntaxError("Context variable assignment " +
40 "must take the form of {%% %r object.example_set.all ... as " +
41 "context_var_name %%}" % split[0])
42 del split[as_index:as_index + 2]
44 return AutoPaginateNode(split[1])
46 return AutoPaginateNode(split[1], paginate_by=split[2],
47 context_var=context_var)
50 orphans = int(split[3])
52 raise template.TemplateSyntaxError(u'Got %s, but expected integer.'
54 return AutoPaginateNode(split[1], paginate_by=split[2], orphans=orphans,
55 context_var=context_var)
57 raise template.TemplateSyntaxError('%r tag takes one required ' +
58 'argument and one optional argument' % split[0])
60 class AutoPaginateNode(template.Node):
62 Emits the required objects to allow for Digg-style pagination.
64 First, it looks in the current context for the variable specified, and using
65 that object, it emits a simple ``Paginator`` and the current page object
66 into the context names ``paginator`` and ``page_obj``, respectively.
68 It will then replace the variable specified with only the objects for the
73 It is recommended to use *{% paginate %}* after using the autopaginate
74 tag. If you choose not to use *{% paginate %}*, make sure to display the
75 list of available pages, or else the application may seem to be buggy.
77 def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION,
78 orphans=DEFAULT_ORPHANS, context_var=None):
79 self.queryset_var = template.Variable(queryset_var)
80 if isinstance(paginate_by, int):
81 self.paginate_by = paginate_by
83 self.paginate_by = template.Variable(paginate_by)
84 self.orphans = orphans
85 self.context_var = context_var
87 def render(self, context):
88 key = self.queryset_var.var
89 value = self.queryset_var.resolve(context)
90 if isinstance(self.paginate_by, int):
91 paginate_by = self.paginate_by
93 paginate_by = self.paginate_by.resolve(context)
94 paginator = Paginator(value, paginate_by, self.orphans)
96 page_obj = paginator.page(context['request'].page)
98 if INVALID_PAGE_RAISES_404:
99 raise Http404('Invalid page requested. If DEBUG were set to ' +
100 'False, an HTTP 404 page would have been shown instead.')
102 context['invalid_page'] = True
104 if self.context_var is not None:
105 context[self.context_var] = page_obj.object_list
107 context[key] = page_obj.object_list
108 context['paginator'] = paginator
109 context['page_obj'] = page_obj
113 def paginate(context, window=DEFAULT_WINDOW, hashtag=''):
115 Renders the ``pagination/pagination.html`` template, resulting in a
116 Digg-like display of the available pages, given the current page. If there
117 are too many pages to be displayed before and after the current page, then
118 elipses will be used to indicate the undisplayed gap between page numbers.
120 Requires one argument, ``context``, which should be a dictionary-like data
121 structure and must contain the following keys:
124 A ``Paginator`` or ``QuerySetPaginator`` object.
127 This should be the result of calling the page method on the
128 aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
131 This same ``context`` dictionary-like data structure may also include:
134 A dictionary of all of the **GET** parameters in the current request.
135 This is useful to maintain certain types of state, even when requesting
138 ``pagination_template``
139 A custom template to include in place of the default ``pagination.html``
144 paginator = context['paginator']
145 page_obj = context['page_obj']
146 page_range = paginator.page_range
147 pagination_template = context.get('pagination_template', 'pagination/default.html')
148 # Calculate the record range in the current page for display.
149 records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
150 records['last'] = records['first'] + paginator.per_page - 1
151 if records['last'] + paginator.orphans >= paginator.count:
152 records['last'] = paginator.count
153 # First and last are simply the first *n* pages and the last *n* pages,
154 # where *n* is the current window size.
155 first = set(page_range[:window])
156 last = set(page_range[-window:])
157 # Now we look around our current page, making sure that we don't wrap
159 current_start = page_obj.number-1-window
160 if current_start < 0:
162 current_end = page_obj.number-1+window
165 current = set(page_range[current_start:current_end])
167 # If there's no overlap between the first set of pages and the current
168 # set of pages, then there's a possible need for elusion.
169 if len(first.intersection(current)) == 0:
170 first_list = list(first)
172 second_list = list(current)
174 pages.extend(first_list)
175 diff = second_list[0] - first_list[-1]
176 # If there is a gap of two, between the last page of the first
177 # set and the first page of the current set, then we're missing a
180 pages.append(second_list[0] - 1)
181 # If the difference is just one, then there's nothing to be done,
182 # as the pages need no elusion and are correct.
185 # Otherwise, there's a bigger gap which needs to be signaled for
186 # elusion, by pushing a None value to the page list.
189 pages.extend(second_list)
191 unioned = list(first.union(current))
193 pages.extend(unioned)
194 # If there's no overlap between the current set of pages and the last
195 # set of pages, then there's a possible need for elusion.
196 if len(current.intersection(last)) == 0:
197 second_list = list(last)
199 diff = second_list[0] - pages[-1]
200 # If there is a gap of two, between the last page of the current
201 # set and the first page of the last set, then we're missing a
204 pages.append(second_list[0] - 1)
205 # If the difference is just one, then there's nothing to be done,
206 # as the pages need no elusion and are correct.
209 # Otherwise, there's a bigger gap which needs to be signaled for
210 # elusion, by pushing a None value to the page list.
213 pages.extend(second_list)
215 differenced = list(last.difference(current))
217 pages.extend(differenced)
219 'MEDIA_URL': settings.MEDIA_URL,
222 'page_obj': page_obj,
223 'paginator': paginator,
225 'is_paginated': paginator.count > paginator.per_page,
226 'display_page_links': DISPLAY_PAGE_LINKS,
227 'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
228 'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
229 'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
230 'next_link_decorator': NEXT_LINK_DECORATOR,
231 'pagination_template': pagination_template,
233 if 'request' in context:
234 getvars = context['request'].GET.copy()
235 if 'page' in getvars:
237 if len(getvars.keys()) > 0:
238 to_return['getvars'] = "&%s" % getvars.urlencode()
240 to_return['getvars'] = ''
242 except KeyError, AttributeError:
245 register.inclusion_tag('pagination/pagination.html', takes_context=True)(
247 register.tag('autopaginate', do_autopaginate)