Merge pull request #3067 from uploadcare/cursor-hooks

Cursor hooks
This commit is contained in:
Tom Christie 2015-06-23 21:59:50 +01:00
commit a17270589c
2 changed files with 52 additions and 55 deletions

View File

@ -127,50 +127,6 @@ 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.
"""
# The offset in the cursor is used in situations where we have a
# nearly-unique index. (Eg millisecond precision creation timestamps)
# We guard against malicious users attempting to cause expensive database
# queries, by having a hard cap on the maximum possible size of the offset.
OFFSET_CUTOFF = 1000
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, cutoff=OFFSET_CUTOFF)
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): def _reverse_ordering(ordering_tuple):
""" """
Given an order_by tuple such as `('-created', 'uuid')` reverse the Given an order_by tuple such as `('-created', 'uuid')` reverse the
@ -503,15 +459,10 @@ class CursorPagination(BasePagination):
self.base_url = request.build_absolute_uri() self.base_url = request.build_absolute_uri()
self.ordering = self.get_ordering(request, queryset, view) self.ordering = self.get_ordering(request, queryset, view)
# Determine if we have a cursor, and if so then decode it. self.cursor = self.decode_cursor(request)
encoded = request.query_params.get(self.cursor_query_param) if self.cursor is None:
if encoded is None:
self.cursor = None
(offset, reverse, current_position) = (0, False, None) (offset, reverse, current_position) = (0, False, None)
else: else:
self.cursor = _decode_cursor(encoded)
if self.cursor is None:
raise NotFound(self.invalid_cursor_message)
(offset, reverse, current_position) = self.cursor (offset, reverse, current_position) = self.cursor
# Cursor pagination always enforces an ordering. # Cursor pagination always enforces an ordering.
@ -623,8 +574,7 @@ class CursorPagination(BasePagination):
position = self.previous_position position = self.previous_position
cursor = Cursor(offset=offset, reverse=False, position=position) cursor = Cursor(offset=offset, reverse=False, position=position)
encoded = _encode_cursor(cursor) return self.encode_cursor(cursor)
return replace_query_param(self.base_url, self.cursor_query_param, encoded)
def get_previous_link(self): def get_previous_link(self):
if not self.has_previous: if not self.has_previous:
@ -672,8 +622,7 @@ class CursorPagination(BasePagination):
position = self.next_position position = self.next_position
cursor = Cursor(offset=offset, reverse=True, position=position) cursor = Cursor(offset=offset, reverse=True, position=position)
encoded = _encode_cursor(cursor) return self.encode_cursor(cursor)
return replace_query_param(self.base_url, self.cursor_query_param, encoded)
def get_ordering(self, request, queryset, view): def get_ordering(self, request, queryset, view):
""" """
@ -715,6 +664,53 @@ class CursorPagination(BasePagination):
return (ordering,) return (ordering,)
return tuple(ordering) return tuple(ordering)
def decode_cursor(self, request):
"""
Given a request with a cursor, return a `Cursor` instance.
"""
# 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:
return None
# The offset in the cursor is used in situations where we have a
# nearly-unique index. (Eg millisecond precision creation timestamps)
# We guard against malicious users attempting to cause expensive database
# queries, by having a hard cap on the maximum possible size of the offset.
OFFSET_CUTOFF = 1000
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, cutoff=OFFSET_CUTOFF)
reverse = tokens.get('r', ['0'])[0]
reverse = bool(int(reverse))
position = tokens.get('p', [None])[0]
except (TypeError, ValueError):
raise NotFound(self.invalid_cursor_message)
return Cursor(offset=offset, reverse=reverse, position=position)
def encode_cursor(self, cursor):
"""
Given a Cursor instance, return an url with encoded cursor.
"""
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)
encoded = b64encode(querystring.encode('ascii')).decode('ascii')
return replace_query_param(self.base_url, self.cursor_query_param, encoded)
def _get_position_from_instance(self, instance, ordering): def _get_position_from_instance(self, instance, ordering):
attr = getattr(instance, ordering[0].lstrip('-')) attr = getattr(instance, ordering[0].lstrip('-'))
return six.text_type(attr) return six.text_type(attr)

View File

@ -186,6 +186,7 @@ class TestPageNumberPagination:
def setup(self): def setup(self):
class ExamplePagination(pagination.PageNumberPagination): class ExamplePagination(pagination.PageNumberPagination):
page_size = 5 page_size = 5
self.pagination = ExamplePagination() self.pagination = ExamplePagination()
self.queryset = range(1, 101) self.queryset = range(1, 101)