From 9fd97276a15c0c3daa9b76401d47a17de0ce35a4 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 20 Aug 2018 17:59:25 -0400 Subject: [PATCH 1/5] JSONAPIOrderingFilter --- CHANGELOG.md | 3 +- README.rst | 3 +- docs/usage.md | 37 ++++++++++++++++-- example/fixtures/blogentry.yaml | 60 +++++++++++++++++++++++++++++ example/settings/dev.py | 2 +- example/tests/test_backends.py | 55 ++++++++++++++++++++++++++ requirements-development.txt | 1 + rest_framework_json_api/backends.py | 35 +++++++++++++++++ tox.ini | 1 + 9 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 example/fixtures/blogentry.yaml create mode 100644 example/tests/test_backends.py create mode 100644 rest_framework_json_api/backends.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fc160067..c1365934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.rst b/README.rst index 46813188..6fa4aee5 100644 --- a/README.rst +++ b/README.rst @@ -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', ), diff --git a/docs/usage.md b/docs/usage.md index 25bb7310..75fbad7d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -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` @@ -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', ), @@ -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, diff --git a/example/fixtures/blogentry.yaml b/example/fixtures/blogentry.yaml new file mode 100644 index 00000000..a00034b9 --- /dev/null +++ b/example/fixtures/blogentry.yaml @@ -0,0 +1,60 @@ +- model: example.blog + 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'} diff --git a/example/settings/dev.py b/example/settings/dev.py index 5f938f78..e8ed4094 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -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': ( diff --git a/example/tests/test_backends.py b/example/tests/test_backends.py new file mode 100644 index 00000000..78f0c5e7 --- /dev/null +++ b/example/tests/test_backends.py @@ -0,0 +1,55 @@ +import json + +from rest_framework.test import APITestCase +from ..models import Blog, Entry + +ENTRIES = "/nopage-entries" + + +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') + 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.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") diff --git a/requirements-development.txt b/requirements-development.txt index f5c7cacb..52059e6a 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -14,3 +14,4 @@ Sphinx sphinx_rtd_theme tox twine +PyYAML diff --git a/rest_framework_json_api/backends.py b/rest_framework_json_api/backends.py new file mode 100644 index 00000000..3fd97407 --- /dev/null +++ b/rest_framework_json_api/backends.py @@ -0,0 +1,35 @@ +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) diff --git a/tox.ini b/tox.ini index d5cca046..56c9c814 100644 --- a/tox.ini +++ b/tox.ini @@ -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} From a38704dde403a680cb13f0b0b6328cef09d799c1 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 20 Aug 2018 18:12:11 -0400 Subject: [PATCH 2/5] isort, again. --- rest_framework_json_api/backends.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework_json_api/backends.py b/rest_framework_json_api/backends.py index 3fd97407..e6fda16b 100644 --- a/rest_framework_json_api/backends.py +++ b/rest_framework_json_api/backends.py @@ -1,5 +1,6 @@ from rest_framework.exceptions import ValidationError from rest_framework.filters import OrderingFilter + from rest_framework_json_api.utils import format_value From 8380dd27b5d8f3ac2deb5645b9157c9f35f78797 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 20 Aug 2018 18:13:06 -0400 Subject: [PATCH 3/5] isort, again again --- example/tests/test_backends.py | 1 + 1 file changed, 1 insertion(+) diff --git a/example/tests/test_backends.py b/example/tests/test_backends.py index 78f0c5e7..b8701c4c 100644 --- a/example/tests/test_backends.py +++ b/example/tests/test_backends.py @@ -1,6 +1,7 @@ import json from rest_framework.test import APITestCase + from ..models import Blog, Entry ENTRIES = "/nopage-entries" From 2456cb810c3a0444476bc997eac118e18c0329e1 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 21 Aug 2018 17:42:34 -0400 Subject: [PATCH 4/5] @sliverc review requested changes - use json instead of yaml fixture to remove dependency on PyYAML - use reverse() - request.data dict instead of string concat to the url - response.json() instead of json.loads(response.content.decode(...) --- example/fixtures/blogentry.json | 280 ++++++++++++++++++++++++++++++++ example/fixtures/blogentry.yaml | 60 ------- example/tests/test_backends.py | 29 ++-- requirements-development.txt | 2 +- tox.ini | 1 - 5 files changed, 295 insertions(+), 77 deletions(-) create mode 100644 example/fixtures/blogentry.json delete mode 100644 example/fixtures/blogentry.yaml diff --git a/example/fixtures/blogentry.json b/example/fixtures/blogentry.json new file mode 100644 index 00000000..15ceded9 --- /dev/null +++ b/example/fixtures/blogentry.json @@ -0,0 +1,280 @@ +[ +{ + "model": "example.blog", + "pk": 1, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "ANTB", + "tagline": "ANTHROPOLOGY (BARNARD)" + } +}, +{ + "model": "example.blog", + "pk": 2, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "CLSB", + "tagline": "CLASSICS (BARNARD)" + } +}, +{ + "model": "example.blog", + "pk": 3, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "AMSB", + "tagline": "AMERICAN STUDIES (BARNARD)" + } +}, +{ + "model": "example.blog", + "pk": 4, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "CHMB", + "tagline": "CHEMISTRY (BARNARD)" + } +}, +{ + "model": "example.blog", + "pk": 5, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "ARHB", + "tagline": "ART HISTORY (BARNARD)" + } +}, +{ + "model": "example.blog", + "pk": 6, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "ITLB", + "tagline": "ITALIAN (BARNARD)" + } +}, +{ + "model": "example.blog", + "pk": 7, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "name": "BIOB", + "tagline": "BIOLOGICAL SCIENCES (BARNARD)" + } +}, +{ + "model": "example.entry", + "pk": 1, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 1, + "headline": "ANTH1009V", + "body_text": "INTRO TO LANGUAGE & CULTURE", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [ + 1 + ] + } +}, +{ + "model": "example.entry", + "pk": 2, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 2, + "headline": "CLCV2442V", + "body_text": "EGYPT IN CLASSICAL WORLD-DISC", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [ + 2 + ] + } +}, +{ + "model": "example.entry", + "pk": 3, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 3, + "headline": "AMST3704X", + "body_text": "SENIOR RESEARCH ESSAY SEMINAR", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 4, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 1, + "headline": "ANTH3976V", + "body_text": "ANTHROPOLOGY OF SCIENCE", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 5, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 4, + "headline": "CHEM3271X", + "body_text": "INORGANIC CHEMISTRY", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 6, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 5, + "headline": "AHIS3915X", + "body_text": "ISLAM AND MEDIEVAL WEST", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 7, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 1, + "headline": "ANTH3868X", + "body_text": "ETHNOGRAPHIC FIELD RESEARCH IN NYC", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 8, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 6, + "headline": "CLIA3660V", + "body_text": "MAFIA MOVIES", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 9, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 5, + "headline": "AHIS3999X", + "body_text": "INDEPENDENT RESEARCH", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 10, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 7, + "headline": "BIOL3594X", + "body_text": "SENIOR THESIS SEMINAR", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 11, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 7, + "headline": "BIOL9999X", + "body_text": null, + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +}, +{ + "model": "example.entry", + "pk": 12, + "fields": { + "created_at": "2018-08-20T00:00:00", + "modified_at": "2018-08-20T00:00:00", + "blog": 7, + "headline": "BIOL0000X", + "body_text": "", + "pub_date": null, + "mod_date": null, + "n_comments": 0, + "n_pingbacks": 0, + "rating": 0, + "authors": [] + } +} +] diff --git a/example/fixtures/blogentry.yaml b/example/fixtures/blogentry.yaml deleted file mode 100644 index a00034b9..00000000 --- a/example/fixtures/blogentry.yaml +++ /dev/null @@ -1,60 +0,0 @@ -- model: example.blog - 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'} diff --git a/example/tests/test_backends.py b/example/tests/test_backends.py index b8701c4c..5af473cc 100644 --- a/example/tests/test_backends.py +++ b/example/tests/test_backends.py @@ -1,12 +1,10 @@ import json from rest_framework.test import APITestCase +from rest_framework.reverse import reverse from ..models import Blog, Entry -ENTRIES = "/nopage-entries" - - class DJATestParameters(APITestCase): """ tests of JSON:API backends @@ -16,17 +14,18 @@ class DJATestParameters(APITestCase): 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(ENTRIES + '?sort=headline') + response = self.client.get(self.url, data = {'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']] + 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() self.assertEqual(headlines, sorted_headlines) @@ -34,12 +33,12 @@ def test_sort_reverse(self): """ confirm switching the sort order actually works """ - response = self.client.get(ENTRIES + '?sort=-headline') + response = self.client.get(self.url, data = {'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']] + 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() self.assertNotEqual(headlines, sorted_headlines) @@ -47,10 +46,10 @@ def test_sort_invalid(self): """ test sort of invalid field """ - response = self.client.get( - ENTRIES + '?sort=nonesuch,headline,-not_a_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")) - j = json.loads(response.content.decode("utf-8")) - self.assertEqual(j['errors'][0]['detail'], + dja_response = response.json() + self.assertEqual(dja_response['errors'][0]['detail'], "invalid sort parameters: nonesuch,-not_a_field") diff --git a/requirements-development.txt b/requirements-development.txt index 52059e6a..e2e8aae3 100644 --- a/requirements-development.txt +++ b/requirements-development.txt @@ -14,4 +14,4 @@ Sphinx sphinx_rtd_theme tox twine -PyYAML + diff --git a/tox.ini b/tox.ini index 56c9c814..d5cca046 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ 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} From 779aed9413800c3efd76dc7d31fd098051db1bee Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 21 Aug 2018 18:02:08 -0400 Subject: [PATCH 5/5] chagrined to once again being caught by flake8-isort --- example/tests/test_backends.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/example/tests/test_backends.py b/example/tests/test_backends.py index 5af473cc..0721f780 100644 --- a/example/tests/test_backends.py +++ b/example/tests/test_backends.py @@ -1,10 +1,9 @@ -import json - -from rest_framework.test import APITestCase 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 @@ -20,7 +19,7 @@ def test_sort(self): """ test sort """ - response = self.client.get(self.url, data = {'sort': 'headline'}) + 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() @@ -33,7 +32,7 @@ def test_sort_reverse(self): """ confirm switching the sort order actually works """ - response = self.client.get(self.url, data = {'sort': '-headline'}) + 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() @@ -47,7 +46,7 @@ def test_sort_invalid(self): test sort of invalid field """ response = self.client.get(self.url, - data = {'sort': 'nonesuch,headline,-not_a_field'}) + data={'sort': 'nonesuch,headline,-not_a_field'}) self.assertEqual(response.status_code, 400, msg=response.content.decode("utf-8")) dja_response = response.json()