Skip to content

Commit

Permalink
Pagination: allowing negative page numbers and offsets
Browse files Browse the repository at this point in the history
  • Loading branch information
dennybiasiolli committed Oct 19, 2024
1 parent d3dd45b commit 16dc419
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 0 deletions.
15 changes: 15 additions & 0 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')

Expand Down Expand Up @@ -225,6 +226,14 @@ def get_page_number(self, request, paginator):
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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -447,6 +457,11 @@ def get_limit(self, request):

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],
)
Expand Down
70 changes: 70 additions & 0 deletions tests/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,40 @@ def test_invalid_page(self):
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',
Expand Down Expand Up @@ -527,6 +561,42 @@ def test_invalid_offset(self):
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.
Expand Down

0 comments on commit 16dc419

Please sign in to comment.