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