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