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