Django Rest Framework Cursos pagination with multiple ordering fields and filters
I have an issue with DRF, CursorPagination and Filters.
I have an endpoint. When I access the initial page of the enpoint I get a next URL
"next": "http://my-url/api/my-endpoint/?cursor=bz0yMDA%3D&date__gte=2025-04-25T10%3A00%3A00Z&date__lte=2025-04-26T10%3A00%3A00Z"
When I access this URL I get a previous URL
"next": "http://my-url/api/my-endpoint/?cursor=bz00MDA%3D&date__gte=2025-04-25T10%3A00%3A00Z&date__lte=2025-04-26T10%3A00%3A00Z",
"previous": "http://my-url/api/my-endpoint/?cursor=cj0xJnA9MjAyNS0wNC0yNSsxMCUzQTAwJTNBMDAlMkIwMCUzQTAw&date__gte=2025-04-25T10%3A00%3A00Z&date__lte=2025-04-26T10%3A00%3A00Z",
Now when I try to access the previous URL, I get an empty result list.
Here is the code for the endpoint
class RevenuePagination(CursorPagination):
page_size = 200
ordering = ['date', 'custom_channel_name', 'date', 'country_name', 'platform_type_code', 'id']
class RevenueFilter(django_filters.FilterSet):
class Meta:
model = Revenue
fields = {
'date': ['lte', 'gte'],
'custom_channel_name': ['exact'],
'country_name': ['exact'],
'platform_type_code': ['exact'],
}
class RevenueViewSet(viewsets.ModelViewSet):
permission_classes = [HasAPIKey]
queryset = Revenue.objects.all()
serializer_class = RevenueSerializer
filterset_class = RevenueFilter
pagination_class = RevenuePagination
ordering = ['date', 'custom_channel_name', 'date', 'country_name', 'platform_type_code', 'id']
ordering_fields = ['date', 'custom_channel_name', 'date', 'country_name', 'platform_type_code', 'id']
@revenue_list_schema()
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
From what I get, the problem seems to be that the previous URL cursor (cj0xJnA9MjAyNS0wNC0yNSsxMCUzQTAwJTNBMDAlMkIwMCUzQTAw
) gets decoded to r=1&p=2025-04-25+10:00:00+00:00
, which is not right, because only the date field is in the cursor, while I have specidied 6 fields in the ordering of the pagination.
I tried it narrowing down the order to ['date', 'id'], but it does not work.
All the fields in the ordering are actual DB fields (DateTime, Char or Integer), no properties.
I am really struggling, tried debugging with Google AI and ChatGPT, both came to a dead end.
ordering
can (and often should!) be multiple fields.
But you're right that only the first field is stored in the cursor token. However, the other fields are still applied:
# rest_framework/pagination.py
class CursorPagination(BasePagination):
[...]
def paginate_queryset(self, queryset, request, view=None):
[...]
'''
The below part is where the ordering fields come into play.
They are not stored in the token, instead they are reapplied
on every hit of the paginator.
'''
# Cursor pagination always enforces an ordering.
if reverse:
queryset = queryset.order_by(*_reverse_ordering(self.ordering))
else:
queryset = queryset.order_by(*self.ordering)
So this part of the investigation is a red herring.
What I'm willing to bet is the culprit is the fact that your ordering
list contains date
twice. I haven't tested it, but there's a chance that including the same field twice leads to some kind of nonsense SQL being generated at some point in this operation. It might also be data-dependent for all I know.
Try removing the duplicate date
.
Alternatively, inspect what .order_by()
produces for you and/or look at the raw SQL to verify that the query isn't malformed.