Skip to content

Instantly share code, notes, and snippets.

@sehmaschine
Created September 14, 2015 18:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sehmaschine/678403f7e5ffa829781c to your computer and use it in GitHub Desktop.
Save sehmaschine/678403f7e5ffa829781c to your computer and use it in GitHub Desktop.
OrderingFilter for Django Rest Framework
# coding: utf-8
# PYTHON IMPORTS
from django.utils import six
from django.core.exceptions import ImproperlyConfigured
# REST IMPORTS
from rest_framework.settings import api_settings
from rest_framework.filters import BaseFilterBackend
class OrderingFilter(BaseFilterBackend):
# The URL query parameter used for the ordering.
ordering_param = api_settings.ORDERING_PARAM
ordering_fields = None
def get_ordering(self, request, queryset, view):
"""
Ordering is set by a comma delimited ?ordering=... query parameter.
The `ordering` query parameter can be overridden by setting
the `ordering_param` value on the OrderingFilter or by
specifying an `ORDERING_PARAM` value in the API settings.
"""
params = request.query_params.get(self.ordering_param)
if params:
fields = [param.strip() for param in params.split(',')]
ordering = self.remove_invalid_fields(queryset, fields, view)
if ordering:
return ordering
# No ordering was included, or all the ordering fields were invalid
return self.get_default_ordering(view)
def get_default_ordering(self, view):
ordering = getattr(view, 'ordering', None)
if isinstance(ordering, six.string_types):
return (ordering,)
return ordering
def remove_invalid_fields(self, queryset, fields, view):
ordering_fields = getattr(view, 'ordering_fields', self.ordering_fields)
if not ordering_fields == '__all__':
serializer_class = getattr(view, 'serializer_class', None)
if serializer_class is None:
serializer_class = view.get_serializer_class()
if serializer_class is None:
msg = ("Cannot use %s on a view which does not have either a "
"'serializer_class' or 'ordering_fields' attribute.")
raise ImproperlyConfigured(msg % self.__class__.__name__)
if ordering_fields is None:
# Default to allowing filtering on serializer field names (return field sources)
valid_fields = [
(field.source, field_name)
for field_name, field in serializer_class().fields.items()
if not getattr(field, 'write_only', False)
]
return [term[0] for term in valid_fields if term[0] != "*"]
elif ordering_fields == '__all__':
# View explicitly allows filtering on any model field
valid_fields = [field.name for field in queryset.model._meta.fields]
valid_fields += queryset.query.aggregates.keys()
return [term for term in fields if term.lstrip('-') in valid_fields]
else:
# Allow filtering on defined field name (return field sources)
valid_fields = [
(field.source, field_name)
for field_name, field in serializer_class().fields.items()
if not getattr(field, 'write_only', False)
]
return [term[0] for term in valid_fields if term[0] != "*" and term[1].lstrip('-') in fields]
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request, queryset, view)
if ordering:
return queryset.order_by(*ordering)
return queryset
@sehmaschine
Copy link
Author

In contrast to the original OrderingFilter of DRF, this one allows for ordering on serializer fields (not model fields).

Let's say you defined a field with your serializer class like this:

group = serializers.CharField(source="category")

With the original OrderingFilter you can only use category as an ordering field, with the new OrderingFilter you can use group:

ordering_fields = ("id", "group",)

I think this behaviour is better, because an API user doesn't know that the field category even exists (it is not part of the response with a GET request). Moreover, you can use group with both filters and ordering, so your API is consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment