Custom pagination templates
[django-pagination.git] / pagination / templatetags / pagination_tags.py
1 try:
2     set
3 except NameError:
4     from sets import Set as set
5
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
10
11 register = template.Library()
12
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)
23
24 def do_autopaginate(parser, token):
25     """
26     Splits the arguments to the autopaginate tag and formats them correctly.
27     """
28     split = token.split_contents()
29     as_index = None
30     context_var = None
31     for i, bit in enumerate(split):
32         if bit == 'as':
33             as_index = i
34             break
35     if as_index is not None:
36         try:
37             context_var = split[as_index + 1]
38         except IndexError:
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]
43     if len(split) == 2:
44         return AutoPaginateNode(split[1])
45     elif len(split) == 3:
46         return AutoPaginateNode(split[1], paginate_by=split[2], 
47             context_var=context_var)
48     elif len(split) == 4:
49         try:
50             orphans = int(split[3])
51         except ValueError:
52             raise template.TemplateSyntaxError(u'Got %s, but expected integer.'
53                 % split[3])
54         return AutoPaginateNode(split[1], paginate_by=split[2], orphans=orphans,
55             context_var=context_var)
56     else:
57         raise template.TemplateSyntaxError('%r tag takes one required ' +
58             'argument and one optional argument' % split[0])
59
60 class AutoPaginateNode(template.Node):
61     """
62     Emits the required objects to allow for Digg-style pagination.
63     
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.
67     
68     It will then replace the variable specified with only the objects for the
69     current page.
70     
71     .. note::
72         
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.
76     """
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
82         else:
83             self.paginate_by = template.Variable(paginate_by)
84         self.orphans = orphans
85         self.context_var = context_var
86
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
92         else:
93             paginate_by = self.paginate_by.resolve(context)
94         paginator = Paginator(value, paginate_by, self.orphans)
95         try:
96             page_obj = paginator.page(context['request'].page)
97         except InvalidPage:
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.')
101             context[key] = []
102             context['invalid_page'] = True
103             return u''
104         if self.context_var is not None:
105             context[self.context_var] = page_obj.object_list
106         else:
107             context[key] = page_obj.object_list
108         context['paginator'] = paginator
109         context['page_obj'] = page_obj
110         return u''
111
112
113 def paginate(context, window=DEFAULT_WINDOW, hashtag=''):
114     """
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.
119     
120     Requires one argument, ``context``, which should be a dictionary-like data
121     structure and must contain the following keys:
122     
123     ``paginator``
124         A ``Paginator`` or ``QuerySetPaginator`` object.
125     
126     ``page_obj``
127         This should be the result of calling the page method on the 
128         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
129         the current page.
130     
131     This same ``context`` dictionary-like data structure may also include:
132     
133     ``getvars``
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
136         a different page.
137     
138     ``pagination_template``
139         A custom template to include in place of the default ``pagination.html`` 
140         contents.
141         
142     """
143     try:
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
158         # around.
159         current_start = page_obj.number-1-window
160         if current_start < 0:
161             current_start = 0
162         current_end = page_obj.number-1+window
163         if current_end < 0:
164             current_end = 0
165         current = set(page_range[current_start:current_end])
166         pages = []
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)
171             first_list.sort()
172             second_list = list(current)
173             second_list.sort()
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
178             # page.
179             if diff == 2:
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.
183             elif diff == 1:
184                 pass
185             # Otherwise, there's a bigger gap which needs to be signaled for
186             # elusion, by pushing a None value to the page list.
187             else:
188                 pages.append(None)
189             pages.extend(second_list)
190         else:
191             unioned = list(first.union(current))
192             unioned.sort()
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)
198             second_list.sort()
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 
202             # page.
203             if diff == 2:
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.
207             elif diff == 1:
208                 pass
209             # Otherwise, there's a bigger gap which needs to be signaled for
210             # elusion, by pushing a None value to the page list.
211             else:
212                 pages.append(None)
213             pages.extend(second_list)
214         else:
215             differenced = list(last.difference(current))
216             differenced.sort()
217             pages.extend(differenced)
218         to_return = {
219             'MEDIA_URL': settings.MEDIA_URL,
220             'pages': pages,
221             'records': records,
222             'page_obj': page_obj,
223             'paginator': paginator,
224             'hashtag': hashtag,
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,
232         }
233         if 'request' in context:
234             getvars = context['request'].GET.copy()
235             if 'page' in getvars:
236                 del getvars['page']
237             if len(getvars.keys()) > 0:
238                 to_return['getvars'] = "&%s" % getvars.urlencode()
239             else:
240                 to_return['getvars'] = ''
241         return to_return
242     except KeyError, AttributeError:
243         return {}
244
245 register.inclusion_tag('pagination/pagination.html', takes_context=True)(
246     paginate)
247 register.tag('autopaginate', do_autopaginate)