From 0d7afccd87493e3240c26ed88c8ac7386f78c6e5 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Wed, 22 Aug 2018 10:48:12 -0400 Subject: [PATCH 1/2] rename `backends` to `filters` --- docs/usage.md | 18 ++++++++++++++++-- example/settings/dev.py | 3 +-- .../{test_backends.py => test_filters.py} | 0 .../{backends.py => filters.py} | 0 4 files changed, 17 insertions(+), 4 deletions(-) rename example/tests/{test_backends.py => test_filters.py} (100%) rename rest_framework_json_api/{backends.py => filters.py} (100%) diff --git a/docs/usage.md b/docs/usage.md index e172df47..b9d89ecf 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -33,7 +33,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata', 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework_json_api.backends.JSONAPIOrderingFilter', + 'rest_framework_json_api.filters.JSONAPIOrderingFilter', ), 'TEST_REQUEST_RENDERER_CLASSES': ( 'rest_framework_json_api.renderers.JSONRenderer', @@ -98,7 +98,7 @@ _This is the first of several anticipated JSON:API-specific filter backends._ `JSONAPIOrderingFilter` implements the [JSON:API `sort`](http://jsonapi.org/format/#fetching-sorting) and uses DRF's [ordering filter](http://django-rest-framework.readthedocs.io/en/latest/api-guide/filtering/#orderingfilter). -Per the JSON:API, "If the server does not support sorting as specified in the query parameter `sort`, +Per the JSON:API specification, "If the server does not support sorting as specified in the query parameter `sort`, it **MUST** return `400 Bad Request`." For example, for `?sort=`abc,foo,def` where `foo` is a valid field name and the other two are not valid: ```json @@ -118,6 +118,20 @@ field name and the other two are not valid: If you want to silently ignore bad sort fields, just use `rest_framework.filters.OrderingFilter` and set `ordering_param` to `sort`. +#### Configuring Filter Backends + +You can configure the filter backends either by setting the `REST_FRAMEWORK['DEFAULT_FILTER_BACKENDS']` as shown +in the [preceding](#configuration) example or individually add them as `.filter_backends` View attributes: + + ```python +from rest_framework_json_api import filters + +class MyViewset(ModelViewSet): + queryset = MyModel.objects.all() + serializer_class = MyModelSerializer + filter_backends = (filters.JSONAPIOrderingFilter,) +``` + ### Performance Testing diff --git a/example/settings/dev.py b/example/settings/dev.py index e8ed4094..6856a91b 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -89,9 +89,8 @@ ), 'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata', 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework_json_api.backends.JSONAPIOrderingFilter', + 'rest_framework_json_api.filters.JSONAPIOrderingFilter', ), - 'ORDERING_PARAM': 'sort', 'TEST_REQUEST_RENDERER_CLASSES': ( 'rest_framework_json_api.renderers.JSONRenderer', ), diff --git a/example/tests/test_backends.py b/example/tests/test_filters.py similarity index 100% rename from example/tests/test_backends.py rename to example/tests/test_filters.py diff --git a/rest_framework_json_api/backends.py b/rest_framework_json_api/filters.py similarity index 100% rename from rest_framework_json_api/backends.py rename to rest_framework_json_api/filters.py From d77b17a2c3c462ba8a00cf30d1aaf561a56fc810 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Wed, 22 Aug 2018 17:18:46 -0400 Subject: [PATCH 2/2] bugfix: camelcase, etc. sort parameters were being ignored. Also added more test_cases and suppor for sorting via relationship paths. --- example/tests/test_filters.py | 59 ++++++++++++++++++++++++++++-- example/views.py | 1 + rest_framework_json_api/filters.py | 24 ++++++++---- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/example/tests/test_filters.py b/example/tests/test_filters.py index 0721f780..2b18b5f3 100644 --- a/example/tests/test_filters.py +++ b/example/tests/test_filters.py @@ -24,8 +24,7 @@ def test_sort(self): msg=response.content.decode("utf-8")) dja_response = response.json() headlines = [c['attributes']['headline'] for c in dja_response['data']] - sorted_headlines = [c['attributes']['headline'] for c in dja_response['data']] - sorted_headlines.sort() + sorted_headlines = sorted(headlines) self.assertEqual(headlines, sorted_headlines) def test_sort_reverse(self): @@ -37,8 +36,19 @@ def test_sort_reverse(self): msg=response.content.decode("utf-8")) dja_response = response.json() headlines = [c['attributes']['headline'] for c in dja_response['data']] - sorted_headlines = [c['attributes']['headline'] for c in dja_response['data']] - sorted_headlines.sort() + sorted_headlines = sorted(headlines) + self.assertNotEqual(headlines, sorted_headlines) + + def test_sort_double_negative(self): + """ + what if they provide multiple `-`'s? It's OK. + """ + response = self.client.get(self.url, data={'sort': '--headline'}) + self.assertEqual(response.status_code, 200, + msg=response.content.decode("utf-8")) + dja_response = response.json() + headlines = [c['attributes']['headline'] for c in dja_response['data']] + sorted_headlines = sorted(headlines) self.assertNotEqual(headlines, sorted_headlines) def test_sort_invalid(self): @@ -52,3 +62,44 @@ def test_sort_invalid(self): dja_response = response.json() self.assertEqual(dja_response['errors'][0]['detail'], "invalid sort parameters: nonesuch,-not_a_field") + + def test_sort_camelcase(self): + """ + test sort of camelcase field name + """ + response = self.client.get(self.url, data={'sort': 'bodyText'}) + self.assertEqual(response.status_code, 200, + msg=response.content.decode("utf-8")) + dja_response = response.json() + blog_ids = [(c['attributes']['bodyText'] or '') for c in dja_response['data']] + sorted_blog_ids = sorted(blog_ids) + self.assertEqual(blog_ids, sorted_blog_ids) + + def test_sort_underscore(self): + """ + test sort of underscore field name + Do we allow this notation in a search even if camelcase is in effect? + "Be conservative in what you send, be liberal in what you accept" + -- https://en.wikipedia.org/wiki/Robustness_principle + """ + response = self.client.get(self.url, data={'sort': 'body_text'}) + self.assertEqual(response.status_code, 200, + msg=response.content.decode("utf-8")) + dja_response = response.json() + blog_ids = [(c['attributes']['bodyText'] or '') for c in dja_response['data']] + sorted_blog_ids = sorted(blog_ids) + self.assertEqual(blog_ids, sorted_blog_ids) + + def test_sort_related(self): + """ + test sort via related field using jsonapi path `.` and django orm `__` notation. + ORM relations must be predefined in the View's .ordering_fields attr + """ + for datum in ('blog__id', 'blog.id'): + response = self.client.get(self.url, data={'sort': datum}) + self.assertEqual(response.status_code, 200, + msg=response.content.decode("utf-8")) + dja_response = response.json() + blog_ids = [c['relationships']['blog']['data']['id'] for c in dja_response['data']] + sorted_blog_ids = sorted(blog_ids) + self.assertEqual(blog_ids, sorted_blog_ids) diff --git a/example/views.py b/example/views.py index a42a80ae..36026b17 100644 --- a/example/views.py +++ b/example/views.py @@ -90,6 +90,7 @@ class NoPagination(PageNumberPagination): class NonPaginatedEntryViewSet(EntryViewSet): pagination_class = NoPagination + ordering_fields = ('headline', 'body_text', 'blog__name', 'blog__id') class AuthorViewSet(ModelViewSet): diff --git a/rest_framework_json_api/filters.py b/rest_framework_json_api/filters.py index e6fda16b..748b18bf 100644 --- a/rest_framework_json_api/filters.py +++ b/rest_framework_json_api/filters.py @@ -10,27 +10,35 @@ class JSONAPIOrderingFilter(OrderingFilter): if any sort field is invalid. If you prefer *not* to report 400 errors for invalid sort fields, just use OrderingFilter with `ordering_param='sort'` - TODO: Add sorting based upon relationships (sort=relname.fieldname) + Also applies DJA format_value() to convert (e.g. camelcase) to underscore. + (See JSON_API_FORMAT_FIELD_NAMES in docs/usage.md) """ ordering_param = 'sort' def remove_invalid_fields(self, queryset, fields, view, request): - """ - overrides remove_invalid_fields to raise a 400 exception instead of - silently removing them. set `ignore_bad_sort_fields = True` to not - do this validation. - """ valid_fields = [ item[0] for item in self.get_valid_fields(queryset, view, {'request': request}) ] bad_terms = [ term for term in fields - if format_value(term.lstrip('-'), "underscore") not in valid_fields + if format_value(term.replace(".", "__").lstrip('-'), "underscore") not in valid_fields ] if bad_terms: raise ValidationError('invalid sort parameter{}: {}'.format( ('s' if len(bad_terms) > 1 else ''), ','.join(bad_terms))) + # this looks like it duplicates code above, but we want the ValidationError to report + # the actual parameter supplied while we want the fields passed to the super() to + # be correctly rewritten. + # The leading `-` has to be stripped to prevent format_value from turning it into `_`. + underscore_fields = [] + for item in fields: + item_rewritten = item.replace(".", "__") + if item_rewritten.startswith('-'): + underscore_fields.append( + '-' + format_value(item_rewritten.lstrip('-'), "underscore")) + else: + underscore_fields.append(format_value(item_rewritten, "underscore")) return super(JSONAPIOrderingFilter, self).remove_invalid_fields( - queryset, fields, view, request) + queryset, underscore_fields, view, request)