aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/filtering.md46
-rw-r--r--docs/topics/release-notes.md5
-rw-r--r--rest_framework/filters.py53
-rw-r--r--rest_framework/routers.py2
-rw-r--r--rest_framework/tests/filters.py (renamed from rest_framework/tests/filterset.py)116
5 files changed, 213 insertions, 9 deletions
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index b5dfc68e..a710ad7d 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -182,7 +182,7 @@ For more details on using filter sets see the [django-filter documentation][djan
The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
-The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text fields on the model.
+The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
@@ -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,12 +198,50 @@ 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. If the search parameter contains multiple whitespace seperated words, then objects will be returned in the list only if all the provided words 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`.
+
+* '^' Starts-with search.
+* '=' Exact matches.
+* '@' Full-text search. (Currently only supported Django's MySQL backend.)
+
+For example:
+
+ search_fields = ('=username', '=email')
For more details, see the [Django documentation][search-django-admin].
---
+## OrderingFilter
+
+The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'order'` to the required field name. 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.
@@ -223,7 +261,7 @@ For example, you might need to restrict users to only being able to see objects
def filter_queryset(self, request, queryset, view):
return queryset.filter(owner=request.user)
-We could do the same thing by overriding `get_queryset` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
+We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter]: https://github.com/alex/django-filter
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 259aafdd..7ec3d79a 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
+### Master
+
+* Added SearchFilter
+* Added GenericViewSet
+
### 2.3.2
**Date**: 8th May 2013
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 57f0f7c8..6a3e055d 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
@@ -75,7 +75,14 @@ class DjangoFilterBackend(BaseFilterBackend):
class SearchFilter(BaseFilterBackend):
search_param = 'search' # The URL query parameter used for the search.
- delimiter = None # For example, set to ',' for comma delimited searchs.
+
+ def get_search_terms(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.search_param)
+ return params.replace(',', ' ').split()
def construct_search(self, field_name):
if field_name.startswith('^'):
@@ -93,13 +100,51 @@ class SearchFilter(BaseFilterBackend):
if not search_fields:
return None
- search_terms = request.QUERY_PARAMS.get(self.search_param)
orm_lookups = [self.construct_search(str(search_field))
for search_field in search_fields]
- for search_term in search_terms.split(self.delimiter):
+ for search_term in self.get_search_terms(request):
or_queries = [models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups]
queryset = queryset.filter(reduce(operator.or_, or_queries))
return queryset
+
+
+class OrderingFilter(BaseFilterBackend):
+ ordering_param = 'ordering' # 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/routers.py b/rest_framework/routers.py
index ebdf2b2a..76714fd0 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -16,7 +16,7 @@ For example, you might have a `urls.py` that looks something like this:
from __future__ import unicode_literals
from collections import namedtuple
-from django.conf.urls import url, patterns
+from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filters.py
index e5414232..b20d5980 100644
--- a/rest_framework/tests/filterset.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('?ordering=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('?ordering=-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('?ordering=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'},
+ ]
+ )