This commit is contained in:
Ülgen Sarıkavak 2025-12-02 16:44:53 +00:00 committed by GitHub
commit 21ef11e798
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 60 deletions

View File

@ -11,6 +11,7 @@ from urllib import parse
from django.core.paginator import InvalidPage from django.core.paginator import InvalidPage
from django.core.paginator import Paginator as DjangoPaginator from django.core.paginator import Paginator as DjangoPaginator
from django.db.models import OrderBy
from django.template import loader from django.template import loader
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -615,7 +616,7 @@ class CursorPagination(BasePagination):
return None return None
self.base_url = request.build_absolute_uri() 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) self.cursor = self.decode_cursor(request)
if self.cursor is None: if self.cursor is None:
@ -624,10 +625,11 @@ class CursorPagination(BasePagination):
(offset, reverse, current_position) = self.cursor (offset, reverse, current_position) = self.cursor
# Cursor pagination always enforces an ordering. # Cursor pagination always enforces an ordering.
if reverse: if not queryset.ordered:
queryset = queryset.order_by(*_reverse_ordering(self.ordering)) if reverse:
else: queryset = queryset.order_by(*_reverse_ordering(self.ordering))
queryset = queryset.order_by(*self.ordering) else:
queryset = queryset.order_by(*self.ordering)
# If we have a cursor with a fixed position then filter by that. # If we have a cursor with a fixed position then filter by that.
if current_position is not None: if current_position is not None:
@ -801,28 +803,38 @@ class CursorPagination(BasePagination):
cursor = Cursor(offset=offset, reverse=True, position=position) cursor = Cursor(offset=offset, reverse=True, position=position)
return self.encode_cursor(cursor) return self.encode_cursor(cursor)
def get_ordering(self, request, queryset, view): def get_ordering_from_queryset(self, queryset):
if not queryset.ordered:
return False
qs_ordering = queryset.query.order_by
# Fallback to model's Meta ordering if no order_by is given
if not qs_ordering:
qs_ordering = queryset.query.get_meta().ordering
ordering = []
for expr in qs_ordering:
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}")
return ordering
def get_ordering(self, queryset):
""" """
Return a tuple of strings, that may be used in an `order_by` method. Return a tuple of strings, that may be used in an `order_by` method.
""" """
# The default case is to check for an `ordering` attribute # Return the ordering value from the queryset if it has one.
# on this pagination instance. if queryset.ordered:
return self.get_ordering_from_queryset(queryset)
ordering = self.ordering ordering = self.ordering
ordering_filters = [
filter_cls for filter_cls in getattr(view, 'filter_backends', [])
if hasattr(filter_cls, 'get_ordering')
]
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
assert ordering is not None, ( assert ordering is not None, (
'Using cursor pagination, but no ordering attribute was declared ' 'Using cursor pagination, but no ordering attribute was declared '
'on the pagination class.' 'on the pagination class.'

View File

@ -616,42 +616,6 @@ class CursorPaginationTestsMixin:
with pytest.raises(exceptions.NotFound): with pytest.raises(exceptions.NotFound):
self.pagination.paginate_queryset(self.queryset, request) 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): def test_cursor_pagination(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/') (previous, current, next, previous_url, next_url) = self.get_pages('/')
@ -969,8 +933,9 @@ class TestCursorPagination(CursorPaginationTestsMixin):
self.created = idx self.created = idx
class MockQuerySet: class MockQuerySet:
def __init__(self, items): def __init__(self, items, ordered=False):
self.items = items self.items = items
self.ordered = ordered
def filter(self, created__gt=None, created__lt=None): def filter(self, created__gt=None, created__lt=None):
if created__gt is not None: if created__gt is not None:
@ -987,7 +952,7 @@ class TestCursorPagination(CursorPaginationTestsMixin):
def order_by(self, *ordering): def order_by(self, *ordering):
if ordering[0].startswith('-'): if ordering[0].startswith('-'):
return MockQuerySet(list(reversed(self.items))) return MockQuerySet(list(reversed(self.items)), ordered=True)
return self return self
def __getitem__(self, sliced): def __getitem__(self, sliced):