Skip to content

JSONAPIOrderingFilter #459

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 6 commits into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
[unreleased]

* Add testing configuration to `REST_FRAMEWORK` configuration as described in [DRF](https://www.django-rest-framework.org/api-guide/testing/#configuration)
* Add sorting configuration to `REST_FRAMEWORK` as defined in [json api spec](http://jsonapi.org/format/#fetching-sorting)
* Add `HyperlinkedRelatedField` and `SerializerMethodHyperlinkedRelatedField`. See [usage docs](docs/usage.md#related-fields)
* Add related urls support. See [usage docs](docs/usage.md#related-urls)

* Add optional [jsonapi-style](http://jsonapi.org/format/) sort filter backend. See [usage docs](docs/usage.md#filter-backends)

v2.5.0 - Released July 11, 2018

Expand Down
3 changes: 1 addition & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,8 @@ override ``settings.REST_FRAMEWORK``
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.OrderingFilter',
'rest_framework_json_api.backends.JSONAPIOrderingFilter',
),
'ORDERING_PARAM': 'sort',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
Expand Down
37 changes: 33 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@

# Usage

The DJA package implements a custom renderer, parser, exception handler, and
The DJA package implements a custom renderer, parser, exception handler, query filter backends, and
pagination. To get started enable the pieces in `settings.py` that you want to use.

Many features of the JSON:API format standard have been implemented using Mixin classes in `serializers.py`.
Many features of the [JSON:API](http://jsonapi.org/format) format standard have been implemented using
Mixin classes in `serializers.py`.
The easiest way to make use of those features is to import ModelSerializer variants
from `rest_framework_json_api` instead of the usual `rest_framework`

Expand Down Expand Up @@ -32,9 +33,8 @@ REST_FRAMEWORK = {
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.OrderingFilter',
'rest_framework_json_api.backends.JSONAPIOrderingFilter',
),
'ORDERING_PARAM': 'sort',
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
),
Expand Down Expand Up @@ -90,6 +90,35 @@ class MyLimitPagination(JsonApiLimitOffsetPagination):
max_limit = None
```

### Filter Backends

_This is the first of several anticipated JSON:API-specific filter backends._

#### `JSONAPIOrderingFilter`
`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`,
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
{
"errors": [
{
"detail": "invalid sort parameters: abc,def",
"source": {
"pointer": "/data"
},
"status": "400"
}
]
}
```

If you want to silently ignore bad sort fields, just use `rest_framework.filters.OrderingFilter` and set
`ordering_param` to `sort`.


### Performance Testing

If you are trying to see if your viewsets are configured properly to optimize performance,
Expand Down
60 changes: 60 additions & 0 deletions example/fixtures/blogentry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
- model: example.blog
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's also do this in JSON as in drf_example

pk: 1
fields: {name: ANTB, tagline: ANTHROPOLOGY (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.blog
pk: 2
fields: {name: CLSB, tagline: CLASSICS (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.blog
pk: 3
fields: {name: AMSB, tagline: AMERICAN STUDIES (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.blog
pk: 4
fields: {name: CHMB, tagline: CHEMISTRY (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.blog
pk: 5
fields: {name: ARHB, tagline: ART HISTORY (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.blog
pk: 6
fields: {name: ITLB, tagline: ITALIAN (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.blog
pk: 7
fields: {name: BIOB, tagline: BIOLOGICAL SCIENCES (BARNARD), created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 1
fields: {blog: 1, headline: ANTH1009V, body_text: INTRO TO LANGUAGE & CULTURE, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 2
fields: {blog: 2, headline: CLCV2442V, body_text: EGYPT IN CLASSICAL WORLD-DISC, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 3
fields: {blog: 3, headline: AMST3704X, body_text: SENIOR RESEARCH ESSAY SEMINAR, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 4
fields: {blog: 1, headline: ANTH3976V, body_text: ANTHROPOLOGY OF SCIENCE, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 5
fields: {blog: 4, headline: CHEM3271X, body_text: INORGANIC CHEMISTRY, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 6
fields: {blog: 5, headline: AHIS3915X, body_text: ISLAM AND MEDIEVAL WEST, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 7
fields: {blog: 1, headline: ANTH3868X, body_text: ETHNOGRAPHIC FIELD RESEARCH IN
NYC, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 8
fields: {blog: 6, headline: CLIA3660V, body_text: MAFIA MOVIES, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 9
fields: {blog: 5, headline: AHIS3999X, body_text: INDEPENDENT RESEARCH, created_at: '2018-08-20', modified_at: '2018-08-20'}
- model: example.entry
pk: 10
fields: {blog: 7, headline: BIOL3594X, body_text: SENIOR THESIS SEMINAR, created_at: '2018-08-20', modified_at: '2018-08-20'}
# a null body_text:
- model: example.entry
pk: 11
fields: {blog: 7, headline: BIOL9999X, body_text: null, created_at: '2018-08-20', modified_at: '2018-08-20'}
# an empty body_text:
- model: example.entry
pk: 12
fields: {blog: 7, headline: BIOL0000X, body_text: "", created_at: '2018-08-20', modified_at: '2018-08-20'}
2 changes: 1 addition & 1 deletion example/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.OrderingFilter',
'rest_framework_json_api.backends.JSONAPIOrderingFilter',
),
'ORDERING_PARAM': 'sort',
'TEST_REQUEST_RENDERER_CLASSES': (
Expand Down
56 changes: 56 additions & 0 deletions example/tests/test_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import json

from rest_framework.test import APITestCase

from ..models import Blog, Entry

ENTRIES = "/nopage-entries"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouldn't build routes manually but instead use django reverse resolver

In this case this would be reverse('nopage-entry'). It will be then a full path of the testserver url.

This needs to be done in setUp or what I usually do in each single test as it is more implicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

django.urls.exceptions.NoReverseMatch: Reverse for 'nopage-entry' not found. 'nopage-entry' is not a valid view function or pattern name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe nopage-entry-list



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

def setUp(self):
self.entries = Entry.objects.all()
self.blogs = Blog.objects.all()

def test_sort(self):
"""
test sort
"""
response = self.client.get(ENTRIES + '?sort=headline')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameters can be passed on as data e.g. in this case data={'sort': 'headline'}. This way there is no need for string concats

self.assertEqual(response.status_code, 200,
msg=response.content.decode("utf-8"))
j = json.loads(response.content.decode("utf-8"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to get json result from response you should be able to simply do result = response.json() I would also prefer not to use single letter variables as python is not strongly typed so the name should tell what it which makes the code more readable (e.g. in this example result instead j or similar)

The same counts to the tests below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen cases where response.json() doesn't work. Taking a look if this is one of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this is not one of them.

headlines = [c['attributes']['headline'] for c in j['data']]
sorted_headlines = [c['attributes']['headline'] for c in j['data']]
sorted_headlines.sort()
self.assertEqual(headlines, sorted_headlines)

def test_sort_reverse(self):
"""
confirm switching the sort order actually works
"""
response = self.client.get(ENTRIES + '?sort=-headline')
self.assertEqual(response.status_code, 200,
msg=response.content.decode("utf-8"))
j = json.loads(response.content.decode("utf-8"))
headlines = [c['attributes']['headline'] for c in j['data']]
sorted_headlines = [c['attributes']['headline'] for c in j['data']]
sorted_headlines.sort()
self.assertNotEqual(headlines, sorted_headlines)

def test_sort_invalid(self):
"""
test sort of invalid field
"""
response = self.client.get(
ENTRIES + '?sort=nonesuch,headline,-not_a_field')
self.assertEqual(response.status_code, 400,
msg=response.content.decode("utf-8"))
j = json.loads(response.content.decode("utf-8"))
self.assertEqual(j['errors'][0]['detail'],
"invalid sort parameters: nonesuch,-not_a_field")
1 change: 1 addition & 0 deletions requirements-development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ Sphinx
sphinx_rtd_theme
tox
twine
PyYAML
36 changes: 36 additions & 0 deletions rest_framework_json_api/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter

from rest_framework_json_api.utils import format_value


class JSONAPIOrderingFilter(OrderingFilter):
"""
This implements http://jsonapi.org/format/#fetching-sorting and raises 400
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)
"""
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 bad_terms:
raise ValidationError('invalid sort parameter{}: {}'.format(
('s' if len(bad_terms) > 1 else ''), ','.join(bad_terms)))

return super(JSONAPIOrderingFilter, self).remove_invalid_fields(
queryset, fields, view, request)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ deps =
drf36: djangorestframework>=3.6.3,<3.7
drf37: djangorestframework>=3.7.0,<3.8
drf38: djangorestframework>=3.8.0,<3.9
PyYAML

setenv =
PYTHONPATH = {toxinidir}
Expand Down