7505b746581dc1900a94733f2b873a546cc4dd44
[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, 'PAGINATION_INVALID_PAGE_RAISES_404',
17     False)
18
19 def do_autopaginate(parser, token):
20     """
21     Splits the arguments to the autopaginate tag and formats them correctly.
22     """
23     split = token.split_contents()
24     as_index = None
25     context_var = None
26     for i, bit in enumerate(split):
27         if bit == 'as':
28             as_index = i
29             break
30     if as_index is not None:
31         try:
32             context_var = split[as_index + 1]
33         except IndexError:
34             raise template.TemplateSyntaxError("Context variable assignment " +\
35                 "must take the form of {%% %r object.example_set.all ... as " +\
36                 "context_var_name %%}" % split[0])
37         del split[as_index:as_index + 2]
38     if len(split) == 2:
39         return AutoPaginateNode(split[1])
40     elif len(split) == 3:
41         return AutoPaginateNode(split[1], paginate_by=split[2], 
42             context_var=context_var)
43     elif len(split) == 4:
44         try:
45             orphans = int(split[3])
46         except ValueError:
47             raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[3])
48         return AutoPaginateNode(split[1], paginate_by=split[2], orphans=orphans,
49             context_var=context_var)
50     else:
51         raise template.TemplateSyntaxError('%r tag takes one required ' + \
52             'argument and one optional argument' % split[0])
53
54 class AutoPaginateNode(template.Node):
55     """
56     Emits the required objects to allow for Digg-style pagination.
57     
58     First, it looks in the current context for the variable specified, and using
59     that object, it emits a simple ``Paginator`` and the current page object 
60     into the context names ``paginator`` and ``page_obj``, respectively.
61     
62     It will then replace the variable specified with only the objects for the
63     current page.
64     
65     .. note::
66         
67         It is recommended to use *{% paginate %}* after using the autopaginate
68         tag.  If you choose not to use *{% paginate %}*, make sure to display the
69         list of available pages, or else the application may seem to be buggy.
70     """
71     def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION,
72         orphans=DEFAULT_ORPHANS, context_var=None):
73         self.queryset_var = template.Variable(queryset_var)
74         if isinstance(paginate_by, int):
75             self.paginate_by = paginate_by
76         else:
77             self.paginate_by = template.Variable(paginate_by)
78         self.orphans = orphans
79         self.context_var = context_var
80
81     def render(self, context):
82         key = self.queryset_var.var
83         value = self.queryset_var.resolve(context)
84         if isinstance(self.paginate_by, int):
85             paginate_by = self.paginate_by
86         else:
87             paginate_by = self.paginate_by.resolve(context)
88         paginator = Paginator(value, paginate_by, self.orphans)
89         try:
90             page_obj = paginator.page(context['request'].page)
91         except InvalidPage:
92             if INVALID_PAGE_RAISES_404:
93                 raise Http404('Invalid page requested.  If DEBUG were set to ' +
94                     'False, an HTTP 404 page would have been shown instead.')
95             context[key] = []
96             context['invalid_page'] = True
97             return u''
98         if self.context_var is not None:
99             context[self.context_var] = page_obj.object_list
100         else:
101             context[key] = page_obj.object_list
102         context['paginator'] = paginator
103         context['page_obj'] = page_obj
104         return u''
105
106 def paginate(context, window=DEFAULT_WINDOW):
107     """
108     Renders the ``pagination/pagination.html`` template, resulting in a
109     Digg-like display of the available pages, given the current page.  If there
110     are too many pages to be displayed before and after the current page, then
111     elipses will be used to indicate the undisplayed gap between page numbers.
112     
113     Requires one argument, ``context``, which should be a dictionary-like data
114     structure and must contain the following keys:
115     
116     ``paginator``
117         A ``Paginator`` or ``QuerySetPaginator`` object.
118     
119     ``page_obj``
120         This should be the result of calling the page method on the 
121         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
122         the current page.
123     
124     This same ``context`` dictionary-like data structure may also include:
125     
126     ``getvars``
127         A dictionary of all of the **GET** parameters in the current request.
128         This is useful to maintain certain types of state, even when requesting
129         a different page.
130         """
131     try:
132         paginator = context['paginator']
133         page_obj = context['page_obj']
134         page_range = paginator.page_range
135         # First and last are simply the first *n* pages and the last *n* pages,
136         # where *n* is the current window size.
137         first = set(page_range[:window])
138         last = set(page_range[-window:])
139         # Now we look around our current page, making sure that we don't wrap
140         # around.
141         current_start = page_obj.number-1-window
142         if current_start < 0:
143             current_start = 0
144         current_end = page_obj.number-1+window
145         if current_end < 0:
146             current_end = 0
147         current = set(page_range[current_start:current_end])
148         pages = []
149         # If there's no overlap between the first set of pages and the current
150         # set of pages, then there's a possible need for elusion.
151         if len(first.intersection(current)) == 0:
152             first_list = list(first)
153             first_list.sort()
154             second_list = list(current)
155             second_list.sort()
156             pages.extend(first_list)
157             diff = second_list[0] - first_list[-1]
158             # If there is a gap of two, between the last page of the first
159             # set and the first page of the current set, then we're missing a
160             # page.
161             if diff == 2:
162                 pages.append(second_list[0] - 1)
163             # If the difference is just one, then there's nothing to be done,
164             # as the pages need no elusion and are correct.
165             elif diff == 1:
166                 pass
167             # Otherwise, there's a bigger gap which needs to be signaled for
168             # elusion, by pushing a None value to the page list.
169             else:
170                 pages.append(None)
171             pages.extend(second_list)
172         else:
173             unioned = list(first.union(current))
174             unioned.sort()
175             pages.extend(unioned)
176         # If there's no overlap between the current set of pages and the last
177         # set of pages, then there's a possible need for elusion.
178         if len(current.intersection(last)) == 0:
179             second_list = list(last)
180             second_list.sort()
181             diff = second_list[0] - pages[-1]
182             # If there is a gap of two, between the last page of the current
183             # set and the first page of the last set, then we're missing a 
184             # page.
185             if diff == 2:
186                 pages.append(second_list[0] - 1)
187             # If the difference is just one, then there's nothing to be done,
188             # as the pages need no elusion and are correct.
189             elif diff == 1:
190                 pass
191             # Otherwise, there's a bigger gap which needs to be signaled for
192             # elusion, by pushing a None value to the page list.
193             else:
194                 pages.append(None)
195             pages.extend(second_list)
196         else:
197             differenced = list(last.difference(current))
198             differenced.sort()
199             pages.extend(differenced)
200         to_return = {
201             'pages': pages,
202             'page_obj': page_obj,
203             'paginator': paginator,
204             'is_paginated': paginator.count > paginator.per_page,
205         }
206         if 'request' in context:
207             getvars = context['request'].GET.copy()
208             if 'page' in getvars:
209                 del getvars['page']
210             if len(getvars.keys()) > 0:
211                 to_return['getvars'] = "&%s" % getvars.urlencode()
212             else:
213                 to_return['getvars'] = ''
214         return to_return
215     except KeyError, AttributeError:
216         return {}
217 register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
218 register.tag('autopaginate', do_autopaginate)