diff options
| -rw-r--r-- | docs/api-guide/filtering.md | 46 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 5 | ||||
| -rw-r--r-- | rest_framework/filters.py | 53 | ||||
| -rw-r--r-- | rest_framework/routers.py | 2 | ||||
| -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'}, + ] + ) |
