diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index b1e3c82eca..a3821a5089 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -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 diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index b6329b8c3a..934ba14472 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -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 _ @@ -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: @@ -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: @@ -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 ' diff --git a/tests/test_pagination.py b/tests/test_pagination.py index d8f66e95bc..af8e82b6b3 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -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('/') @@ -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: @@ -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): diff --git a/tox.ini b/tox.ini index cbaaf159e5..01a959455c 100644 --- a/tox.ini +++ b/tox.ini @@ -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