Skip to content

Refactor backends to filters + bugfix #464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 1 addition & 2 deletions example/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
Expand Down
54 changes: 0 additions & 54 deletions example/tests/test_backends.py

This file was deleted.

105 changes: 105 additions & 0 deletions example/tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase

from ..models import Blog, Entry


class DJATestParameters(APITestCase):
"""
tests of JSON:API backends
"""
fixtures = ('blogentry',)

def setUp(self):
self.entries = Entry.objects.all()
self.blogs = Blog.objects.all()
self.url = reverse('nopage-entry-list')

def test_sort(self):
"""
test sort
"""
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.assertEqual(headlines, sorted_headlines)

def test_sort_reverse(self):
"""
confirm switching the sort order actually works
"""
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_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):
"""
test sort of invalid field
"""
response = self.client.get(self.url,
data={'sort': 'nonesuch,headline,-not_a_field'})
self.assertEqual(response.status_code, 400,
msg=response.content.decode("utf-8"))
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)
1 change: 1 addition & 0 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)