From 43d983fae82ab23ca94f52deb29e938eb2a40e88 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 17:25:12 +0000 Subject: [PATCH] Add paging controls --- rest_framework/pagination.py | 66 ++++++++++++++----- .../rest_framework/css/bootstrap-tweaks.css | 12 +++- .../pagination/previous_and_next.html | 12 ++++ tests/test_pagination.py | 5 +- 4 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 rest_framework/templates/rest_framework/pagination/previous_and_next.html diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 1b4174bc6..b3658acad 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -133,9 +133,14 @@ def _decode_cursor(encoded): try: querystring = b64decode(encoded.encode('ascii')).decode('ascii') tokens = urlparse.parse_qs(querystring, keep_blank_values=True) - offset = _positive_int(tokens['offset'][0]) - reverse = bool(int(tokens['reverse'][0])) - position = tokens.get('position', [None])[0] + + 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 @@ -146,12 +151,13 @@ def _encode_cursor(cursor): """ Given a Cursor instance, return an encoded string representation. """ - tokens = { - 'offset': str(cursor.offset), - 'reverse': '1' if cursor.reverse else '0', - } + tokens = {} + if cursor.offset != 0: + tokens['o'] = str(cursor.offset) + if cursor.reverse: + tokens['r'] = '1' if cursor.position is not None: - tokens['position'] = cursor.position + tokens['p'] = cursor.position querystring = urlparse.urlencode(tokens, doseq=True) return b64encode(querystring.encode('ascii')).decode('ascii') @@ -430,10 +436,12 @@ 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() @@ -452,17 +460,22 @@ class CursorPagination(BasePagination): # Cursor pagination always enforces an ordering. if reverse: - queryset = queryset.order_by(_reverse_ordering(self.ordering)) + queryset = queryset.order_by(*_reverse_ordering(self.ordering)) else: - queryset = queryset.order_by(self.ordering) + 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: - primary_ordering_attr = self.ordering[0].lstrip('-') - if self.cursor.reverse: - kwargs = {primary_ordering_attr + '__lt': current_position} + 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 = {primary_ordering_attr + '__gt': current_position} + kwargs = {order_attr + '__gt': current_position} + queryset = queryset.filter(**kwargs) # If we have an offset cursor then offset the entire page by that amount. @@ -501,6 +514,11 @@ class CursorPagination(BasePagination): 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): @@ -642,5 +660,23 @@ class CursorPagination(BasePagination): return tuple(ordering) def _get_position_from_instance(self, instance, ordering): - attr = getattr(instance, ordering[0]) + 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) diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css index 15b42178f..04f12ed3d 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -63,10 +63,20 @@ a single block in the template. .pagination>.disabled>a, .pagination>.disabled>a:hover, .pagination>.disabled>a:focus { - cursor: default; + cursor: not-allowed; 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 ====*/ html { diff --git a/rest_framework/templates/rest_framework/pagination/previous_and_next.html b/rest_framework/templates/rest_framework/pagination/previous_and_next.html new file mode 100644 index 000000000..eacbfff45 --- /dev/null +++ b/rest_framework/templates/rest_framework/pagination/previous_and_next.html @@ -0,0 +1,12 @@ + diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 338be610c..13bfb6272 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -1,3 +1,4 @@ +# coding: utf-8 from __future__ import unicode_literals from rest_framework import exceptions, generics, pagination, serializers, status, filters from rest_framework.request import Request @@ -471,7 +472,7 @@ class TestCursorPagination: if item.created < int(created__lt) ]) - def order_by(self, ordering): + def order_by(self, *ordering): if ordering[0].startswith('-'): return MockQuerySet(list(reversed(self.items))) return self @@ -614,6 +615,8 @@ class TestCursorPagination: 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(): """