Skip to content

Commit 16dc419

Browse files
Pagination: allowing negative page numbers and offsets
1 parent d3dd45b commit 16dc419

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-0
lines changed

rest_framework/pagination.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class PageNumberPagination(BasePagination):
191191
last_page_strings = ('last',)
192192

193193
template = 'rest_framework/pagination/numbers.html'
194+
allow_negative_page_numbers = False
194195

195196
invalid_page_message = _('Invalid page.')
196197

@@ -225,6 +226,14 @@ def get_page_number(self, request, paginator):
225226
page_number = request.query_params.get(self.page_query_param) or 1
226227
if page_number in self.last_page_strings:
227228
page_number = paginator.num_pages
229+
if self.allow_negative_page_numbers:
230+
try:
231+
page_number = int(page_number)
232+
if page_number < 0:
233+
page_number = paginator.num_pages + page_number
234+
return max(page_number, 0)
235+
except ValueError:
236+
return page_number
228237
return page_number
229238

230239
def get_paginated_response(self, data):
@@ -384,6 +393,7 @@ class LimitOffsetPagination(BasePagination):
384393
offset_query_description = _('The initial index from which to return the results.')
385394
max_limit = None
386395
template = 'rest_framework/pagination/numbers.html'
396+
allow_negative_offsets = False
387397

388398
def paginate_queryset(self, queryset, request, view=None):
389399
self.request = request
@@ -447,6 +457,11 @@ def get_limit(self, request):
447457

448458
def get_offset(self, request):
449459
try:
460+
if self.allow_negative_offsets:
461+
offset = int(request.query_params[self.offset_query_param])
462+
if offset < 0:
463+
offset = self.count + offset
464+
return max(offset, 0)
450465
return _positive_int(
451466
request.query_params[self.offset_query_param],
452467
)

tests/test_pagination.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,40 @@ def test_invalid_page(self):
260260
with pytest.raises(exceptions.NotFound):
261261
self.paginate_queryset(request)
262262

263+
def test_negative_page(self):
264+
request = Request(factory.get('/', {'page': -1}))
265+
print(request)
266+
with pytest.raises(exceptions.NotFound):
267+
self.paginate_queryset(request)
268+
269+
def test_allowed_negative_page(self):
270+
self.pagination.allow_negative_page_numbers = True
271+
request = Request(factory.get('/', {'page': -2}))
272+
queryset = self.paginate_queryset(request)
273+
content = self.get_paginated_content(queryset)
274+
context = self.get_html_context()
275+
assert queryset == [86, 87, 88, 89, 90]
276+
assert content == {
277+
'results': [86, 87, 88, 89, 90],
278+
'previous': 'http://testserver/?page=17',
279+
'next': 'http://testserver/?page=19',
280+
'count': 100
281+
}
282+
assert context == {
283+
'previous_url': 'http://testserver/?page=17',
284+
'next_url': 'http://testserver/?page=19',
285+
'page_links': [
286+
PageLink('http://testserver/', 1, False, False),
287+
PAGE_BREAK,
288+
PageLink('http://testserver/?page=17', 17, False, False),
289+
PageLink('http://testserver/?page=18', 18, True, False),
290+
PageLink('http://testserver/?page=19', 19, False, False),
291+
PageLink('http://testserver/?page=20', 20, False, False),
292+
]
293+
}
294+
assert self.pagination.display_page_controls
295+
assert isinstance(self.pagination.to_html(), str)
296+
263297
def test_get_paginated_response_schema(self):
264298
unpaginated_schema = {
265299
'type': 'object',
@@ -527,6 +561,42 @@ def test_invalid_offset(self):
527561
queryset = self.paginate_queryset(request)
528562
assert queryset == [1, 2, 3, 4, 5]
529563

564+
def test_negative_offset(self):
565+
"""
566+
A negative offset query param should be treated as 0.
567+
"""
568+
request = Request(factory.get('/', {'limit': 5, 'offset': -5}))
569+
queryset = self.paginate_queryset(request)
570+
assert queryset == [1, 2, 3, 4, 5]
571+
572+
def test_allowed_negative_offset(self):
573+
"""
574+
A negative offset query param should be treated as `count - offset`.
575+
"""
576+
self.pagination.allow_negative_offsets = True
577+
request = Request(factory.get('/', {'limit': 5, 'offset': -10}))
578+
queryset = self.paginate_queryset(request)
579+
content = self.get_paginated_content(queryset)
580+
context = self.get_html_context()
581+
assert queryset == [91, 92, 93, 94, 95]
582+
assert content == {
583+
'results': [91, 92, 93, 94, 95],
584+
'previous': 'http://testserver/?limit=5&offset=85',
585+
'next': 'http://testserver/?limit=5&offset=95',
586+
'count': 100
587+
}
588+
assert context == {
589+
'previous_url': 'http://testserver/?limit=5&offset=85',
590+
'next_url': 'http://testserver/?limit=5&offset=95',
591+
'page_links': [
592+
PageLink('http://testserver/?limit=5', 1, False, False),
593+
PAGE_BREAK,
594+
PageLink('http://testserver/?limit=5&offset=85', 18, False, False),
595+
PageLink('http://testserver/?limit=5&offset=90', 19, True, False),
596+
PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
597+
]
598+
}
599+
530600
def test_invalid_limit(self):
531601
"""
532602
An invalid limit query param should be ignored in favor of the default.

0 commit comments

Comments
 (0)