Skip to content

Commit 6a037f6

Browse files
Added OrderingFilter
1 parent 08bc976 commit 6a037f6

File tree

3 files changed

+186
-3
lines changed

3 files changed

+186
-3
lines changed

docs/api-guide/filtering.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,15 @@ The `SearchFilterBackend` class will only be applied if the view has a `search_f
190190
filter_backends = (filters.SearchFilter,)
191191
search_fields = ('username', 'email')
192192

193-
This will allow the client to filter the itemss in the list by making queries such as:
193+
This will allow the client to filter the items in the list by making queries such as:
194194

195195
http://example.com/api/users?search=russell
196196

197197
You can also perform a related lookup on a ForeignKey or ManyToManyField with the lookup API double-underscore notation:
198198

199199
search_fields = ('username', 'email', 'profile__profession')
200200

201-
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.
201+
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.
202202

203203
The search behavior may be restricted by prepending various characters to the `search_fields`.
204204

@@ -214,6 +214,34 @@ For more details, see the [Django documentation][search-django-admin].
214214

215215
---
216216

217+
## OrderingFilter
218+
219+
The `OrderingFilter` class supports simple query parameter controlled ordering of results. For example:
220+
221+
http://example.com/api/users?ordering=username
222+
223+
The client may also specify reverse orderings by prefixing the field name with '-', like so:
224+
225+
http://example.com/api/users?ordering=-username
226+
227+
Multiple orderings may also be specified:
228+
229+
http://example.com/api/users?ordering=account,username
230+
231+
If an `ordering` attribute is set on the view, this will be used as the default ordering.
232+
233+
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.
234+
235+
class UserListView(generics.ListAPIView):
236+
queryset = User.objects.all()
237+
serializer = UserSerializer
238+
filter_backends = (filters.OrderingFilter,)
239+
ordering = ('username',)
240+
241+
The `ordering` attribute may be either a string or a list/tuple of strings.
242+
243+
---
244+
217245
# Custom generic filtering
218246

219247
You can also provide your own generic filtering backend, or write an installable app for other developers to use.

rest_framework/filters.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55
from __future__ import unicode_literals
66
from django.db import models
7-
from rest_framework.compat import django_filters
7+
from rest_framework.compat import django_filters, six
88
from functools import reduce
99
import operator
1010

@@ -109,3 +109,42 @@ def filter_queryset(self, request, queryset, view):
109109
queryset = queryset.filter(reduce(operator.or_, or_queries))
110110

111111
return queryset
112+
113+
114+
class OrderingFilter(BaseFilterBackend):
115+
ordering_param = 'order' # The URL query parameter used for the ordering.
116+
117+
def get_ordering(self, request):
118+
"""
119+
Search terms are set by a ?search=... query parameter,
120+
and may be comma and/or whitespace delimited.
121+
"""
122+
params = request.QUERY_PARAMS.get(self.ordering_param)
123+
if params:
124+
return [param.strip() for param in params.split(',')]
125+
126+
def get_default_ordering(self, view):
127+
ordering = getattr(view, 'ordering', None)
128+
if isinstance(ordering, six.string_types):
129+
return (ordering,)
130+
return ordering
131+
132+
def remove_invalid_fields(self, queryset, ordering):
133+
field_names = [field.name for field in queryset.model._meta.fields]
134+
return [term for term in ordering if term.lstrip('-') in field_names]
135+
136+
def filter_queryset(self, request, queryset, view):
137+
ordering = self.get_ordering(request)
138+
139+
if ordering:
140+
# Skip any incorrect parameters
141+
ordering = self.remove_invalid_fields(queryset, ordering)
142+
143+
if not ordering:
144+
# Use 'ordering' attribtue by default
145+
ordering = self.get_default_ordering(view)
146+
147+
if ordering:
148+
return queryset.order_by(*ordering)
149+
150+
return queryset

rest_framework/tests/filters.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,119 @@ class SearchListView(generics.ListAPIView):
335335
{'id': 2, 'title': 'zz', 'text': 'bcd'}
336336
]
337337
)
338+
339+
340+
class OrdringFilterModel(models.Model):
341+
title = models.CharField(max_length=20)
342+
text = models.CharField(max_length=100)
343+
344+
345+
class OrderingFilterTests(TestCase):
346+
def setUp(self):
347+
# Sequence of title/text is:
348+
#
349+
# zyx abc
350+
# yxw bcd
351+
# xwv cde
352+
for idx in range(3):
353+
title = (
354+
chr(ord('z') - idx) +
355+
chr(ord('y') - idx) +
356+
chr(ord('x') - idx)
357+
)
358+
text = (
359+
chr(idx + ord('a')) +
360+
chr(idx + ord('b')) +
361+
chr(idx + ord('c'))
362+
)
363+
OrdringFilterModel(title=title, text=text).save()
364+
365+
def test_ordering(self):
366+
class OrderingListView(generics.ListAPIView):
367+
model = OrdringFilterModel
368+
filter_backends = (filters.OrderingFilter,)
369+
ordering = ('title',)
370+
371+
view = OrderingListView.as_view()
372+
request = factory.get('?order=text')
373+
response = view(request)
374+
self.assertEqual(
375+
response.data,
376+
[
377+
{'id': 1, 'title': 'zyx', 'text': 'abc'},
378+
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
379+
{'id': 3, 'title': 'xwv', 'text': 'cde'},
380+
]
381+
)
382+
383+
def test_reverse_ordering(self):
384+
class OrderingListView(generics.ListAPIView):
385+
model = OrdringFilterModel
386+
filter_backends = (filters.OrderingFilter,)
387+
ordering = ('title',)
388+
389+
view = OrderingListView.as_view()
390+
request = factory.get('?order=-text')
391+
response = view(request)
392+
self.assertEqual(
393+
response.data,
394+
[
395+
{'id': 3, 'title': 'xwv', 'text': 'cde'},
396+
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
397+
{'id': 1, 'title': 'zyx', 'text': 'abc'},
398+
]
399+
)
400+
401+
def test_incorrectfield_ordering(self):
402+
class OrderingListView(generics.ListAPIView):
403+
model = OrdringFilterModel
404+
filter_backends = (filters.OrderingFilter,)
405+
ordering = ('title',)
406+
407+
view = OrderingListView.as_view()
408+
request = factory.get('?order=foobar')
409+
response = view(request)
410+
self.assertEqual(
411+
response.data,
412+
[
413+
{'id': 3, 'title': 'xwv', 'text': 'cde'},
414+
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
415+
{'id': 1, 'title': 'zyx', 'text': 'abc'},
416+
]
417+
)
418+
419+
def test_default_ordering(self):
420+
class OrderingListView(generics.ListAPIView):
421+
model = OrdringFilterModel
422+
filter_backends = (filters.OrderingFilter,)
423+
ordering = ('title',)
424+
425+
view = OrderingListView.as_view()
426+
request = factory.get('')
427+
response = view(request)
428+
self.assertEqual(
429+
response.data,
430+
[
431+
{'id': 3, 'title': 'xwv', 'text': 'cde'},
432+
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
433+
{'id': 1, 'title': 'zyx', 'text': 'abc'},
434+
]
435+
)
436+
437+
def test_default_ordering_using_string(self):
438+
class OrderingListView(generics.ListAPIView):
439+
model = OrdringFilterModel
440+
filter_backends = (filters.OrderingFilter,)
441+
ordering = 'title'
442+
443+
view = OrderingListView.as_view()
444+
request = factory.get('')
445+
response = view(request)
446+
self.assertEqual(
447+
response.data,
448+
[
449+
{'id': 3, 'title': 'xwv', 'text': 'cde'},
450+
{'id': 2, 'title': 'yxw', 'text': 'bcd'},
451+
{'id': 1, 'title': 'zyx', 'text': 'abc'},
452+
]
453+
)

0 commit comments

Comments
 (0)