Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow pagination using negative page_number or offset #9568

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading