From 16dc419ff05d75e6ee27653c23a9023b24820358 Mon Sep 17 00:00:00 2001 From: Denny Biasiolli Date: Sat, 19 Oct 2024 22:23:33 +0200 Subject: [PATCH] Pagination: allowing negative page numbers and offsets --- rest_framework/pagination.py | 15 ++++++++ tests/test_pagination.py | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index a543ceeb5..c62212d1c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -191,6 +191,7 @@ class PageNumberPagination(BasePagination): last_page_strings = ('last',) template = 'rest_framework/pagination/numbers.html' + allow_negative_page_numbers = False invalid_page_message = _('Invalid page.') @@ -225,6 +226,14 @@ class PageNumberPagination(BasePagination): page_number = request.query_params.get(self.page_query_param) or 1 if page_number in self.last_page_strings: page_number = paginator.num_pages + if self.allow_negative_page_numbers: + try: + page_number = int(page_number) + if page_number < 0: + page_number = paginator.num_pages + page_number + return max(page_number, 0) + except ValueError: + return page_number return page_number def get_paginated_response(self, data): @@ -384,6 +393,7 @@ class LimitOffsetPagination(BasePagination): offset_query_description = _('The initial index from which to return the results.') max_limit = None template = 'rest_framework/pagination/numbers.html' + allow_negative_offsets = False def paginate_queryset(self, queryset, request, view=None): self.request = request @@ -447,6 +457,11 @@ class LimitOffsetPagination(BasePagination): def get_offset(self, request): try: + if self.allow_negative_offsets: + offset = int(request.query_params[self.offset_query_param]) + if offset < 0: + offset = self.count + offset + return max(offset, 0) return _positive_int( request.query_params[self.offset_query_param], ) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 02d443ade..f5fdedf2c 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -260,6 +260,40 @@ class TestPageNumberPagination: with pytest.raises(exceptions.NotFound): self.paginate_queryset(request) + def test_negative_page(self): + request = Request(factory.get('/', {'page': -1})) + print(request) + with pytest.raises(exceptions.NotFound): + self.paginate_queryset(request) + + def test_allowed_negative_page(self): + self.pagination.allow_negative_page_numbers = True + request = Request(factory.get('/', {'page': -2})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [86, 87, 88, 89, 90] + assert content == { + 'results': [86, 87, 88, 89, 90], + 'previous': 'http://testserver/?page=17', + 'next': 'http://testserver/?page=19', + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?page=17', + 'next_url': 'http://testserver/?page=19', + 'page_links': [ + PageLink('http://testserver/', 1, False, False), + PAGE_BREAK, + PageLink('http://testserver/?page=17', 17, False, False), + PageLink('http://testserver/?page=18', 18, True, False), + PageLink('http://testserver/?page=19', 19, False, False), + PageLink('http://testserver/?page=20', 20, False, False), + ] + } + assert self.pagination.display_page_controls + assert isinstance(self.pagination.to_html(), str) + def test_get_paginated_response_schema(self): unpaginated_schema = { 'type': 'object', @@ -527,6 +561,42 @@ class TestLimitOffset: queryset = self.paginate_queryset(request) assert queryset == [1, 2, 3, 4, 5] + def test_negative_offset(self): + """ + A negative offset query param should be treated as 0. + """ + request = Request(factory.get('/', {'limit': 5, 'offset': -5})) + queryset = self.paginate_queryset(request) + assert queryset == [1, 2, 3, 4, 5] + + def test_allowed_negative_offset(self): + """ + A negative offset query param should be treated as `count - offset`. + """ + self.pagination.allow_negative_offsets = True + request = Request(factory.get('/', {'limit': 5, 'offset': -10})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [91, 92, 93, 94, 95] + assert content == { + 'results': [91, 92, 93, 94, 95], + 'previous': 'http://testserver/?limit=5&offset=85', + 'next': 'http://testserver/?limit=5&offset=95', + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?limit=5&offset=85', + 'next_url': 'http://testserver/?limit=5&offset=95', + 'page_links': [ + PageLink('http://testserver/?limit=5', 1, False, False), + PAGE_BREAK, + PageLink('http://testserver/?limit=5&offset=85', 18, False, False), + PageLink('http://testserver/?limit=5&offset=90', 19, True, False), + PageLink('http://testserver/?limit=5&offset=95', 20, False, False), + ] + } + def test_invalid_limit(self): """ An invalid limit query param should be ignored in favor of the default.