mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-04 18:08:03 +03:00 
			
		
		
		
	Merge pull request #2428 from tomchristie/cursor-pagination
Cursor pagination
This commit is contained in:
		
						commit
						37dc2520f9
					
				| 
						 | 
					@ -114,7 +114,7 @@ class OrderingFilter(BaseFilterBackend):
 | 
				
			||||||
    ordering_param = api_settings.ORDERING_PARAM
 | 
					    ordering_param = api_settings.ORDERING_PARAM
 | 
				
			||||||
    ordering_fields = None
 | 
					    ordering_fields = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_ordering(self, request):
 | 
					    def get_ordering(self, request, queryset, view):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Ordering is set by a comma delimited ?ordering=... query parameter.
 | 
					        Ordering is set by a comma delimited ?ordering=... query parameter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -124,7 +124,13 @@ class OrderingFilter(BaseFilterBackend):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        params = request.query_params.get(self.ordering_param)
 | 
					        params = request.query_params.get(self.ordering_param)
 | 
				
			||||||
        if params:
 | 
					        if params:
 | 
				
			||||||
            return [param.strip() for param in params.split(',')]
 | 
					            fields = [param.strip() for param in params.split(',')]
 | 
				
			||||||
 | 
					            ordering = self.remove_invalid_fields(queryset, fields, view)
 | 
				
			||||||
 | 
					            if ordering:
 | 
				
			||||||
 | 
					                return ordering
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # No ordering was included, or all the ordering fields were invalid
 | 
				
			||||||
 | 
					        return self.get_default_ordering(view)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_default_ordering(self, view):
 | 
					    def get_default_ordering(self, view):
 | 
				
			||||||
        ordering = getattr(view, 'ordering', None)
 | 
					        ordering = getattr(view, 'ordering', None)
 | 
				
			||||||
| 
						 | 
					@ -132,7 +138,7 @@ class OrderingFilter(BaseFilterBackend):
 | 
				
			||||||
            return (ordering,)
 | 
					            return (ordering,)
 | 
				
			||||||
        return ordering
 | 
					        return ordering
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def remove_invalid_fields(self, queryset, ordering, view):
 | 
					    def remove_invalid_fields(self, queryset, fields, view):
 | 
				
			||||||
        valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
 | 
					        valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if valid_fields is None:
 | 
					        if valid_fields is None:
 | 
				
			||||||
| 
						 | 
					@ -152,18 +158,10 @@ class OrderingFilter(BaseFilterBackend):
 | 
				
			||||||
            valid_fields = [field.name for field in queryset.model._meta.fields]
 | 
					            valid_fields = [field.name for field in queryset.model._meta.fields]
 | 
				
			||||||
            valid_fields += queryset.query.aggregates.keys()
 | 
					            valid_fields += queryset.query.aggregates.keys()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [term for term in ordering if term.lstrip('-') in valid_fields]
 | 
					        return [term for term in fields if term.lstrip('-') in valid_fields]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def filter_queryset(self, request, queryset, view):
 | 
					    def filter_queryset(self, request, queryset, view):
 | 
				
			||||||
        ordering = self.get_ordering(request)
 | 
					        ordering = self.get_ordering(request, queryset, view)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if ordering:
 | 
					 | 
				
			||||||
            # Skip any incorrect parameters
 | 
					 | 
				
			||||||
            ordering = self.remove_invalid_fields(queryset, ordering, view)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not ordering:
 | 
					 | 
				
			||||||
            # Use 'ordering' attribute by default
 | 
					 | 
				
			||||||
            ordering = self.get_default_ordering(view)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ordering:
 | 
					        if ordering:
 | 
				
			||||||
            return queryset.order_by(*ordering)
 | 
					            return queryset.order_by(*ordering)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,15 @@
 | 
				
			||||||
 | 
					# coding: utf-8
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Pagination serializers determine the structure of the output that should
 | 
					Pagination serializers determine the structure of the output that should
 | 
				
			||||||
be used for paginated responses.
 | 
					be used for paginated responses.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					from base64 import b64encode, b64decode
 | 
				
			||||||
from collections import namedtuple
 | 
					from collections import namedtuple
 | 
				
			||||||
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
 | 
					from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
 | 
				
			||||||
from django.template import Context, loader
 | 
					from django.template import Context, loader
 | 
				
			||||||
from django.utils import six
 | 
					from django.utils import six
 | 
				
			||||||
 | 
					from django.utils.six.moves.urllib import parse as urlparse
 | 
				
			||||||
from django.utils.translation import ugettext as _
 | 
					from django.utils.translation import ugettext as _
 | 
				
			||||||
from rest_framework.compat import OrderedDict
 | 
					from rest_framework.compat import OrderedDict
 | 
				
			||||||
from rest_framework.exceptions import NotFound
 | 
					from rest_framework.exceptions import NotFound
 | 
				
			||||||
| 
						 | 
					@ -17,12 +20,12 @@ from rest_framework.utils.urls import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _strict_positive_int(integer_string, cutoff=None):
 | 
					def _positive_int(integer_string, strict=False, cutoff=None):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Cast a string to a strictly positive integer.
 | 
					    Cast a string to a strictly positive integer.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    ret = int(integer_string)
 | 
					    ret = int(integer_string)
 | 
				
			||||||
    if ret <= 0:
 | 
					    if ret < 0 or (ret == 0 and strict):
 | 
				
			||||||
        raise ValueError()
 | 
					        raise ValueError()
 | 
				
			||||||
    if cutoff:
 | 
					    if cutoff:
 | 
				
			||||||
        ret = min(ret, cutoff)
 | 
					        ret = min(ret, cutoff)
 | 
				
			||||||
| 
						 | 
					@ -123,6 +126,53 @@ def _get_page_links(page_numbers, current, url_func):
 | 
				
			||||||
    return page_links
 | 
					    return page_links
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _decode_cursor(encoded):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Given a string representing an encoded cursor, return a `Cursor` instance.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        querystring = b64decode(encoded.encode('ascii')).decode('ascii')
 | 
				
			||||||
 | 
					        tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        offset = tokens.get('o', ['0'])[0]
 | 
				
			||||||
 | 
					        offset = _positive_int(offset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reverse = tokens.get('r', ['0'])[0]
 | 
				
			||||||
 | 
					        reverse = bool(int(reverse))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        position = tokens.get('p', [None])[0]
 | 
				
			||||||
 | 
					    except (TypeError, ValueError):
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Cursor(offset=offset, reverse=reverse, position=position)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _encode_cursor(cursor):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Given a Cursor instance, return an encoded string representation.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    tokens = {}
 | 
				
			||||||
 | 
					    if cursor.offset != 0:
 | 
				
			||||||
 | 
					        tokens['o'] = str(cursor.offset)
 | 
				
			||||||
 | 
					    if cursor.reverse:
 | 
				
			||||||
 | 
					        tokens['r'] = '1'
 | 
				
			||||||
 | 
					    if cursor.position is not None:
 | 
				
			||||||
 | 
					        tokens['p'] = cursor.position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    querystring = urlparse.urlencode(tokens, doseq=True)
 | 
				
			||||||
 | 
					    return b64encode(querystring.encode('ascii')).decode('ascii')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _reverse_ordering(ordering_tuple):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Given an order_by tuple such as `('-created', 'uuid')` reverse the
 | 
				
			||||||
 | 
					    ordering and return a new tuple, eg. `('created', '-uuid')`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    invert = lambda x: x[1:] if (x.startswith('-')) else '-' + x
 | 
				
			||||||
 | 
					    return tuple([invert(item) for item in ordering_tuple])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position'])
 | 
				
			||||||
PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
 | 
					PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
 | 
					PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
 | 
				
			||||||
| 
						 | 
					@ -168,6 +218,8 @@ class PageNumberPagination(BasePagination):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template = 'rest_framework/pagination/numbers.html'
 | 
					    template = 'rest_framework/pagination/numbers.html'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    invalid_page_message = _('Invalid page "{page_number}": {message}.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _handle_backwards_compat(self, view):
 | 
					    def _handle_backwards_compat(self, view):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Prior to version 3.1, pagination was handled in the view, and the
 | 
					        Prior to version 3.1, pagination was handled in the view, and the
 | 
				
			||||||
| 
						 | 
					@ -200,7 +252,7 @@ class PageNumberPagination(BasePagination):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.page = paginator.page(page_number)
 | 
					            self.page = paginator.page(page_number)
 | 
				
			||||||
        except InvalidPage as exc:
 | 
					        except InvalidPage as exc:
 | 
				
			||||||
            msg = _('Invalid page "{page_number}": {message}.').format(
 | 
					            msg = self.invalid_page_message.format(
 | 
				
			||||||
                page_number=page_number, message=six.text_type(exc)
 | 
					                page_number=page_number, message=six.text_type(exc)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            raise NotFound(msg)
 | 
					            raise NotFound(msg)
 | 
				
			||||||
| 
						 | 
					@ -223,8 +275,9 @@ class PageNumberPagination(BasePagination):
 | 
				
			||||||
    def get_page_size(self, request):
 | 
					    def get_page_size(self, request):
 | 
				
			||||||
        if self.paginate_by_param:
 | 
					        if self.paginate_by_param:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                return _strict_positive_int(
 | 
					                return _positive_int(
 | 
				
			||||||
                    request.query_params[self.paginate_by_param],
 | 
					                    request.query_params[self.paginate_by_param],
 | 
				
			||||||
 | 
					                    strict=True,
 | 
				
			||||||
                    cutoff=self.max_paginate_by
 | 
					                    cutoff=self.max_paginate_by
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            except (KeyError, ValueError):
 | 
					            except (KeyError, ValueError):
 | 
				
			||||||
| 
						 | 
					@ -307,7 +360,7 @@ class LimitOffsetPagination(BasePagination):
 | 
				
			||||||
    def get_limit(self, request):
 | 
					    def get_limit(self, request):
 | 
				
			||||||
        if self.limit_query_param:
 | 
					        if self.limit_query_param:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                return _strict_positive_int(
 | 
					                return _positive_int(
 | 
				
			||||||
                    request.query_params[self.limit_query_param],
 | 
					                    request.query_params[self.limit_query_param],
 | 
				
			||||||
                    cutoff=self.max_limit
 | 
					                    cutoff=self.max_limit
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
| 
						 | 
					@ -318,7 +371,7 @@ class LimitOffsetPagination(BasePagination):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_offset(self, request):
 | 
					    def get_offset(self, request):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return _strict_positive_int(
 | 
					            return _positive_int(
 | 
				
			||||||
                request.query_params[self.offset_query_param],
 | 
					                request.query_params[self.offset_query_param],
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        except (KeyError, ValueError):
 | 
					        except (KeyError, ValueError):
 | 
				
			||||||
| 
						 | 
					@ -377,3 +430,253 @@ class LimitOffsetPagination(BasePagination):
 | 
				
			||||||
        template = loader.get_template(self.template)
 | 
					        template = loader.get_template(self.template)
 | 
				
			||||||
        context = Context(self.get_html_context())
 | 
					        context = Context(self.get_html_context())
 | 
				
			||||||
        return template.render(context)
 | 
					        return template.render(context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CursorPagination(BasePagination):
 | 
				
			||||||
 | 
					    # Determine how/if True, False and None positions work - do the string
 | 
				
			||||||
 | 
					    # encodings work with Django queryset filters?
 | 
				
			||||||
 | 
					    # Consider a max offset cap.
 | 
				
			||||||
 | 
					    # Tidy up the `get_ordering` API (eg remove queryset from it)
 | 
				
			||||||
 | 
					    cursor_query_param = 'cursor'
 | 
				
			||||||
 | 
					    page_size = api_settings.PAGINATE_BY
 | 
				
			||||||
 | 
					    invalid_cursor_message = _('Invalid cursor')
 | 
				
			||||||
 | 
					    ordering = None
 | 
				
			||||||
 | 
					    template = 'rest_framework/pagination/previous_and_next.html'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def paginate_queryset(self, queryset, request, view=None):
 | 
				
			||||||
 | 
					        self.base_url = request.build_absolute_uri()
 | 
				
			||||||
 | 
					        self.ordering = self.get_ordering(request, queryset, view)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Determine if we have a cursor, and if so then decode it.
 | 
				
			||||||
 | 
					        encoded = request.query_params.get(self.cursor_query_param)
 | 
				
			||||||
 | 
					        if encoded is None:
 | 
				
			||||||
 | 
					            self.cursor = None
 | 
				
			||||||
 | 
					            (offset, reverse, current_position) = (0, False, None)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.cursor = _decode_cursor(encoded)
 | 
				
			||||||
 | 
					            if self.cursor is None:
 | 
				
			||||||
 | 
					                raise NotFound(self.invalid_cursor_message)
 | 
				
			||||||
 | 
					            (offset, reverse, current_position) = self.cursor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Cursor pagination always enforces an ordering.
 | 
				
			||||||
 | 
					        if reverse:
 | 
				
			||||||
 | 
					            queryset = queryset.order_by(*_reverse_ordering(self.ordering))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            queryset = queryset.order_by(*self.ordering)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If we have a cursor with a fixed position then filter by that.
 | 
				
			||||||
 | 
					        if current_position is not None:
 | 
				
			||||||
 | 
					            order = self.ordering[0]
 | 
				
			||||||
 | 
					            is_reversed = order.startswith('-')
 | 
				
			||||||
 | 
					            order_attr = order.lstrip('-')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Test for: (cursor reversed) XOR (queryset reversed)
 | 
				
			||||||
 | 
					            if self.cursor.reverse != is_reversed:
 | 
				
			||||||
 | 
					                kwargs = {order_attr + '__lt': current_position}
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                kwargs = {order_attr + '__gt': current_position}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            queryset = queryset.filter(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If we have an offset cursor then offset the entire page by that amount.
 | 
				
			||||||
 | 
					        # We also always fetch an extra item in order to determine if there is a
 | 
				
			||||||
 | 
					        # page following on from this one.
 | 
				
			||||||
 | 
					        results = list(queryset[offset:offset + self.page_size + 1])
 | 
				
			||||||
 | 
					        self.page = results[:self.page_size]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Determine the position of the final item following the page.
 | 
				
			||||||
 | 
					        if len(results) > len(self.page):
 | 
				
			||||||
 | 
					            has_following_postion = True
 | 
				
			||||||
 | 
					            following_position = self._get_position_from_instance(results[-1], self.ordering)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            has_following_postion = False
 | 
				
			||||||
 | 
					            following_position = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # If we have a reverse queryset, then the query ordering was in reverse
 | 
				
			||||||
 | 
					        # so we need to reverse the items again before returning them to the user.
 | 
				
			||||||
 | 
					        if reverse:
 | 
				
			||||||
 | 
					            self.page = list(reversed(self.page))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if reverse:
 | 
				
			||||||
 | 
					            # Determine next and previous positions for reverse cursors.
 | 
				
			||||||
 | 
					            self.has_next = (current_position is not None) or (offset > 0)
 | 
				
			||||||
 | 
					            self.has_previous = has_following_postion
 | 
				
			||||||
 | 
					            if self.has_next:
 | 
				
			||||||
 | 
					                self.next_position = current_position
 | 
				
			||||||
 | 
					            if self.has_previous:
 | 
				
			||||||
 | 
					                self.previous_position = following_position
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Determine next and previous positions for forward cursors.
 | 
				
			||||||
 | 
					            self.has_next = has_following_postion
 | 
				
			||||||
 | 
					            self.has_previous = (current_position is not None) or (offset > 0)
 | 
				
			||||||
 | 
					            if self.has_next:
 | 
				
			||||||
 | 
					                self.next_position = following_position
 | 
				
			||||||
 | 
					            if self.has_previous:
 | 
				
			||||||
 | 
					                self.previous_position = current_position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Display page controls in the browsable API if there is more
 | 
				
			||||||
 | 
					        # than one page.
 | 
				
			||||||
 | 
					        if self.has_previous or self.has_next:
 | 
				
			||||||
 | 
					            self.display_page_controls = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.page
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_next_link(self):
 | 
				
			||||||
 | 
					        if not self.has_next:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.cursor and self.cursor.reverse and self.cursor.offset != 0:
 | 
				
			||||||
 | 
					            # If we're reversing direction and we have an offset cursor
 | 
				
			||||||
 | 
					            # then we cannot use the first position we find as a marker.
 | 
				
			||||||
 | 
					            compare = self._get_position_from_instance(self.page[-1], self.ordering)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            compare = self.next_position
 | 
				
			||||||
 | 
					        offset = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for item in reversed(self.page):
 | 
				
			||||||
 | 
					            position = self._get_position_from_instance(item, self.ordering)
 | 
				
			||||||
 | 
					            if position != compare:
 | 
				
			||||||
 | 
					                # The item in this position and the item following it
 | 
				
			||||||
 | 
					                # have different positions. We can use this position as
 | 
				
			||||||
 | 
					                # our marker.
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # The item in this postion has the same position as the item
 | 
				
			||||||
 | 
					            # following it, we can't use it as a marker position, so increment
 | 
				
			||||||
 | 
					            # the offset and keep seeking to the previous item.
 | 
				
			||||||
 | 
					            compare = position
 | 
				
			||||||
 | 
					            offset += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # There were no unique positions in the page.
 | 
				
			||||||
 | 
					            if not self.has_previous:
 | 
				
			||||||
 | 
					                # We are on the first page.
 | 
				
			||||||
 | 
					                # Our cursor will have an offset equal to the page size,
 | 
				
			||||||
 | 
					                # but no position to filter against yet.
 | 
				
			||||||
 | 
					                offset = self.page_size
 | 
				
			||||||
 | 
					                position = None
 | 
				
			||||||
 | 
					            elif self.cursor.reverse:
 | 
				
			||||||
 | 
					                # The change in direction will introduce a paging artifact,
 | 
				
			||||||
 | 
					                # where we end up skipping forward a few extra items.
 | 
				
			||||||
 | 
					                offset = 0
 | 
				
			||||||
 | 
					                position = self.previous_position
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # Use the position from the existing cursor and increment
 | 
				
			||||||
 | 
					                # it's offset by the page size.
 | 
				
			||||||
 | 
					                offset = self.cursor.offset + self.page_size
 | 
				
			||||||
 | 
					                position = self.previous_position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cursor = Cursor(offset=offset, reverse=False, position=position)
 | 
				
			||||||
 | 
					        encoded = _encode_cursor(cursor)
 | 
				
			||||||
 | 
					        return replace_query_param(self.base_url, self.cursor_query_param, encoded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_previous_link(self):
 | 
				
			||||||
 | 
					        if not self.has_previous:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
 | 
				
			||||||
 | 
					            # If we're reversing direction and we have an offset cursor
 | 
				
			||||||
 | 
					            # then we cannot use the first position we find as a marker.
 | 
				
			||||||
 | 
					            compare = self._get_position_from_instance(self.page[0], self.ordering)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            compare = self.previous_position
 | 
				
			||||||
 | 
					        offset = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for item in self.page:
 | 
				
			||||||
 | 
					            position = self._get_position_from_instance(item, self.ordering)
 | 
				
			||||||
 | 
					            if position != compare:
 | 
				
			||||||
 | 
					                # The item in this position and the item following it
 | 
				
			||||||
 | 
					                # have different positions. We can use this position as
 | 
				
			||||||
 | 
					                # our marker.
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # The item in this postion has the same position as the item
 | 
				
			||||||
 | 
					            # following it, we can't use it as a marker position, so increment
 | 
				
			||||||
 | 
					            # the offset and keep seeking to the previous item.
 | 
				
			||||||
 | 
					            compare = position
 | 
				
			||||||
 | 
					            offset += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # There were no unique positions in the page.
 | 
				
			||||||
 | 
					            if not self.has_next:
 | 
				
			||||||
 | 
					                # We are on the final page.
 | 
				
			||||||
 | 
					                # Our cursor will have an offset equal to the page size,
 | 
				
			||||||
 | 
					                # but no position to filter against yet.
 | 
				
			||||||
 | 
					                offset = self.page_size
 | 
				
			||||||
 | 
					                position = None
 | 
				
			||||||
 | 
					            elif self.cursor.reverse:
 | 
				
			||||||
 | 
					                # Use the position from the existing cursor and increment
 | 
				
			||||||
 | 
					                # it's offset by the page size.
 | 
				
			||||||
 | 
					                offset = self.cursor.offset + self.page_size
 | 
				
			||||||
 | 
					                position = self.next_position
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # The change in direction will introduce a paging artifact,
 | 
				
			||||||
 | 
					                # where we end up skipping back a few extra items.
 | 
				
			||||||
 | 
					                offset = 0
 | 
				
			||||||
 | 
					                position = self.next_position
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cursor = Cursor(offset=offset, reverse=True, position=position)
 | 
				
			||||||
 | 
					        encoded = _encode_cursor(cursor)
 | 
				
			||||||
 | 
					        return replace_query_param(self.base_url, self.cursor_query_param, encoded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_ordering(self, request, queryset, view):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a tuple of strings, that may be used in an `order_by` method.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        ordering_filters = [
 | 
				
			||||||
 | 
					            filter_cls for filter_cls in getattr(view, 'filter_backends', [])
 | 
				
			||||||
 | 
					            if hasattr(filter_cls, 'get_ordering')
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ordering_filters:
 | 
				
			||||||
 | 
					            # If a filter exists on the view that implements `get_ordering`
 | 
				
			||||||
 | 
					            # then we defer to that filter to determine the ordering.
 | 
				
			||||||
 | 
					            filter_cls = ordering_filters[0]
 | 
				
			||||||
 | 
					            filter_instance = filter_cls()
 | 
				
			||||||
 | 
					            ordering = filter_instance.get_ordering(request, queryset, view)
 | 
				
			||||||
 | 
					            assert ordering is not None, (
 | 
				
			||||||
 | 
					                'Using cursor pagination, but filter class {filter_cls} '
 | 
				
			||||||
 | 
					                'returned a `None` ordering.'.format(
 | 
				
			||||||
 | 
					                    filter_cls=filter_cls.__name__
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # The default case is to check for an `ordering` attribute,
 | 
				
			||||||
 | 
					            # first on the view instance, and then on this pagination instance.
 | 
				
			||||||
 | 
					            ordering = getattr(view, 'ordering', getattr(self, 'ordering', None))
 | 
				
			||||||
 | 
					            assert ordering is not None, (
 | 
				
			||||||
 | 
					                'Using cursor pagination, but no ordering attribute was declared '
 | 
				
			||||||
 | 
					                'on the view or on the pagination class.'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert isinstance(ordering, (six.string_types, list, tuple)), (
 | 
				
			||||||
 | 
					            'Invalid ordering. Expected string or tuple, but got {type}'.format(
 | 
				
			||||||
 | 
					                type=type(ordering).__name__
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if isinstance(ordering, six.string_types):
 | 
				
			||||||
 | 
					            return (ordering,)
 | 
				
			||||||
 | 
					        return tuple(ordering)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_position_from_instance(self, instance, ordering):
 | 
				
			||||||
 | 
					        attr = getattr(instance, ordering[0].lstrip('-'))
 | 
				
			||||||
 | 
					        return six.text_type(attr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_paginated_response(self, data):
 | 
				
			||||||
 | 
					        return Response(OrderedDict([
 | 
				
			||||||
 | 
					            ('next', self.get_next_link()),
 | 
				
			||||||
 | 
					            ('previous', self.get_previous_link()),
 | 
				
			||||||
 | 
					            ('results', data)
 | 
				
			||||||
 | 
					        ]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_html_context(self):
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            'previous_url': self.get_previous_link(),
 | 
				
			||||||
 | 
					            'next_url': self.get_next_link()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def to_html(self):
 | 
				
			||||||
 | 
					        template = loader.get_template(self.template)
 | 
				
			||||||
 | 
					        context = Context(self.get_html_context())
 | 
				
			||||||
 | 
					        return template.render(context)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,10 +63,20 @@ a single block in the template.
 | 
				
			||||||
.pagination>.disabled>a,
 | 
					.pagination>.disabled>a,
 | 
				
			||||||
.pagination>.disabled>a:hover,
 | 
					.pagination>.disabled>a:hover,
 | 
				
			||||||
.pagination>.disabled>a:focus {
 | 
					.pagination>.disabled>a:focus {
 | 
				
			||||||
  cursor: default;
 | 
					  cursor: not-allowed;
 | 
				
			||||||
  pointer-events: none;
 | 
					  pointer-events: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pager>.disabled>a,
 | 
				
			||||||
 | 
					.pager>.disabled>a:hover,
 | 
				
			||||||
 | 
					.pager>.disabled>a:focus {
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pager .next {
 | 
				
			||||||
 | 
					  margin-left: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*=== dabapps bootstrap styles ====*/
 | 
					/*=== dabapps bootstrap styles ====*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html {
 | 
					html {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					<ul class="pager">
 | 
				
			||||||
 | 
					{% if previous_url %}
 | 
				
			||||||
 | 
					    <li class="previous"><a href="{{ previous_url }}">« Previous</a></li>
 | 
				
			||||||
 | 
					{% else %}
 | 
				
			||||||
 | 
					    <li class="previous disabled"><a href="#">« Previous</a></li>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					{% if next_url %}
 | 
				
			||||||
 | 
					    <li class="next"><a href="{{ next_url }}">Next »</a></li>
 | 
				
			||||||
 | 
					{% else %}
 | 
				
			||||||
 | 
					    <li class="next disabled"><a href="#">Next »</li>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					# coding: utf-8
 | 
				
			||||||
from __future__ import unicode_literals
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
from rest_framework import exceptions, generics, pagination, serializers, status, filters
 | 
					from rest_framework import exceptions, generics, pagination, serializers, status, filters
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
| 
						 | 
					@ -77,6 +78,20 @@ class TestPaginationIntegration:
 | 
				
			||||||
            'count': 50
 | 
					            'count': 50
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_setting_page_size_to_zero(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        When page_size parameter is invalid it should return to the default.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        request = factory.get('/', {'page_size': 0})
 | 
				
			||||||
 | 
					        response = self.view(request)
 | 
				
			||||||
 | 
					        assert response.status_code == status.HTTP_200_OK
 | 
				
			||||||
 | 
					        assert response.data == {
 | 
				
			||||||
 | 
					            'results': [2, 4, 6, 8, 10],
 | 
				
			||||||
 | 
					            'previous': None,
 | 
				
			||||||
 | 
					            'next': 'http://testserver/?page=2&page_size=0',
 | 
				
			||||||
 | 
					            'count': 50
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_additional_query_params_are_preserved(self):
 | 
					    def test_additional_query_params_are_preserved(self):
 | 
				
			||||||
        request = factory.get('/', {'page': 2, 'filter': 'even'})
 | 
					        request = factory.get('/', {'page': 2, 'filter': 'even'})
 | 
				
			||||||
        response = self.view(request)
 | 
					        response = self.view(request)
 | 
				
			||||||
| 
						 | 
					@ -88,6 +103,14 @@ class TestPaginationIntegration:
 | 
				
			||||||
            'count': 50
 | 
					            'count': 50
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_404_not_found_for_zero_page(self):
 | 
				
			||||||
 | 
					        request = factory.get('/', {'page': '0'})
 | 
				
			||||||
 | 
					        response = self.view(request)
 | 
				
			||||||
 | 
					        assert response.status_code == status.HTTP_404_NOT_FOUND
 | 
				
			||||||
 | 
					        assert response.data == {
 | 
				
			||||||
 | 
					            'detail': 'Invalid page "0": That page number is less than 1.'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_404_not_found_for_invalid_page(self):
 | 
					    def test_404_not_found_for_invalid_page(self):
 | 
				
			||||||
        request = factory.get('/', {'page': 'invalid'})
 | 
					        request = factory.get('/', {'page': 'invalid'})
 | 
				
			||||||
        response = self.view(request)
 | 
					        response = self.view(request)
 | 
				
			||||||
| 
						 | 
					@ -422,6 +445,179 @@ class TestLimitOffset:
 | 
				
			||||||
        assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 | 
					        assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestCursorPagination:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Unit tests for `pagination.CursorPagination`.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setup(self):
 | 
				
			||||||
 | 
					        class MockObject(object):
 | 
				
			||||||
 | 
					            def __init__(self, idx):
 | 
				
			||||||
 | 
					                self.created = idx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class MockQuerySet(object):
 | 
				
			||||||
 | 
					            def __init__(self, items):
 | 
				
			||||||
 | 
					                self.items = items
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def filter(self, created__gt=None, created__lt=None):
 | 
				
			||||||
 | 
					                if created__gt is not None:
 | 
				
			||||||
 | 
					                    return MockQuerySet([
 | 
				
			||||||
 | 
					                        item for item in self.items
 | 
				
			||||||
 | 
					                        if item.created > int(created__gt)
 | 
				
			||||||
 | 
					                    ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                assert created__lt is not None
 | 
				
			||||||
 | 
					                return MockQuerySet([
 | 
				
			||||||
 | 
					                    item for item in self.items
 | 
				
			||||||
 | 
					                    if item.created < int(created__lt)
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def order_by(self, *ordering):
 | 
				
			||||||
 | 
					                if ordering[0].startswith('-'):
 | 
				
			||||||
 | 
					                    return MockQuerySet(list(reversed(self.items)))
 | 
				
			||||||
 | 
					                return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def __getitem__(self, sliced):
 | 
				
			||||||
 | 
					                return self.items[sliced]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class ExamplePagination(pagination.CursorPagination):
 | 
				
			||||||
 | 
					            page_size = 5
 | 
				
			||||||
 | 
					            ordering = 'created'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.pagination = ExamplePagination()
 | 
				
			||||||
 | 
					        self.queryset = MockQuerySet([
 | 
				
			||||||
 | 
					            MockObject(idx) for idx in [
 | 
				
			||||||
 | 
					                1, 1, 1, 1, 1,
 | 
				
			||||||
 | 
					                1, 2, 3, 4, 4,
 | 
				
			||||||
 | 
					                4, 4, 5, 6, 7,
 | 
				
			||||||
 | 
					                7, 7, 7, 7, 7,
 | 
				
			||||||
 | 
					                7, 7, 7, 8, 9,
 | 
				
			||||||
 | 
					                9, 9, 9, 9, 9
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_pages(self, url):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Given a URL return a tuple of:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous page, current page, next page, previous url, next url)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        request = Request(factory.get(url))
 | 
				
			||||||
 | 
					        queryset = self.pagination.paginate_queryset(self.queryset, request)
 | 
				
			||||||
 | 
					        current = [item.created for item in queryset]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        next_url = self.pagination.get_next_link()
 | 
				
			||||||
 | 
					        previous_url = self.pagination.get_previous_link()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if next_url is not None:
 | 
				
			||||||
 | 
					            request = Request(factory.get(next_url))
 | 
				
			||||||
 | 
					            queryset = self.pagination.paginate_queryset(self.queryset, request)
 | 
				
			||||||
 | 
					            next = [item.created for item in queryset]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            next = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if previous_url is not None:
 | 
				
			||||||
 | 
					            request = Request(factory.get(previous_url))
 | 
				
			||||||
 | 
					            queryset = self.pagination.paginate_queryset(self.queryset, request)
 | 
				
			||||||
 | 
					            previous = [item.created for item in queryset]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            previous = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (previous, current, next, previous_url, next_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_invalid_cursor(self):
 | 
				
			||||||
 | 
					        request = Request(factory.get('/', {'cursor': '123'}))
 | 
				
			||||||
 | 
					        with pytest.raises(exceptions.NotFound):
 | 
				
			||||||
 | 
					            self.pagination.paginate_queryset(self.queryset, request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_use_with_ordering_filter(self):
 | 
				
			||||||
 | 
					        class MockView:
 | 
				
			||||||
 | 
					            filter_backends = (filters.OrderingFilter,)
 | 
				
			||||||
 | 
					            ordering_fields = ['username', 'created']
 | 
				
			||||||
 | 
					            ordering = 'created'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = Request(factory.get('/', {'ordering': 'username'}))
 | 
				
			||||||
 | 
					        ordering = self.pagination.get_ordering(request, [], MockView())
 | 
				
			||||||
 | 
					        assert ordering == ('username',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = Request(factory.get('/', {'ordering': '-username'}))
 | 
				
			||||||
 | 
					        ordering = self.pagination.get_ordering(request, [], MockView())
 | 
				
			||||||
 | 
					        assert ordering == ('-username',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = Request(factory.get('/', {'ordering': 'invalid'}))
 | 
				
			||||||
 | 
					        ordering = self.pagination.get_ordering(request, [], MockView())
 | 
				
			||||||
 | 
					        assert ordering == ('created',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_cursor_pagination(self):
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages('/')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous is None
 | 
				
			||||||
 | 
					        assert current == [1, 1, 1, 1, 1]
 | 
				
			||||||
 | 
					        assert next == [1, 2, 3, 4, 4]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [1, 1, 1, 1, 1]
 | 
				
			||||||
 | 
					        assert current == [1, 2, 3, 4, 4]
 | 
				
			||||||
 | 
					        assert next == [4, 4, 5, 6, 7]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [1, 2, 3, 4, 4]
 | 
				
			||||||
 | 
					        assert current == [4, 4, 5, 6, 7]
 | 
				
			||||||
 | 
					        assert next == [7, 7, 7, 7, 7]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [4, 4, 4, 5, 6]  # Paging artifact
 | 
				
			||||||
 | 
					        assert current == [7, 7, 7, 7, 7]
 | 
				
			||||||
 | 
					        assert next == [7, 7, 7, 8, 9]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [7, 7, 7, 7, 7]
 | 
				
			||||||
 | 
					        assert current == [7, 7, 7, 8, 9]
 | 
				
			||||||
 | 
					        assert next == [9, 9, 9, 9, 9]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [7, 7, 7, 8, 9]
 | 
				
			||||||
 | 
					        assert current == [9, 9, 9, 9, 9]
 | 
				
			||||||
 | 
					        assert next is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [7, 7, 7, 7, 7]
 | 
				
			||||||
 | 
					        assert current == [7, 7, 7, 8, 9]
 | 
				
			||||||
 | 
					        assert next == [9, 9, 9, 9, 9]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [4, 4, 5, 6, 7]
 | 
				
			||||||
 | 
					        assert current == [7, 7, 7, 7, 7]
 | 
				
			||||||
 | 
					        assert next == [8, 9, 9, 9, 9]  # Paging artifact
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [1, 2, 3, 4, 4]
 | 
				
			||||||
 | 
					        assert current == [4, 4, 5, 6, 7]
 | 
				
			||||||
 | 
					        assert next == [7, 7, 7, 7, 7]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous == [1, 1, 1, 1, 1]
 | 
				
			||||||
 | 
					        assert current == [1, 2, 3, 4, 4]
 | 
				
			||||||
 | 
					        assert next == [4, 4, 5, 6, 7]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert previous is None
 | 
				
			||||||
 | 
					        assert current == [1, 1, 1, 1, 1]
 | 
				
			||||||
 | 
					        assert next == [1, 2, 3, 4, 4]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert isinstance(self.pagination.to_html(), type(''))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_get_displayed_page_numbers():
 | 
					def test_get_displayed_page_numbers():
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Test our contextual page display function.
 | 
					    Test our contextual page display function.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user