Add paging controls

This commit is contained in:
Tom Christie 2015-01-22 17:25:12 +00:00
parent 0822c9e558
commit 43d983fae8
4 changed files with 78 additions and 17 deletions

View File

@ -133,9 +133,14 @@ def _decode_cursor(encoded):
try: try:
querystring = b64decode(encoded.encode('ascii')).decode('ascii') querystring = b64decode(encoded.encode('ascii')).decode('ascii')
tokens = urlparse.parse_qs(querystring, keep_blank_values=True) tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
offset = _positive_int(tokens['offset'][0])
reverse = bool(int(tokens['reverse'][0])) offset = tokens.get('o', ['0'])[0]
position = tokens.get('position', [None])[0] offset = _positive_int(offset)
reverse = tokens.get('r', ['0'])[0]
reverse = bool(int(reverse))
position = tokens.get('p', [None])[0]
except (TypeError, ValueError): except (TypeError, ValueError):
return None return None
@ -146,12 +151,13 @@ def _encode_cursor(cursor):
""" """
Given a Cursor instance, return an encoded string representation. Given a Cursor instance, return an encoded string representation.
""" """
tokens = { tokens = {}
'offset': str(cursor.offset), if cursor.offset != 0:
'reverse': '1' if cursor.reverse else '0', tokens['o'] = str(cursor.offset)
} if cursor.reverse:
tokens['r'] = '1'
if cursor.position is not None: if cursor.position is not None:
tokens['position'] = cursor.position tokens['p'] = cursor.position
querystring = urlparse.urlencode(tokens, doseq=True) querystring = urlparse.urlencode(tokens, doseq=True)
return b64encode(querystring.encode('ascii')).decode('ascii') 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 # Determine how/if True, False and None positions work - do the string
# encodings work with Django queryset filters? # encodings work with Django queryset filters?
# Consider a max offset cap. # Consider a max offset cap.
# Tidy up the `get_ordering` API (eg remove queryset from it)
cursor_query_param = 'cursor' cursor_query_param = 'cursor'
page_size = api_settings.PAGINATE_BY page_size = api_settings.PAGINATE_BY
invalid_cursor_message = _('Invalid cursor') invalid_cursor_message = _('Invalid cursor')
ordering = None ordering = None
template = 'rest_framework/pagination/previous_and_next.html'
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
self.base_url = request.build_absolute_uri() self.base_url = request.build_absolute_uri()
@ -452,17 +460,22 @@ class CursorPagination(BasePagination):
# Cursor pagination always enforces an ordering. # Cursor pagination always enforces an ordering.
if reverse: if reverse:
queryset = queryset.order_by(_reverse_ordering(self.ordering)) queryset = queryset.order_by(*_reverse_ordering(self.ordering))
else: 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 we have a cursor with a fixed position then filter by that.
if current_position is not None: if current_position is not None:
primary_ordering_attr = self.ordering[0].lstrip('-') order = self.ordering[0]
if self.cursor.reverse: is_reversed = order.startswith('-')
kwargs = {primary_ordering_attr + '__lt': current_position} order_attr = order.lstrip('-')
# Test for: (cursor reversed) XOR (queryset reversed)
if self.cursor.reverse != is_reversed:
kwargs = {order_attr + '__lt': current_position}
else: else:
kwargs = {primary_ordering_attr + '__gt': current_position} kwargs = {order_attr + '__gt': current_position}
queryset = queryset.filter(**kwargs) queryset = queryset.filter(**kwargs)
# If we have an offset cursor then offset the entire page by that amount. # 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: if self.has_previous:
self.previous_position = current_position 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 return self.page
def get_next_link(self): def get_next_link(self):
@ -642,5 +660,23 @@ class CursorPagination(BasePagination):
return tuple(ordering) return tuple(ordering)
def _get_position_from_instance(self, instance, 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) 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)

View File

@ -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 {

View File

@ -0,0 +1,12 @@
<ul class="pager">
{% if previous_url %}
<li class="previous"><a href="{{ previous_url }}">&laquo; Previous</a></li>
{% else %}
<li class="previous disabled"><a href="#">&laquo; Previous</a></li>
{% endif %}
{% if next_url %}
<li class="next"><a href="{{ next_url }}">Next &raquo;</a></li>
{% else %}
<li class="next disabled"><a href="#">Next &raquo;</li>
{% endif %}
</ul>

View File

@ -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
@ -471,7 +472,7 @@ class TestCursorPagination:
if item.created < int(created__lt) if item.created < int(created__lt)
]) ])
def order_by(self, ordering): def order_by(self, *ordering):
if ordering[0].startswith('-'): if ordering[0].startswith('-'):
return MockQuerySet(list(reversed(self.items))) return MockQuerySet(list(reversed(self.items)))
return self return self
@ -614,6 +615,8 @@ class TestCursorPagination:
assert current == [1, 1, 1, 1, 1] assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4] 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():
""" """