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