aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2013-05-14 11:27:03 +0100
committerTom Christie2013-05-14 11:27:03 +0100
commit6a037f63edf33e7a76f56828cf68bfae4ccb4f80 (patch)
treec33f1d00434d987c2feb532c314102f4e260dbb9
parent08bc97626960f108f01657e4ad12b7fd62e6183d (diff)
downloaddjango-rest-framework-6a037f63edf33e7a76f56828cf68bfae4ccb4f80.tar.bz2
Added OrderingFilter
-rw-r--r--docs/api-guide/filtering.md32
-rw-r--r--rest_framework/filters.py41
-rw-r--r--rest_framework/tests/filters.py116
3 files changed, 186 insertions, 3 deletions
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 94244301..7e99832e 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -190,7 +190,7 @@ The `SearchFilterBackend` class will only be applied if the view has a `search_f
filter_backends = (filters.SearchFilter,)
search_fields = ('username', 'email')
-This will allow the client to filter the itemss in the list by making queries such as:
+This will allow the client to filter the items in the list by making queries such as:
http://example.com/api/users?search=russell
@@ -198,7 +198,7 @@ You can also perform a related lookup on a ForeignKey or ManyToManyField with th
search_fields = ('username', 'email', 'profile__profession')
-By default, searches will use case-insensitive partial matches. The search parameter may contain multiple search terms, which should be whitespace and/or comma seperated. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched.
+By default, searches will use case-insensitive partial matches. The search parameter may contain multiple search terms, which should be whitespace and/or comma separated. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched.
The search behavior may be restricted by prepending various characters to the `search_fields`.
@@ -214,6 +214,34 @@ For more details, see the [Django documentation][search-django-admin].
---
+## OrderingFilter
+
+The `OrderingFilter` class supports simple query parameter controlled ordering of results. For example:
+
+ http://example.com/api/users?ordering=username
+
+The client may also specify reverse orderings by prefixing the field name with '-', like so:
+
+ http://example.com/api/users?ordering=-username
+
+Multiple orderings may also be specified:
+
+ http://example.com/api/users?ordering=account,username
+
+If an `ordering` attribute is set on the view, this will be used as the default ordering.
+
+Typicaly you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
+
+ class UserListView(generics.ListAPIView):
+ queryset = User.objects.all()
+ serializer = UserSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('username',)
+
+The `ordering` attribute may be either a string or a list/tuple of strings.
+
+---
+
# Custom generic filtering
You can also provide your own generic filtering backend, or write an installable app for other developers to use.
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index c496ec4b..308e7da2 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -4,7 +4,7 @@ returned by list views.
"""
from __future__ import unicode_literals
from django.db import models
-from rest_framework.compat import django_filters
+from rest_framework.compat import django_filters, six
from functools import reduce
import operator
@@ -109,3 +109,42 @@ class SearchFilter(BaseFilterBackend):
queryset = queryset.filter(reduce(operator.or_, or_queries))
return queryset
+
+
+class OrderingFilter(BaseFilterBackend):
+ ordering_param = 'order' # The URL query parameter used for the ordering.
+
+ def get_ordering(self, request):
+ """
+ Search terms are set by a ?search=... query parameter,
+ and may be comma and/or whitespace delimited.
+ """
+ params = request.QUERY_PARAMS.get(self.ordering_param)
+ if params:
+ return [param.strip() for param in params.split(',')]
+
+ 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, ordering):
+ field_names = [field.name for field in queryset.model._meta.fields]
+ return [term for term in ordering if term.lstrip('-') in field_names]
+
+ def filter_queryset(self, request, queryset, view):
+ ordering = self.get_ordering(request)
+
+ if ordering:
+ # Skip any incorrect parameters
+ ordering = self.remove_invalid_fields(queryset, ordering)
+
+ if not ordering:
+ # Use 'ordering' attribtue by default
+ ordering = self.get_default_ordering(view)
+
+ if ordering:
+ return queryset.order_by(*ordering)
+
+ return queryset
diff --git a/rest_framework/tests/filters.py b/rest_framework/tests/filters.py
index e5414232..6b604deb 100644
--- a/rest_framework/tests/filters.py
+++ b/rest_framework/tests/filters.py
@@ -335,3 +335,119 @@ class SearchFilterTests(TestCase):
{'id': 2, 'title': 'zz', 'text': 'bcd'}
]
)
+
+
+class OrdringFilterModel(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+
+
+class OrderingFilterTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text is:
+ #
+ # zyx abc
+ # yxw bcd
+ # xwv cde
+ for idx in range(3):
+ title = (
+ chr(ord('z') - idx) +
+ chr(ord('y') - idx) +
+ chr(ord('x') - idx)
+ )
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ OrdringFilterModel(title=title, text=text).save()
+
+ def test_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('?order=text')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ ]
+ )
+
+ def test_reverse_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('?order=-text')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_incorrectfield_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('?order=foobar')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_default_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_default_ordering_using_string(self):
+ class OrderingListView(generics.ListAPIView):
+ model = OrdringFilterModel
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+
+ view = OrderingListView.as_view()
+ request = factory.get('')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )