Fixed bug with extra getvars where an ampersand was always appended.
[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
9 register = template.Library()
10
11 DEFAULT_PAGINATION = 20
12 DEFAULT_WINDOW = 4
13
14 def do_autopaginate(parser, token):
15     """
16     Splits the arguments to the autopaginate tag and formats them correctly.
17     """
18     split = token.split_contents()
19     if len(split) == 2:
20         return AutoPaginateNode(split[1])
21     elif len(split) == 3:
22         try:
23             paginate_by = int(split[2])
24         except ValueError:
25             raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
26         return AutoPaginateNode(split[1], paginate_by=paginate_by)
27     else:
28         raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])
29
30 class AutoPaginateNode(template.Node):
31     """
32     Emits the required objects to allow for Digg-style pagination.
33     
34     First, it looks in the current context for the variable specified.  This
35     should be either a QuerySet or a list.
36     
37     1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a 
38        ``QuerySetPaginator`` and the current page object into the context names
39        ``paginator`` and ``page_obj``, respectively.
40     
41     2. If it is a list, this ``AutoPaginateNode`` will emit a simple
42        ``Paginator`` and the current page object into the context names 
43        ``paginator`` and ``page_obj``, respectively.
44     
45     It will then replace the variable specified with only the objects for the
46     current page.
47     
48     .. note::
49         
50         It is recommended to use *{% paginate %}* after using the autopaginate
51         tag.  If you choose not to use *{% paginate %}*, make sure to display the
52         list of availabale pages, or else the application may seem to be buggy.
53     """
54     def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION):
55         self.queryset_var = template.Variable(queryset_var)
56         self.paginate_by = paginate_by
57
58     def render(self, context):
59         key = self.queryset_var.var
60         value = self.queryset_var.resolve(context)
61         if issubclass(value.__class__, QuerySet):
62             model = value.model
63             paginator_class = QuerySetPaginator
64         else:
65             value = list(value)
66             try:
67                 model = value[0].__class__
68             except IndexError:
69                 return u''
70             paginator_class = Paginator
71         paginator = paginator_class(value, self.paginate_by)
72         try:
73             page_obj = paginator.page(context['request'].page)
74         except:
75             return u''
76         context[key] = page_obj.object_list
77         context['paginator'] = paginator
78         context['page_obj'] = page_obj
79         return u''
80
81 def paginate(context, window=DEFAULT_WINDOW):
82     """
83     Renders the ``pagination/pagination.html`` template, resulting in a
84     Digg-like display of the available pages, given the current page.  If there
85     are too many pages to be displayed before and after the current page, then
86     elipses will be used to indicate the undisplayed gap between page numbers.
87     
88     Requires one argument, ``context``, which should be a dictionary-like data
89     structure and must contain the following keys:
90     
91     ``paginator``
92         A ``Paginator`` or ``QuerySetPaginator`` object.
93     
94     ``page_obj``
95         This should be the result of calling the page method on the 
96         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
97         the current page.
98     
99     This same ``context`` dictionary-like data structure may also include:
100     
101     ``getvars``
102         A dictionary of all of the **GET** parameters in the current request.
103         This is useful to maintain certain types of state, even when requesting
104         a different page.
105         """
106     try:
107         paginator = context['paginator']
108         page_obj = context['page_obj']
109         page_range = paginator.page_range
110         # First and last are simply the first *n* pages and the last *n* pages,
111         # where *n* is the current window size.
112         first = set(page_range[:window])
113         last = set(page_range[-window:])
114         # Now we look around our current page, making sure that we don't wrap
115         # around.
116         current_start = page_obj.number-1-window
117         if current_start < 0:
118             current_start = 0
119         current_end = page_obj.number-1+window
120         if current_end < 0:
121             current_end = 0
122         current = set(page_range[current_start:current_end])
123         pages = []
124         # If there's no overlap between the first set of pages and the current
125         # set of pages, then there's a possible need for elusion.
126         if len(first.intersection(current)) == 0:
127             first_list = sorted(list(first))
128             second_list = sorted(list(current))
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             pages.extend(sorted(list(first.union(current))))
147         # If there's no overlap between the current set of pages and the last
148         # set of pages, then there's a possible need for elusion.
149         if len(current.intersection(last)) == 0:
150             second_list = sorted(list(last))
151             diff = second_list[0] - pages[-1]
152             # If there is a gap of two, between the last page of the current
153             # set and the first page of the last 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             pages.extend(sorted(list(last.difference(current))))
168         to_return = {
169             'pages': pages,
170             'page_obj': page_obj,
171             'paginator': paginator,
172             'is_paginated': paginator.count > paginator.per_page,
173         }
174         if 'request' in context:
175             getvars = context['request'].GET.copy()
176             if 'page' in getvars:
177                 del getvars['page']
178             if len(getvars.keys()) > 0:
179                 to_return['getvars'] = "&%s" % getvars.urlencode()
180             else:
181                 to_return['getvars'] = ''
182         return to_return
183     except KeyError:
184         return {}
185 register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
186 register.tag('autopaginate', do_autopaginate)