From afc036740211f8c9e7f65de67cc0fac16a050702 Mon Sep 17 00:00:00 2001 From: Kevin Turner Date: Mon, 22 May 2017 17:20:06 -0700 Subject: [PATCH 1/2] test demonstrating CursorPagination with float position #5160 --- tests/test_pagination.py | 47 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index dd7f70330..9e7f2c6c0 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -7,9 +7,9 @@ from django.db import models from django.test import TestCase from rest_framework import ( - exceptions, filters, generics, pagination, serializers, status + exceptions, filters, generics, pagination, serializers, status, viewsets ) -from rest_framework.pagination import PAGE_BREAK, PageLink +from rest_framework.pagination import CursorPagination, PAGE_BREAK, PageLink from rest_framework.request import Request from rest_framework.test import APIRequestFactory @@ -824,3 +824,46 @@ def test_get_displayed_page_numbers(): assert displayed_page_numbers(7, 9) == [1, None, 6, 7, 8, 9] assert displayed_page_numbers(8, 9) == [1, None, 7, 8, 9] assert displayed_page_numbers(9, 9) == [1, None, 7, 8, 9] + + +class ThreeItemCursorPagination(CursorPagination): + page_size = 3 + ordering = 'score' + + +class FloatyModel(models.Model): + score = models.FloatField() + + +class PassThroughSerializer(serializers.BaseSerializer): + def to_representation(self, item): + return item + + +class FloatyViewSet(viewsets.ReadOnlyModelViewSet): + queryset = FloatyModel.objects.all() + pagination_class = ThreeItemCursorPagination + serializer_class = PassThroughSerializer + page_size = 3 + + +class TestCursorPaginationWithFloatingPointPosition(TestCase): + def setUp(self): + self.view = FloatyViewSet.as_view(actions={'get': 'list'}) + + def test_page_boundary_does_not_repeat_elements(self): + for i in range(12): + FloatyModel.objects.create(score=i/9.0) + + request = factory.get('/') + first_response = self.view(request) + + first_page_last_item = first_response.data['results'][-1] + + second_request = factory.get(first_response.data['next']) + second_response = self.view(second_request) + + second_page_first_item = second_response.data['results'][0] + + self.assertNotEqual(first_page_last_item.pk, second_page_first_item.pk) + self.assertLess(first_page_last_item.score, second_page_first_item.score) From e9df63a467bc939aa8db29bc95a128f440e5b517 Mon Sep 17 00:00:00 2001 From: Kevin Turner Date: Mon, 22 May 2017 17:33:44 -0700 Subject: [PATCH 2/2] Preemptively reduce the precision of floating point positions so that it does not change in a round-trip through the database backend. #5160 --- rest_framework/pagination.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 0255cfc7f..ef18fc9e4 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from base64 import b64decode, b64encode from collections import OrderedDict, namedtuple +import decimal from django.core.paginator import Paginator as DjangoPaginator from django.core.paginator import InvalidPage @@ -488,6 +489,9 @@ class CursorPagination(BasePagination): # queries, by having a hard cap on the maximum possible size of the offset. offset_cutoff = 1000 + __rounding_down = decimal.Context(prec=14, rounding=decimal.ROUND_FLOOR) + __rounding_up = decimal.Context(prec=14, rounding=decimal.ROUND_CEILING) + def paginate_queryset(self, queryset, request, view=None): self.page_size = self.get_page_size(request) if not self.page_size: @@ -756,6 +760,13 @@ class CursorPagination(BasePagination): attr = instance[field_name] else: attr = getattr(instance, field_name) + + if isinstance(attr, float): + if ordering[0][0] == '-': + attr = self.__rounding_down.create_decimal_from_float(attr) + else: + attr = self.__rounding_up.create_decimal_from_float(attr) + return six.text_type(attr) def get_paginated_response(self, data):