Merge pull request #35 from gitter-badger/gitter-badge
[django-pagination.git] / linaro_django_pagination / templatetags / pagination_tags.py
old mode 100755 (executable)
new mode 100644 (file)
index f4ddde7..f106942
@@ -1,26 +1,58 @@
-try:
-    set
-except NameError:
-    from sets import Set as set
+# Copyright (c) 2008, Eric Florenzano
+# Copyright (c) 2010, 2011 Linaro Limited
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#     * Neither the name of the author nor the names of other
+#       contributors may be used to endorse or promote products derived
+#       from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
 
-from django import template
-from django.template import TOKEN_BLOCK
-from django.http import Http404
-from django.core.paginator import Paginator, InvalidPage
 from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.paginator import Paginator, InvalidPage
+from django.http import Http404
+from django.template import (
+    Context,
+    Library,
+    Node,
+    TemplateSyntaxError,
+    Variable,
+    loader,
+)
+
+try:
+    from django.template.base import TOKEN_BLOCK
+except ImportError:     # Django < 1.8
+    from django.template import TOKEN_BLOCK
+
+from django.template.loader import select_template
+from django.utils.text import unescape_string_literal
 
-register = template.Library()
+# TODO, import this normally later on
+from linaro_django_pagination.settings import *
 
-DEFAULT_PAGINATION = getattr(settings, 'PAGINATION_DEFAULT_PAGINATION', 20)
-DEFAULT_WINDOW = getattr(settings, 'PAGINATION_DEFAULT_WINDOW', 4)
-DEFAULT_ORPHANS = getattr(settings, 'PAGINATION_DEFAULT_ORPHANS', 0)
-INVALID_PAGE_RAISES_404 = getattr(settings,
-    'PAGINATION_INVALID_PAGE_RAISES_404', False)
-DISPLAY_PAGE_LINKS = getattr(settings, 'PAGINATION_DISPLAY_PAGE_LINKS', True)
-PREVIOUS_LINK_DECORATOR = getattr(settings, 'PAGINATION_PREVIOUS_LINK_DECORATOR', "&lsaquo;&lsaquo; ")
-NEXT_LINK_DECORATOR = getattr(settings, 'PAGINATION_NEXT_LINK_DECORATOR', " &rsaquo;&rsaquo;")
-DISPLAY_DISABLED_PREVIOUS_LINK = getattr(settings, 'PAGINATION_DISPLAY_DISABLED_PREVIOUS_LINK', False)
-DISPLAY_DISABLED_NEXT_LINK = getattr(settings, 'PAGINATION_DISPLAY_DISABLED_NEXT_LINK', False)
 
 def do_autopaginate(parser, token):
     """
@@ -28,12 +60,12 @@ def do_autopaginate(parser, token):
 
     Syntax is:
 
-        autopaginate SOMETHING [PAGINATE_BY] [ORPHANS] [as NAME]
+        autopaginate QUERYSET [PAGINATE_BY] [ORPHANS] [as NAME]
     """
     # Check whether there are any other autopaginations are later in this template
     expr = lambda obj: (obj.token_type == TOKEN_BLOCK and \
         len(obj.split_contents()) > 0 and obj.split_contents()[0] == "autopaginate")
-    multiple_paginations = len(filter(expr, parser.tokens)) > 0
+    multiple_paginations = len([tok for tok in parser.tokens if expr(tok)]) > 0
 
     i = iter(token.split_contents())
     paginate_by = None
@@ -42,50 +74,49 @@ def do_autopaginate(parser, token):
     orphans = None
     word = None
     try:
-        word = i.next()
+        word = next(i)
         assert word == "autopaginate"
-        queryset_var = i.next()
-        word = i.next()
+        queryset_var = next(i)
+        word = next(i)
         if word != "as":
             paginate_by = word
             try:
                 paginate_by = int(paginate_by)
             except ValueError:
                 pass
-            word = i.next()
+            word = next(i)
         if word != "as":
             orphans = word
             try:
                 orphans = int(orphans)
             except ValueError:
                 pass
-            word = i.next()
+            word = next(i)
         assert word == "as"
-        context_var = i.next()
+        context_var = next(i)
     except StopIteration:
         pass
     if queryset_var is None:
-        raise template.TemplateSyntaxError(
+        raise TemplateSyntaxError(
             "Invalid syntax. Proper usage of this tag is: "
-            "{%% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
-            " [as CONTEXT_VAR_NAME] %%}"
-        )
+            "{% autopaginate QUERYSET [PAGINATE_BY] [ORPHANS]"
+            " [as CONTEXT_VAR_NAME] %}")
     return AutoPaginateNode(queryset_var, multiple_paginations, paginate_by, orphans, context_var)
 
 
-class AutoPaginateNode(template.Node):
+class AutoPaginateNode(Node):
     """
     Emits the required objects to allow for Digg-style pagination.
-    
+
     First, it looks in the current context for the variable specified, and using
-    that object, it emits a simple ``Paginator`` and the current page object 
+    that object, it emits a simple ``Paginator`` and the current page object
     into the context names ``paginator`` and ``page_obj``, respectively.
-    
+
     It will then replace the variable specified with only the objects for the
     current page.
-    
+
     .. note::
-        
+
         It is recommended to use *{% paginate %}* after using the autopaginate
         tag.  If you choose not to use *{% paginate %}*, make sure to display the
         list of available pages, or else the application may seem to be buggy.
@@ -96,24 +127,28 @@ class AutoPaginateNode(template.Node):
             paginate_by = DEFAULT_PAGINATION
         if orphans is None:
             orphans = DEFAULT_ORPHANS
-        self.queryset_var = template.Variable(queryset_var)
+        self.queryset_var = Variable(queryset_var)
         if isinstance(paginate_by, int):
             self.paginate_by = paginate_by
         else:
-            self.paginate_by = template.Variable(paginate_by)
+            self.paginate_by = Variable(paginate_by)
         if isinstance(orphans, int):
             self.orphans = orphans
         else:
-            self.orphans = template.Variable(orphans)
+            self.orphans = Variable(orphans)
         self.context_var = context_var
         self.multiple_paginations = multiple_paginations
 
     def render(self, context):
-        if self.multiple_paginations or context.has_key('paginator'):
+        # Save multiple_paginations state in context
+        if self.multiple_paginations and 'multiple_paginations' not in context:
+            context['multiple_paginations'] = True
+
+        if context.get('multiple_paginations') or getattr(context, "paginator", None):
             page_suffix = '_%s' % self.queryset_var
         else:
             page_suffix = ''
-        
+
         key = self.queryset_var.var
         value = self.queryset_var.resolve(context)
         if isinstance(self.paginate_by, int):
@@ -126,14 +161,20 @@ class AutoPaginateNode(template.Node):
             orphans = self.orphans.resolve(context)
         paginator = Paginator(value, paginate_by, orphans)
         try:
-            page_obj = paginator.page(context['request'].page(page_suffix))
+            request = context['request']
+        except KeyError:
+            raise ImproperlyConfigured(
+                "You need to enable 'django.core.context_processors.request'."
+                " See linaro-django-pagination/README file for TEMPLATE_CONTEXT_PROCESSORS details")
+        try:
+            page_obj = paginator.page(request.page(page_suffix))
         except InvalidPage:
             if INVALID_PAGE_RAISES_404:
                 raise Http404('Invalid page requested.  If DEBUG were set to ' +
                     'False, an HTTP 404 page would have been shown instead.')
             context[key] = []
             context['invalid_page'] = True
-            return u''
+            return ''
         if self.context_var is not None:
             context[self.context_var] = page_obj.object_list
         else:
@@ -141,144 +182,165 @@ class AutoPaginateNode(template.Node):
         context['paginator'] = paginator
         context['page_obj'] = page_obj
         context['page_suffix'] = page_suffix
-        return u''
+        return ''
+
 
+class PaginateNode(Node):
 
-def paginate(context, window=DEFAULT_WINDOW):
+    def __init__(self, template=None):
+        self.template = template
+
+    def render(self, context):
+        template_list = ['pagination/pagination.html']
+        new_context = paginate(context)
+        if self.template:
+            template_list.insert(0, self.template)
+        return loader.render_to_string(template_list, new_context,
+            context_instance = context)
+
+
+
+def do_paginate(parser, token):
+    """
+    Emits the pagination control for the most recent autopaginate list
+
+    Syntax is:
+
+        paginate [using "TEMPLATE"]
+
+    Where TEMPLATE is a quoted template name. If missing the default template
+    is used (paginate/pagination.html).
+    """
+    argv = token.split_contents()
+    argc = len(argv)
+    if argc == 1:
+        template = None
+    elif argc == 3 and argv[1] == 'using':
+        template = unescape_string_literal(argv[2])
+    else:
+        raise TemplateSyntaxError(
+            "Invalid syntax. Proper usage of this tag is: "
+            "{% paginate [using \"TEMPLATE\"] %}")
+    return PaginateNode(template)
+
+
+def paginate(context, window=DEFAULT_WINDOW, margin=DEFAULT_MARGIN):
     """
     Renders the ``pagination/pagination.html`` template, resulting in a
     Digg-like display of the available pages, given the current page.  If there
     are too many pages to be displayed before and after the current page, then
     elipses will be used to indicate the undisplayed gap between page numbers.
-    
+
     Requires one argument, ``context``, which should be a dictionary-like data
     structure and must contain the following keys:
-    
+
     ``paginator``
         A ``Paginator`` or ``QuerySetPaginator`` object.
-    
+
     ``page_obj``
-        This should be the result of calling the page method on the 
+        This should be the result of calling the page method on the
         aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
         the current page.
-    
+
     This same ``context`` dictionary-like data structure may also include:
-    
+
     ``getvars``
         A dictionary of all of the **GET** parameters in the current request.
         This is useful to maintain certain types of state, even when requesting
         a different page.
-    
-    ``pagination_template``
-        A custom template to include in place of the default ``pagination.html`` 
-        contents.
-        
-    """
+
+    Argument ``window`` is number to pages before/after current page. If window
+    exceeds pagination border (1 and end), window is moved to left or right.
+
+    Argument ``margin``` is number of pages on start/end of pagination.
+    Example:
+        window=2, margin=1, current=6     1 ... 4 5 [6] 7 8 ... 11
+        window=2, margin=0, current=1     [1] 2 3 4 5 ...
+        window=2, margin=0, current=5     ... 3 4 [5] 6 7 ...
+        window=2, margin=0, current=11     ... 7 8 9 10 [11]
+        """
+
+    if window < 0:
+        raise ValueError('Parameter "window" cannot be less than zero')
+    if margin < 0:
+        raise ValueError('Parameter "margin" cannot be less than zero')
     try:
         paginator = context['paginator']
         page_obj = context['page_obj']
         page_suffix = context.get('page_suffix', '')
-        page_range = paginator.page_range
-        pagination_template = context.get('pagination_template', 'pagination/default.html')
+        page_range = list(paginator.page_range)
         # Calculate the record range in the current page for display.
         records = {'first': 1 + (page_obj.number - 1) * paginator.per_page}
         records['last'] = records['first'] + paginator.per_page - 1
         if records['last'] + paginator.orphans >= paginator.count:
             records['last'] = paginator.count
-        # First and last are simply the first *n* pages and the last *n* pages,
-        # where *n* is the current window size.
-        first = set(page_range[:window])
-        last = set(page_range[-window:])
-        # Now we look around our current page, making sure that we don't wrap
-        # around.
-        current_start = page_obj.number-1-window
-        if current_start < 0:
-            current_start = 0
-        current_end = page_obj.number-1+window
-        if current_end < 0:
-            current_end = 0
-        current = set(page_range[current_start:current_end])
-        pages = []
-        # If there's no overlap between the first set of pages and the current
-        # set of pages, then there's a possible need for elusion.
-        if len(first.intersection(current)) == 0:
-            first_list = list(first)
-            first_list.sort()
-            second_list = list(current)
-            second_list.sort()
-            pages.extend(first_list)
-            diff = second_list[0] - first_list[-1]
-            # If there is a gap of two, between the last page of the first
-            # set and the first page of the current set, then we're missing a
-            # page.
-            if diff == 2:
-                pages.append(second_list[0] - 1)
-            # If the difference is just one, then there's nothing to be done,
-            # as the pages need no elusion and are correct.
-            elif diff == 1:
-                pass
-            # Otherwise, there's a bigger gap which needs to be signaled for
-            # elusion, by pushing a None value to the page list.
-            else:
-                pages.append(None)
-            pages.extend(second_list)
+
+        # figure window
+        window_start = page_obj.number - window - 1
+        window_end = page_obj.number + window
+
+        # solve if window exceeded page range
+        if window_start < 0:
+            window_end = window_end - window_start
+            window_start = 0
+        if window_end > paginator.num_pages:
+            window_start = max(0, window_start - (window_end - paginator.num_pages))
+            window_end = paginator.num_pages
+        pages = page_range[window_start:window_end]
+
+        # figure margin and add elipses
+        if margin > 0:
+            # figure margin
+            tmp_pages = set(pages)
+            tmp_pages = tmp_pages.union(page_range[:margin])
+            tmp_pages = tmp_pages.union(page_range[-margin:])
+            tmp_pages = list(tmp_pages)
+            tmp_pages.sort()
+            pages = []
+            pages.append(tmp_pages[0])
+            for i in range(1, len(tmp_pages)):
+                # figure gap size => add elipses or fill in gap
+                gap = tmp_pages[i] - tmp_pages[i - 1]
+                if gap >= 3:
+                    pages.append(None)
+                elif gap == 2:
+                    pages.append(tmp_pages[i] - 1)
+                pages.append(tmp_pages[i])
         else:
-            unioned = list(first.union(current))
-            unioned.sort()
-            pages.extend(unioned)
-        # If there's no overlap between the current set of pages and the last
-        # set of pages, then there's a possible need for elusion.
-        if len(current.intersection(last)) == 0:
-            second_list = list(last)
-            second_list.sort()
-            diff = second_list[0] - pages[-1]
-            # If there is a gap of two, between the last page of the current
-            # set and the first page of the last set, then we're missing a 
-            # page.
-            if diff == 2:
-                pages.append(second_list[0] - 1)
-            # If the difference is just one, then there's nothing to be done,
-            # as the pages need no elusion and are correct.
-            elif diff == 1:
-                pass
-            # Otherwise, there's a bigger gap which needs to be signaled for
-            # elusion, by pushing a None value to the page list.
-            else:
+            if pages[0] != 1:
+                pages.insert(0, None)
+            if pages[-1] != paginator.num_pages:
                 pages.append(None)
-            pages.extend(second_list)
-        else:
-            differenced = list(last.difference(current))
-            differenced.sort()
-            pages.extend(differenced)
-        to_return = {
+
+        new_context = {
             'MEDIA_URL': settings.MEDIA_URL,
-            'STATIC_URL': settings.STATIC_URL,
-            'pages': pages,
-            'page_obj': page_obj,
-            'paginator': paginator,
+            'STATIC_URL': getattr(settings, "STATIC_URL", None),
+            'disable_link_for_first_page': DISABLE_LINK_FOR_FIRST_PAGE,
+            'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
+            'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
+            'display_page_links': DISPLAY_PAGE_LINKS,
             'is_paginated': paginator.count > paginator.per_page,
+            'next_link_decorator': NEXT_LINK_DECORATOR,
+            'page_obj': page_obj,
             'page_suffix': page_suffix,
-            'display_page_links': DISPLAY_PAGE_LINKS,
-            'display_disabled_previous_link': DISPLAY_DISABLED_PREVIOUS_LINK,
-            'display_disabled_next_link': DISPLAY_DISABLED_NEXT_LINK,
+            'pages': pages,
+            'paginator': paginator,
             'previous_link_decorator': PREVIOUS_LINK_DECORATOR,
-            'next_link_decorator': NEXT_LINK_DECORATOR,
-            'pagination_template': pagination_template,
+            'records': records,
         }
         if 'request' in context:
             getvars = context['request'].GET.copy()
             if 'page%s' % page_suffix in getvars:
                 del getvars['page%s' % page_suffix]
             if len(getvars.keys()) > 0:
-                to_return['getvars'] = "&%s" % getvars.urlencode()
+                new_context['getvars'] = "&%s" % getvars.urlencode()
             else:
-                to_return['getvars'] = ''
-        return to_return
+                new_context['getvars'] = ''
+        return new_context
     except (KeyError, AttributeError):
         return {}
 
 
-register.inclusion_tag(
-    'pagination/pagination.html', takes_context=True)(paginate)
-
+register = Library()
+register.tag('paginate', do_paginate)
 register.tag('autopaginate', do_autopaginate)