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