diff --git a/sandbox/manage.py b/sandbox/manage.py
new file mode 100755
index 0000000..839e90e
--- /dev/null
+++ b/sandbox/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_pagination.settings")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django
+ except ImportError:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ )
+ raise
+ execute_from_command_line(sys.argv)
diff --git a/sandbox/requirements.txt b/sandbox/requirements.txt
new file mode 100644
index 0000000..b0789fa
--- /dev/null
+++ b/sandbox/requirements.txt
@@ -0,0 +1,2 @@
+Django==1.11.2
+django-simple-pagination==1.1.8
diff --git a/sandbox/sample/__init__.py b/sandbox/sample/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/sample/admin.py b/sandbox/sample/admin.py
new file mode 100644
index 0000000..13be29d
--- /dev/null
+++ b/sandbox/sample/admin.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.contrib import admin
+
+# Register your models here.
diff --git a/sandbox/sample/apps.py b/sandbox/sample/apps.py
new file mode 100644
index 0000000..4bb2d1c
--- /dev/null
+++ b/sandbox/sample/apps.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+
+
+class SampleConfig(AppConfig):
+ name = 'sample'
diff --git a/sandbox/sample/migrations/__init__.py b/sandbox/sample/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/sample/models.py b/sandbox/sample/models.py
new file mode 100644
index 0000000..1dfab76
--- /dev/null
+++ b/sandbox/sample/models.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models
+
+# Create your models here.
diff --git a/sandbox/sample/tests.py b/sandbox/sample/tests.py
new file mode 100644
index 0000000..5982e6b
--- /dev/null
+++ b/sandbox/sample/tests.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/sandbox/sample/views.py b/sandbox/sample/views.py
new file mode 100644
index 0000000..53ae3ed
--- /dev/null
+++ b/sandbox/sample/views.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.shortcuts import render
+from django.contrib.auth.models import User
+
+
+# Create your views here.
+def users(request):
+ users = User.objects.all()
+ return render(request, 'users.html', {'users': users})
diff --git a/sandbox/simple_pagination/__init__.py b/sandbox/simple_pagination/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/simple_pagination/admin.py b/sandbox/simple_pagination/admin.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/simple_pagination/migrations/__init__.py b/sandbox/simple_pagination/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/simple_pagination/models.py b/sandbox/simple_pagination/models.py
new file mode 100644
index 0000000..a31dff3
--- /dev/null
+++ b/sandbox/simple_pagination/models.py
@@ -0,0 +1,290 @@
+"""Ephemeral models used to represent a page and a list of pages."""
+
+from __future__ import unicode_literals
+
+from django.template import loader
+from django.utils.encoding import iri_to_uri
+
+from simple_pagination import settings
+from simple_pagination import utils
+
+
+# Page templates cache.
+_template_cache = {}
+
+
+class EndlessPage(utils.UnicodeMixin):
+ """A page link representation.
+
+ Interesting attributes:
+
+ - *self.number*: the page number;
+ - *self.label*: the label of the link
+ (usually the page number as string);
+ - *self.url*: the url of the page (strting with "?");
+ - *self.path*: the path of the page;
+ - *self.is_current*: return True if page is the current page displayed;
+ - *self.is_first*: return True if page is the first page;
+ - *self.is_last*: return True if page is the last page.
+ """
+
+ def __init__(self, request, number, current_number, *args, **kwargs):
+ total_number = kwargs.get('total_number')
+ querystring_key = kwargs.get('querystring_key', 'page')
+ label = kwargs.get('label', None)
+ default_number = kwargs.get('default_number', 1)
+ override_path = kwargs.get('override_path', None)
+ self._request = request
+ self.number = number
+ self.label = utils.text(number) if label is None else label
+ self.querystring_key = querystring_key
+
+ self.is_current = number == current_number
+ self.is_first = number == 1
+ self.is_last = number == total_number
+
+ self.url = utils.get_querystring_for_page(
+ request, number, self.querystring_key,
+ default_number=default_number)
+ path = iri_to_uri(override_path or request.path)
+ self.path = '{0}{1}'.format(path, self.url)
+
+ def __unicode__(self):
+ """Render the page as a link."""
+ context = {
+ 'add_nofollow': False,
+ 'page': self,
+ 'querystring_key': self.querystring_key,
+ }
+ if self.is_current:
+ template_name = 'simple/current_link.html'
+ else:
+ template_name = 'simple/page_link.html'
+ template = _template_cache.setdefault(
+ template_name, loader.get_template(template_name))
+ return template.render(context)
+
+
+class PageList(utils.UnicodeMixin):
+ """A sequence of endless pages."""
+
+ def __init__(self, request, page, querystring_key, **kwargs):
+ default_number = kwargs.get('default_number', None)
+ override_path = kwargs.get('override_path', None)
+ self._request = request
+ self._page = page
+ if default_number is None:
+ self._default_number = 1
+ else:
+ self._default_number = int(default_number)
+ self._querystring_key = querystring_key
+ self._override_path = override_path
+
+ def _endless_page(self, number, label=None):
+ """Factory function that returns a *EndlessPage* instance.
+
+ This method works just like a partial constructor.
+ """
+ return EndlessPage(
+ self._request,
+ number,
+ self._page.number,
+ len(self),
+ self._querystring_key,
+ label=label,
+ default_number=self._default_number,
+ override_path=self._override_path,
+ )
+
+ def __getitem__(self, value):
+ # The type conversion is required here because in templates Django
+ # performs a dictionary lookup before the attribute lokups
+ # (when a dot is encountered).
+ try:
+ value = int(value)
+ except (TypeError, ValueError):
+ # A TypeError says to django to continue with an attribute lookup.
+ raise TypeError
+ if 1 <= value <= len(self):
+ return self._endless_page(value)
+ raise IndexError('page list index out of range')
+
+ def __len__(self):
+ """The length of the sequence is the total number of pages."""
+ return self._page.paginator.num_pages
+
+ def __iter__(self):
+ """Iterate over all the endless pages (from first to last)."""
+ for i in range(len(self)):
+ yield self[i + 1]
+
+ def __unicode__(self):
+ """Return a rendered Digg-style pagination (by default).
+
+ The callable *settings.PAGE_LIST_CALLABLE* can be used to customize
+ how the pages are displayed. The callable takes the current page number
+ and the total number of pages, and must return a sequence of page
+ numbers that will be displayed. The sequence can contain other values:
+
+ - *'previous'*: will display the previous page in that position;
+ - *'next'*: will display the next page in that position;
+ - *'first'*: will display the first page as an arrow;
+ - *'last'*: will display the last page as an arrow;
+ - *None*: a separator will be displayed in that position.
+
+ Here is an example of custom calable that displays the previous page,
+ then the first page, then a separator, then the current page, and
+ finally the last page::
+
+ def get_page_numbers(current_page, num_pages):
+ return ('previous', 1, None, current_page, 'last')
+
+ If *settings.PAGE_LIST_CALLABLE* is None an internal callable is used,
+ generating a Digg-style pagination. The value of
+ *settings.PAGE_LIST_CALLABLE* can also be a dotted path to a callable.
+ """
+ if len(self) > 1:
+ pages_callable = utils.get_page_numbers
+ pages = []
+ for item in pages_callable(self._page.number, len(self)):
+ if item is None:
+ pages.append(None)
+ elif item == 'previous':
+ pages.append(self.previous())
+ elif item == 'next':
+ pages.append(self.next())
+ elif item == 'first':
+ pages.append(self.first_as_arrow())
+ elif item == 'last':
+ pages.append(self.last_as_arrow())
+ else:
+ pages.append(self[item])
+ return loader.render_to_string('simple/show_pages.html', {'pages': pages})
+ return ''
+
+ def current(self):
+ """Return the current page."""
+ return self._endless_page(self._page.number)
+
+ def current_start_index(self):
+ """Return the 1-based index of the first item on the current page."""
+ return self._page.start_index()
+
+ def current_end_index(self):
+ """Return the 1-based index of the last item on the current page."""
+ return self._page.end_index()
+
+ def total_count(self):
+ """Return the total number of objects, across all pages."""
+ return self._page.paginator.count
+
+ def first(self, label=None):
+ """Return the first page."""
+ return self._endless_page(1, label=label)
+
+ def last(self, label=None):
+ """Return the last page."""
+ return self._endless_page(len(self), label=label)
+
+ def first_as_arrow(self):
+ """Return the first page as an arrow.
+
+ The page label (arrow) is defined in ``settings.FIRST_LABEL``.
+ """
+ return self.first(label=settings.FIRST_LABEL)
+
+ def last_as_arrow(self):
+ """Return the last page as an arrow.
+
+ The page label (arrow) is defined in ``settings.LAST_LABEL``.
+ """
+ return self.last(label=settings.LAST_LABEL)
+
+ def previous(self):
+ """Return the previous page.
+
+ The page label is defined in ``settings.PREVIOUS_LABEL``.
+ Return an empty string if current page is the first.
+ """
+ if self._page.has_previous():
+ return self._endless_page(
+ self._page.previous_page_number(),
+ label=settings.PREVIOUS_LABEL)
+ return ''
+
+ def next(self):
+ """Return the next page.
+
+ The page label is defined in ``settings.NEXT_LABEL``.
+ Return an empty string if current page is the last.
+ """
+ if self._page.has_next():
+ return self._endless_page(
+ self._page.next_page_number(),
+ label=settings.NEXT_LABEL)
+ return ''
+
+ def paginated(self):
+ """Return True if this page list contains more than one page."""
+ return len(self) > 1
+
+
+class ShowItems(utils.UnicodeMixin):
+ """A page link representation.
+
+ Interesting attributes:
+
+ - *self.number*: the page number;
+ - *self.label*: the label of the link
+ (usually the page number as string);
+ - *self.url*: the url of the page (strting with "?");
+ - *self.path*: the path of the page;
+ - *self.is_current*: return True if page is the current page displayed;
+ - *self.is_first*: return True if page is the first page;
+ - *self.is_last*: return True if page is the last page.
+ """
+
+ def __init__(self, request, page, querystring_key, **kwargs):
+ default_number = kwargs.get('default_number', None)
+ override_path = kwargs.get('override_path', None)
+ self._request = request
+ self._page = page
+ if default_number is None:
+ self._default_number = 1
+ else:
+ self._default_number = int(default_number)
+ self._querystring_key = querystring_key
+ self._override_path = override_path
+
+ def __unicode__(self):
+ """Render the page as a link."""
+ str_data = "Showing "
+ if self._page.paginator.count == 1:
+ str_data += str(1)
+ str_data = str_data + " to " + str(len(self._page.object_list)) + " of " + str(len(self._page.object_list))
+ else:
+ if self._page.number == 1:
+ str_data += str(1)
+ if self._page.paginator.per_page == str(self._page.paginator.count):
+ str_data = str_data + " to " + str(self._page.paginator.per_page) + " of " + str(self._page.paginator.count)
+ else:
+ str_data = str_data + " to " + str(len(self._page.object_list)) + " of " + str(self._page.paginator.count)
+ else:
+ if self._page.has_next():
+ str_data += "".join(map(str, [
+ (self._page.paginator.per_page * self._page.previous_page_number()) + 1,
+ " to ",
+ self._page.paginator.per_page * self._page.number,
+ " of ",
+ self._page.paginator.count
+ ]))
+ else:
+ str_data += "".join(map(str, [
+ self._page.paginator.per_page * self._page.previous_page_number() + 1,
+ " to ",
+ self._page.paginator.count,
+ " of ",
+ self._page.paginator.count
+ ]))
+
+ return str_data + " items"
diff --git a/sandbox/simple_pagination/settings.py b/sandbox/simple_pagination/settings.py
new file mode 100644
index 0000000..b3b1ed4
--- /dev/null
+++ b/sandbox/simple_pagination/settings.py
@@ -0,0 +1,12 @@
+from django.conf import settings
+
+PER_PAGE = getattr(settings, 'SIMPLE_PAGINATION_PER_PAGE', 10)
+PAGE_LABEL = getattr(settings, 'SIMPLE_PAGINATION_PAGE_LABEL', 'page')
+NEXT_LABEL = getattr(
+ settings, 'SIMPLE_PAGINATION_NEXT_LABEL', '>')
+PREVIOUS_LABEL = getattr(
+ settings, 'SIMPLE_PAGINATION_PREVIOUS_LABEL', '<')
+LAST_LABEL = getattr(
+ settings, 'SIMPLE_PAGINATION_LAST_LABEL', '>>')
+FIRST_LABEL = getattr(
+ settings, 'SIMPLE_PAGINATION_FIRST_LABEL', '<<')
diff --git a/sandbox/simple_pagination/templates/simple/current_link.html b/sandbox/simple_pagination/templates/simple/current_link.html
new file mode 100644
index 0000000..7c1a1e1
--- /dev/null
+++ b/sandbox/simple_pagination/templates/simple/current_link.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sandbox/simple_pagination/templates/simple/page_link.html b/sandbox/simple_pagination/templates/simple/page_link.html
new file mode 100644
index 0000000..c2ee786
--- /dev/null
+++ b/sandbox/simple_pagination/templates/simple/page_link.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sandbox/simple_pagination/templates/simple/show_pages.html b/sandbox/simple_pagination/templates/simple/show_pages.html
new file mode 100644
index 0000000..5417742
--- /dev/null
+++ b/sandbox/simple_pagination/templates/simple/show_pages.html
@@ -0,0 +1,3 @@
+
+ {% for page in pages %}{{page}}{% endfor %}
+
diff --git a/sandbox/simple_pagination/templatetags/__init__.py b/sandbox/simple_pagination/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/simple_pagination/templatetags/paginate.py b/sandbox/simple_pagination/templatetags/paginate.py
new file mode 100644
index 0000000..375b374
--- /dev/null
+++ b/sandbox/simple_pagination/templatetags/paginate.py
@@ -0,0 +1,391 @@
+"""Django Endless Pagination template tags."""
+
+import re
+
+from django import template
+from simple_pagination import settings
+from django.core.paginator import (
+ EmptyPage,
+ Paginator,
+)
+from simple_pagination import utils
+from simple_pagination import models
+
+
+PAGINATE_EXPRESSION = re.compile(r"""
+ ^ # Beginning of line.
+ (((?P\w+)\,)?(?P\w+)\s+)? # First page, per page.
+ (?P[\.\w]+) # Objects / queryset.
+ (\s+starting\s+from\s+page\s+(?P[\-]?\d+|\w+))? # Page start.
+ (\s+using\s+(?P[\"\'\-\w]+))? # Querystring key.
+ (\s+with\s+(?P[\"\'\/\w]+))? # Override path.
+ (\s+as\s+(?P\w+))? # Context variable name.
+ $ # End of line.
+""", re.VERBOSE)
+SHOW_CURRENT_NUMBER_EXPRESSION = re.compile(r"""
+ ^ # Beginning of line.
+ (starting\s+from\s+page\s+(?P\w+))?\s* # Page start.
+ (using\s+(?P[\"\'\-\w]+))?\s* # Querystring key.
+ (as\s+(?P\w+))? # Context variable name.
+ $ # End of line.
+""", re.VERBOSE)
+
+
+register = template.Library()
+
+
+@register.tag
+def paginate(_, token, paginator_class=None):
+ """Paginate objects.
+
+ Usage:
+
+ .. code-block:: html+django
+
+ {% paginate entries %}
+
+ After this call, the *entries* variable in the template context is replaced
+ by only the entries of the current page.
+
+ You can also keep your *entries* original variable (usually a queryset)
+ and add to the context another name that refers to entries of the current
+ page, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate entries as page_entries %}
+
+ The *as* argument is also useful when a nested context variable is provided
+ as queryset. In this case, and only in this case, the resulting variable
+ name is mandatory, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate entries.all as entries %}
+
+ The number of paginated entries is taken from settings, but you can
+ override the default locally, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate 20 entries %}
+
+ Of course you can mix it all:
+
+ .. code-block:: html+django
+
+ {% paginate 20 entries as paginated_entries %}
+
+ By default, the first page is displayed the first time you load the page,
+ but you can change this, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate entries starting from page 3 %}
+
+ When changing the default page, it is also possible to reference the last
+ page (or the second last page, and so on) by using negative indexes, e.g:
+
+ .. code-block:: html+django
+
+ {% paginate entries starting from page -1 %}
+
+ This can be also achieved using a template variable that was passed to the
+ context, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate entries starting from page page_number %}
+
+ If the passed page number does not exist, the first page is displayed.
+
+ If you have multiple paginations in the same page, you can change the
+ querydict key for the single pagination, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate entries using article_page %}
+
+ In this case *article_page* is intended to be a context variable, but you
+ can hardcode the key using quotes, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate entries using 'articles_at_page' %}
+
+ Again, you can mix it all (the order of arguments is important):
+
+ .. code-block:: html+django
+
+ {% paginate 20 entries
+ starting from page 3 using page_key as paginated_entries %}
+
+ Additionally you can pass a path to be used for the pagination:
+
+ .. code-block:: html+django
+
+ {% paginate 20 entries
+ using page_key with pagination_url as paginated_entries %}
+
+ This way you can easily create views acting as API endpoints, and point
+ your Ajax calls to that API. In this case *pagination_url* is considered a
+ context variable, but it is also possible to hardcode the URL, e.g.:
+
+ .. code-block:: html+django
+
+ {% paginate 20 entries with "/mypage/" %}
+
+ If you want the first page to contain a different number of items than
+ subsequent pages, you can separate the two values with a comma, e.g. if
+ you want 3 items on the first page and 10 on other pages:
+
+ .. code-block:: html+django
+
+ {% paginate 3,10 entries %}
+
+ You must use this tag before calling the {% show_more %} one.
+ """
+ # Validate arguments.
+ try:
+ tag_name, tag_args = token.contents.split(None, 1)
+ except ValueError:
+ msg = '%r tag requires arguments' % token.contents.split()[0]
+ raise template.TemplateSyntaxError(msg)
+
+ # Use a regexp to catch args.
+ match = PAGINATE_EXPRESSION.match(tag_args)
+ if match is None:
+ msg = 'Invalid arguments for %r tag' % tag_name
+ raise template.TemplateSyntaxError(msg)
+
+ # Retrieve objects.
+ kwargs = match.groupdict()
+ objects = kwargs.pop('objects')
+
+ # The variable name must be present if a nested context variable is passed.
+ if '.' in objects and kwargs['var_name'] is None:
+ msg = (
+ '%(tag)r tag requires a variable name `as` argumnent if the '
+ 'queryset is provided as a nested context variable (%(objects)s). '
+ 'You must either pass a direct queryset (e.g. taking advantage '
+ 'of the `with` template tag) or provide a new variable name to '
+ 'store the resulting queryset (e.g. `%(tag)s %(objects)s as '
+ 'objects`).'
+ ) % {'tag': tag_name, 'objects': objects}
+ raise template.TemplateSyntaxError(msg)
+
+ # Call the node.
+ return PaginateNode(paginator_class, objects, **kwargs)
+
+
+class PaginateNode(template.Node):
+ """Add to context the objects of the current page.
+
+ Also add the Django paginator's *page* object.
+ """
+
+ def __init__(self, paginator_class, objects, **kwargs):
+ first_page = kwargs.get('first_page', None)
+ per_page = kwargs.get('per_page', None)
+ var_name = kwargs.get('var_name', None)
+ number = kwargs.get('number', None)
+ key = kwargs.get('key', None)
+ override_path = kwargs.get('override_path', None)
+ self.paginator = paginator_class or Paginator
+ self.objects = template.Variable(objects)
+
+ # If *var_name* is not passed, then the queryset name will be used.
+ self.var_name = objects if var_name is None else var_name
+
+ # If *per_page* is not passed then the default value from settings
+ # will be used.
+ self.per_page_variable = None
+ if per_page is None:
+ self.per_page = settings.PER_PAGE
+ elif per_page.isdigit():
+ self.per_page = int(per_page)
+ else:
+ self.per_page_variable = template.Variable(per_page)
+
+ # Handle first page: if it is not passed then *per_page* is used.
+ self.first_page_variable = None
+ if first_page is None:
+ self.first_page = None
+ elif first_page.isdigit():
+ self.first_page = int(first_page)
+ else:
+ self.first_page_variable = template.Variable(first_page)
+
+ # Handle page number when it is not specified in querystring.
+ self.page_number_variable = None
+ if number is None:
+ self.page_number = 1
+ else:
+ try:
+ self.page_number = int(number)
+ except ValueError:
+ self.page_number_variable = template.Variable(number)
+
+ # Set the querystring key attribute.
+ self.querystring_key_variable = None
+ if key is None:
+ self.querystring_key = settings.PAGE_LABEL
+ elif key[0] in ('"', "'") and key[-1] == key[0]:
+ self.querystring_key = key[1:-1]
+ else:
+ self.querystring_key_variable = template.Variable(key)
+
+ # Handle *override_path*.
+ self.override_path_variable = None
+ if override_path is None:
+ self.override_path = None
+ elif (
+ override_path[0] in ('"', "'") and
+ override_path[-1] == override_path[0]):
+ self.override_path = override_path[1:-1]
+ else:
+ self.override_path_variable = template.Variable(override_path)
+
+ def render(self, context):
+ # Handle page number when it is not specified in querystring.
+ if self.page_number_variable is None:
+ default_number = self.page_number
+ else:
+ default_number = int(self.page_number_variable.resolve(context))
+
+ # Calculate the number of items to show on each page.
+ if self.per_page_variable is None:
+ per_page = self.per_page
+ else:
+ per_page = int(self.per_page_variable.resolve(context))
+
+ # User can override the querystring key to use in the template.
+ # The default value is defined in the settings file.
+ if self.querystring_key_variable is None:
+ querystring_key = self.querystring_key
+ else:
+ querystring_key = self.querystring_key_variable.resolve(context)
+
+ # Retrieve the override path if used.
+ if self.override_path_variable is None:
+ override_path = self.override_path
+ else:
+ override_path = self.override_path_variable.resolve(context)
+
+ # Retrieve the queryset and create the paginator object.
+ objects = self.objects.resolve(context)
+ paginator = self.paginator(
+ objects, per_page)
+
+ # Normalize the default page number if a negative one is provided.
+ if default_number < 0:
+ default_number = utils.normalize_page_number(
+ default_number, paginator.page_range)
+
+ # The current request is used to get the requested page number.
+ page_number = utils.get_page_number_from_request(
+ context['request'], querystring_key, default=default_number)
+
+ # Get the page.
+ try:
+ page = paginator.page(page_number)
+ except EmptyPage:
+ page = paginator.page(1)
+
+ # Populate the context with required data.
+ data = {
+ 'default_number': default_number,
+ 'override_path': override_path,
+ 'page': page,
+ 'querystring_key': querystring_key,
+ }
+ context.update({'endless': data, self.var_name: page.object_list})
+ return ''
+
+
+@register.tag
+def show_pages(_, token):
+ """Show page links.
+
+ Usage:
+
+ .. code-block:: html+django
+
+ {% show_pages %}
+
+ It is just a shortcut for:
+
+ .. code-block:: html+django
+
+ {% get_pages %}
+ {{ pages }}
+
+ You can set ``ENDLESS_PAGINATION_PAGE_LIST_CALLABLE`` in your *settings.py*
+ to a callable, or to a dotted path representing a callable, used to
+ customize the pages that are displayed.
+
+ See the *__unicode__* method of ``endless_pagination.models.PageList`` for
+ a detailed explanation of how the callable can be used.
+
+ Must be called after ``{% paginate objects %}``.
+ """
+ # Validate args.
+ if len(token.contents.split()) != 1:
+ msg = '%r tag takes no arguments' % token.contents.split()[0]
+ raise template.TemplateSyntaxError(msg)
+ # Call the node.
+ return ShowPagesNode()
+
+
+class ShowPagesNode(template.Node):
+ """Show the pagination."""
+
+ def render(self, context):
+ # This template tag could raise a PaginationError: you have to call
+ # *paginate* or *lazy_paginate* before including the getpages template.
+ data = utils.get_data_from_context(context)
+ # Return the string representation of the sequence of pages.
+ pages = models.PageList(
+ context['request'],
+ data['page'],
+ data['querystring_key'],
+ default_number=data['default_number'],
+ override_path=data['override_path'],
+ )
+ return utils.text(pages)
+
+
+@register.tag
+def show_pageitems(_, token):
+ """Show page items.
+
+ Usage:
+
+ .. code-block:: html+django
+
+ {% show_pageitems per_page %}
+
+ """
+ # Validate args.
+ if len(token.contents.split()) != 1:
+ msg = '%r tag takes no arguments' % token.contents.split()[0]
+ raise template.TemplateSyntaxError(msg)
+ # Call the node.
+ return ShowPageItemsNode()
+
+
+class ShowPageItemsNode(template.Node):
+ """Show the pagination."""
+
+ def render(self, context):
+ # This template tag could raise a PaginationError: you have to call
+ # *paginate* or *lazy_paginate* before including the getpages template.
+ data = utils.get_data_from_context(context)
+ pages = models.ShowItems(
+ context['request'],
+ data['page'],
+ data['querystring_key'],
+ default_number=data['default_number'],
+ override_path=data['override_path'],
+ )
+ return utils.text(pages)
diff --git a/sandbox/simple_pagination/tests.py b/sandbox/simple_pagination/tests.py
new file mode 100644
index 0000000..ab66de2
--- /dev/null
+++ b/sandbox/simple_pagination/tests.py
@@ -0,0 +1,98 @@
+from django.test import TestCase
+from django.template import Template, Context
+from django.http import HttpRequest
+from simple_pagination.utils import(
+ normalize_page_number,
+ get_querystring_for_page,
+ get_page_numbers,
+)
+from simple_pagination.models import EndlessPage, PageList, ShowItems
+from django.http import QueryDict
+from django.core.paginator import Paginator
+
+
+class PaginateAndShowPageItems(TestCase):
+
+ def test_addition(self):
+ t = Template(
+ "{% load paginate %}{% paginate entities %}.{% show_pageitems %} {% paginate 20 entities %} {% show_pages %}")
+ req = HttpRequest()
+ c = Context({"entities": range(100), 'request': req})
+ val = t.render(c)
+ self.assertTrue(bool(val))
+
+
+class NormalizePageNumber(TestCase):
+
+ def test_normalize_page_number(self):
+ page_number = 1
+ page_range = range(2)
+ val = normalize_page_number(page_number, page_range)
+ self.assertTrue(bool(val))
+ page_range = range(1)
+ val = normalize_page_number(page_number, page_range)
+ self.assertFalse(bool(val))
+
+
+class GetQuerystringForPage(TestCase):
+
+ def test_get_querystring_for_page(self):
+ request = self
+ request = HttpRequest()
+ dict = {u"querystring_key": 1,
+ u"key": 2,
+ u"page": 3}
+ qdict = QueryDict('', mutable=True)
+ qdict.update(dict)
+ request.GET = qdict
+ val = get_querystring_for_page(request=request,
+ page_number=1,
+ querystring_key="key",
+ default_number=1)
+ self.assertTrue(bool(val))
+ request.GET = {}
+ val = get_querystring_for_page(request=request,
+ page_number=1,
+ querystring_key="key",
+ default_number=1)
+ self.assertFalse(bool(val))
+
+
+class GetPageNumbers(TestCase):
+
+ def test_get_page_numbers(self):
+ self.assertTrue(get_page_numbers(current_page=2, num_pages=10))
+ self.assertTrue(get_page_numbers(current_page=9, num_pages=10))
+ self.assertTrue(get_page_numbers(current_page=1, num_pages=3))
+
+
+class TestEndlessPage(TestCase):
+
+ def test_endless_page(self):
+ request = HttpRequest()
+ epage = EndlessPage(request=request,
+ number=2,
+ current_number=2,
+ total_number=10,
+ querystring_key='page')
+ self.assertTrue(epage)
+
+
+class TestPageList(TestCase):
+
+ def test_page_list(self):
+ request = HttpRequest()
+ paginator = Paginator(['john', 'paul', 'george', 'ringo'], 3)
+ page = paginator.page(1)
+ page.number = lambda: None
+ setattr(page, 'number', 2)
+ setattr(page, 'paginator', paginator)
+ page_list = PageList(request=request, page=page, querystring_key="page")
+ page_list = PageList(request=request, page=page, querystring_key="page", default_number=1)
+ page_list._endless_page(number=1)
+ page_list._endless_page(number=3)
+ self.assertTrue(page_list[1])
+ page_list.next()
+ self.assertTrue(page_list)
+ si = ShowItems(request=request, page=page, querystring_key="page")
+ self.assertTrue(si)
diff --git a/sandbox/simple_pagination/utils.py b/sandbox/simple_pagination/utils.py
new file mode 100644
index 0000000..9060a8c
--- /dev/null
+++ b/sandbox/simple_pagination/utils.py
@@ -0,0 +1,101 @@
+from __future__ import unicode_literals
+import sys
+
+from simple_pagination.settings import (
+ PAGE_LABEL
+)
+
+
+# Handle the Python 2 to 3 migration.
+if sys.version_info[0] >= 3:
+ PYTHON3 = True
+ text = str
+else:
+ PYTHON3 = False
+ # Avoid lint errors under Python 3.
+ text = unicode # NOQA
+
+
+def get_data_from_context(context):
+ """Get the django paginator data object from the given *context*.
+ The context is a dict-like object. If the context key ``endless``
+ is not found, a *PaginationError* is raised.
+ """
+ try:
+ return context['endless']
+ except KeyError:
+ raise Exception('Cannot find endless data in context.')
+
+
+def get_page_number_from_request(
+ request, querystring_key=PAGE_LABEL, default=1):
+ """Retrieve the current page number from *GET* or *POST* data.
+ If the page does not exists in *request*, or is not a number,
+ then *default* number is returned.
+ """
+ try:
+ return int(request.GET[querystring_key])
+ except (KeyError, TypeError, ValueError):
+ return default
+
+
+def get_page_numbers(current_page, num_pages):
+ """Default callable for page listing.
+ Produce a Digg-style pagination.
+ """
+
+ if current_page <= 2:
+ start_page = 1
+ else:
+ start_page = current_page - 2
+
+ if num_pages <= 4:
+ end_page = num_pages
+ else:
+ end_page = start_page + 4
+ if end_page > num_pages:
+ end_page = num_pages
+
+ pages = []
+ if current_page != 1:
+ pages.append('first')
+ pages.append('previous')
+ pages.extend([i for i in range(start_page, end_page + 1)])
+ if current_page != num_pages:
+ pages.append('next')
+ pages.append('last')
+ return pages
+
+
+def get_querystring_for_page(
+ request, page_number, querystring_key, default_number=1):
+ """Return a querystring pointing to *page_number*."""
+ querydict = request.GET.copy()
+ querydict[querystring_key] = page_number
+ # For the default page number (usually 1) the querystring is not required.
+ if page_number == default_number:
+ del querydict[querystring_key]
+ if 'querystring_key' in querydict:
+ del querydict['querystring_key']
+ if querydict:
+ return '?' + querydict.urlencode()
+ return ''
+
+
+def normalize_page_number(page_number, page_range):
+ """Handle a negative *page_number*.
+ Return a positive page number contained in *page_range*.
+ If the negative index is out of range, return the page number 1.
+ """
+ try:
+ return page_range[page_number]
+ except IndexError:
+ return page_range[0]
+
+
+class UnicodeMixin(object):
+ """Mixin class to handle defining the proper unicode and string methods."""
+
+ if PYTHON3:
+ def __str__(self):
+ return self.__unicode__()
diff --git a/sandbox/templates/users.html b/sandbox/templates/users.html
new file mode 100644
index 0000000..c505359
--- /dev/null
+++ b/sandbox/templates/users.html
@@ -0,0 +1,35 @@
+{% load paginate %}
+{% if per_page %}
+ {% paginate per_page users %}
+{% else %}
+ {% paginate 10 users %}
+{% endif %}
+
+
+{% show_pageitems %}
+
+
+
Users List
+
+
+
+
No
+
Email
+
Username
+
+
+
+ {% for user in users %}
+
+
{{ forloop.counter }}
+
{{ user.email }}
+
{{ user.username }}
+
+
+ {% endfor %}
+
+
+
+
diff --git a/sandbox/test_pagination/__init__.py b/sandbox/test_pagination/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/test_pagination/settings.py b/sandbox/test_pagination/settings.py
new file mode 100644
index 0000000..12ed2a1
--- /dev/null
+++ b/sandbox/test_pagination/settings.py
@@ -0,0 +1,136 @@
+"""
+Django settings for test_pagination project.
+
+Generated by 'django-admin startproject' using Django 1.11.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.11/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.11/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'o04uf+ee_+u+rck@x+vw$ueem=tfurbnmj-$op(31i$iuuyx^a'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'simple_pagination',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'test_pagination.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'test_pagination.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.11/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.11/howto/static-files/
+
+STATIC_URL = '/static/'
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [BASE_DIR + "/templates"],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
diff --git a/sandbox/test_pagination/urls.py b/sandbox/test_pagination/urls.py
new file mode 100644
index 0000000..387bf93
--- /dev/null
+++ b/sandbox/test_pagination/urls.py
@@ -0,0 +1,25 @@
+"""test_pagination URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.11/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+from django.contrib import admin
+from sample.views import users
+
+urlpatterns = [
+ url(r'^admin/', admin.site.urls),
+ url(r'^users/', users),
+
+]
+
diff --git a/sandbox/test_pagination/wsgi.py b/sandbox/test_pagination/wsgi.py
new file mode 100644
index 0000000..92ace79
--- /dev/null
+++ b/sandbox/test_pagination/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for test_pagination project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_pagination.settings")
+
+application = get_wsgi_application()
diff --git a/setup.py b/setup.py
index c2a1cfa..5ba2e90 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@
setup(
name='django-simple-pagination',
- version='1.1.6',
+ version='1.1.8',
packages=['simple_pagination', 'simple_pagination.migrations', 'simple_pagination.templatetags'],
include_package_data=True,
description='A simple pagination app for Django.',
@@ -42,6 +42,6 @@
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
install_requires=[
- "django",
+ "Django>=1.6.0,<=1.11",
],
)