Skip to content
Draft
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
1 change: 1 addition & 0 deletions requirements/requirements-testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ importlib-metadata<5.0
# temporary pin of attrs
attrs==22.1.0
pytz # Remove when dropping support for Django<5.0
setuptools>=77.0.3
43 changes: 22 additions & 21 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from django.core.paginator import InvalidPage
from django.core.paginator import Paginator as DjangoPaginator
from django.db.models import OrderBy
from django.template import loader
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -615,7 +616,7 @@ def paginate_queryset(self, queryset, request, view=None):
return None

self.base_url = request.build_absolute_uri()
self.ordering = self.get_ordering(request, queryset, view)
self.ordering = self.get_ordering(queryset)

self.cursor = self.decode_cursor(request)
if self.cursor is None:
Expand All @@ -624,10 +625,11 @@ def paginate_queryset(self, queryset, request, view=None):
(offset, reverse, current_position) = self.cursor

# Cursor pagination always enforces an ordering.
if reverse:
queryset = queryset.order_by(*_reverse_ordering(self.ordering))
else:
queryset = queryset.order_by(*self.ordering)
if not queryset.ordered:
if reverse:
queryset = queryset.order_by(*_reverse_ordering(self.ordering))
else:
queryset = queryset.order_by(*self.ordering)

# If we have a cursor with a fixed position then filter by that.
if current_position is not None:
Expand Down Expand Up @@ -801,27 +803,26 @@ def get_previous_link(self):
cursor = Cursor(offset=offset, reverse=True, position=position)
return self.encode_cursor(cursor)

def get_ordering(self, request, queryset, view):
def get_ordering(self, queryset):
"""
Return a tuple of strings, that may be used in an `order_by` method.
"""
# The default case is to check for an `ordering` attribute
# on this pagination instance.
ordering = self.ordering
# Return the ordering value from the queryset if it has one.
if queryset.ordered:
ordering = []

ordering_filters = [
filter_cls for filter_cls in getattr(view, 'filter_backends', [])
if hasattr(filter_cls, 'get_ordering')
]
for expr in queryset.query.order_by:
if isinstance(expr, str):
ordering.append(expr)

elif isinstance(expr, OrderBy):
field_name = expr.expression.name
descending = expr.descending
ordering.append(f"{'-' if descending else ''}{field_name}")

if ordering_filters:
# If a filter exists on the view that implements `get_ordering`
# then we defer to that filter to determine the ordering.
filter_cls = ordering_filters[0]
filter_instance = filter_cls()
ordering_from_filter = filter_instance.get_ordering(request, queryset, view)
if ordering_from_filter:
ordering = ordering_from_filter
return ordering

ordering = self.ordering

assert ordering is not None, (
'Using cursor pagination, but no ordering attribute was declared '
Expand Down
41 changes: 3 additions & 38 deletions tests/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,42 +616,6 @@ def test_invalid_cursor(self):
with pytest.raises(exceptions.NotFound):
self.pagination.paginate_queryset(self.queryset, request)

def test_use_with_ordering_filter(self):
class MockView:
filter_backends = (filters.OrderingFilter,)
ordering_fields = ['username', 'created']
ordering = 'created'

request = Request(factory.get('/', {'ordering': 'username'}))
ordering = self.pagination.get_ordering(request, [], MockView())
assert ordering == ('username',)

request = Request(factory.get('/', {'ordering': '-username'}))
ordering = self.pagination.get_ordering(request, [], MockView())
assert ordering == ('-username',)

request = Request(factory.get('/', {'ordering': 'invalid'}))
ordering = self.pagination.get_ordering(request, [], MockView())
assert ordering == ('created',)

def test_use_with_ordering_filter_without_ordering_default_value(self):
class MockView:
filter_backends = (filters.OrderingFilter,)
ordering_fields = ['username', 'created']

request = Request(factory.get('/'))
ordering = self.pagination.get_ordering(request, [], MockView())
# it gets the value of `ordering` provided by CursorPagination
assert ordering == ('created',)

request = Request(factory.get('/', {'ordering': 'username'}))
ordering = self.pagination.get_ordering(request, [], MockView())
assert ordering == ('username',)

request = Request(factory.get('/', {'ordering': 'invalid'}))
ordering = self.pagination.get_ordering(request, [], MockView())
assert ordering == ('created',)

def test_cursor_pagination(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/')

Expand Down Expand Up @@ -969,8 +933,9 @@ def __init__(self, idx):
self.created = idx

class MockQuerySet:
def __init__(self, items):
def __init__(self, items, ordered=False):
self.items = items
self.ordered = ordered

def filter(self, created__gt=None, created__lt=None):
if created__gt is not None:
Expand All @@ -987,7 +952,7 @@ def filter(self, created__gt=None, created__lt=None):

def order_by(self, *ordering):
if ordering[0].startswith('-'):
return MockQuerySet(list(reversed(self.items)))
return MockQuerySet(list(reversed(self.items)), ordered=True)
return self

def __getitem__(self, sliced):
Expand Down
1 change: 0 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ deps =
djangomain: https://github.com/django/django/archive/main.tar.gz
-rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt
setuptools

[testenv:base]
; Ensure optional dependencies are not required
Expand Down