authentik.api.ordering

 1from django.db.models import F, QuerySet
 2from rest_framework.filters import OrderingFilter
 3from rest_framework.request import Request
 4from rest_framework.views import APIView
 5
 6
 7class NullsAwareOrderingFilter(OrderingFilter):
 8    """OrderingFilter that sorts NULL values consistently.
 9
10    For any nullable field, NULLs are treated as the smallest possible value:
11    - ascending  → NULLs appear first  (nulls_first=True)
12    - descending → NULLs appear last   (nulls_last=True)
13    """
14
15    def _nullable_field_names(self, queryset: QuerySet) -> set[str]:
16        return {f.name for f in queryset.model._meta.get_fields() if hasattr(f, "null") and f.null}
17
18    def filter_queryset(self, request: Request, queryset: QuerySet, view: APIView):
19        queryset = super().filter_queryset(request, queryset, view)
20        ordering = queryset.query.order_by
21        if not ordering:
22            return queryset
23        nullable = self._nullable_field_names(queryset)
24        new_ordering = []
25        changed = False
26        for term in ordering:
27            name = term.lstrip("-")
28            if name in nullable:
29                changed = True
30                if term.startswith("-"):
31                    new_ordering.append(F(name).desc(nulls_last=True))
32                else:
33                    new_ordering.append(F(name).asc(nulls_first=True))
34            else:
35                new_ordering.append(term)
36        return queryset.order_by(*new_ordering) if changed else queryset
class NullsAwareOrderingFilter(rest_framework.filters.OrderingFilter):
 8class NullsAwareOrderingFilter(OrderingFilter):
 9    """OrderingFilter that sorts NULL values consistently.
10
11    For any nullable field, NULLs are treated as the smallest possible value:
12    - ascending  → NULLs appear first  (nulls_first=True)
13    - descending → NULLs appear last   (nulls_last=True)
14    """
15
16    def _nullable_field_names(self, queryset: QuerySet) -> set[str]:
17        return {f.name for f in queryset.model._meta.get_fields() if hasattr(f, "null") and f.null}
18
19    def filter_queryset(self, request: Request, queryset: QuerySet, view: APIView):
20        queryset = super().filter_queryset(request, queryset, view)
21        ordering = queryset.query.order_by
22        if not ordering:
23            return queryset
24        nullable = self._nullable_field_names(queryset)
25        new_ordering = []
26        changed = False
27        for term in ordering:
28            name = term.lstrip("-")
29            if name in nullable:
30                changed = True
31                if term.startswith("-"):
32                    new_ordering.append(F(name).desc(nulls_last=True))
33                else:
34                    new_ordering.append(F(name).asc(nulls_first=True))
35            else:
36                new_ordering.append(term)
37        return queryset.order_by(*new_ordering) if changed else queryset

OrderingFilter that sorts NULL values consistently.

For any nullable field, NULLs are treated as the smallest possible value:

  • ascending → NULLs appear first (nulls_first=True)
  • descending → NULLs appear last (nulls_last=True)
def filter_queryset( self, request: rest_framework.request.Request, queryset: django.db.models.query.QuerySet, view: rest_framework.views.APIView):
19    def filter_queryset(self, request: Request, queryset: QuerySet, view: APIView):
20        queryset = super().filter_queryset(request, queryset, view)
21        ordering = queryset.query.order_by
22        if not ordering:
23            return queryset
24        nullable = self._nullable_field_names(queryset)
25        new_ordering = []
26        changed = False
27        for term in ordering:
28            name = term.lstrip("-")
29            if name in nullable:
30                changed = True
31                if term.startswith("-"):
32                    new_ordering.append(F(name).desc(nulls_last=True))
33                else:
34                    new_ordering.append(F(name).asc(nulls_first=True))
35            else:
36                new_ordering.append(term)
37        return queryset.order_by(*new_ordering) if changed else queryset

Return a filtered queryset.