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