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 @@ +
  • {{ page.label|safe }}
  • \ 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 @@ +
  • {{ page.label|safe }}
  • \ 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 @@ + 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

    + + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
    NoEmailUsername
    {{ forloop.counter }}{{ user.email }}{{ user.username }} +
    + + 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", ], )