From 1654ae1f407dcf44a66a11fc19d68a9d8e2e5de3 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 18 Nov 2024 08:27:37 -0800 Subject: [PATCH 01/15] Scheduled biweekly dependency update for week 46 (#1263) * Update flake8-bugbear from 24.8.19 to 24.10.31 * Update sphinx_rtd_theme from 3.0.1 to 3.0.2 * Update faker from 30.6.0 to 33.0.0 * Update pytest-cov from 5.0.0 to 6.0.0 * Revert pytest-cov currently still need to support 3.8 --------- Co-authored-by: Oliver Sauder --- requirements/requirements-codestyle.txt | 2 +- requirements/requirements-documentation.txt | 2 +- requirements/requirements-testing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt index f5e5e92c..83eab224 100644 --- a/requirements/requirements-codestyle.txt +++ b/requirements/requirements-codestyle.txt @@ -1,5 +1,5 @@ black==24.10.0 flake8==7.1.1 -flake8-bugbear==24.8.19 +flake8-bugbear==24.10.31 flake8-isort==6.1.1 isort==5.13.2 diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index aa120a8e..76f3bf9a 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,3 +1,3 @@ recommonmark==0.7.1 Sphinx==8.1.3 -sphinx_rtd_theme==3.0.1 +sphinx_rtd_theme==3.0.2 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index b56d8185..63ed4a7c 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,5 +1,5 @@ factory-boy==3.3.1 -Faker==30.6.0 +Faker==33.0.0 pytest==8.3.3 pytest-cov==5.0.0 pytest-django==4.9.0 From 1b5fb9c8846bff9e3b6ba56d01a7cb033cd0a281 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sun, 12 Jan 2025 23:57:26 -0500 Subject: [PATCH 02/15] Removed support for Python 3.8 (#1266) --- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 9 +++++++++ README.rst | 2 +- docs/getting-started.md | 2 +- setup.py | 3 +-- tox.ini | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca736986..0852bba1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PYTHON: ${{ matrix.python-version }} steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 07cd7d8f..f1825191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note that in line with [Django REST framework policy](https://www.django-rest-framework.org/topics/release-notes/), any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. +## [unreleased] + +### Removed + +* Removed support for Python 3.8. + + ## [7.1.0] - 2024-10-25 +This is the last release supporting Python 3.8. + ### Fixed * Handled zero as a valid ID for resource (regression since 6.1.0) diff --git a/README.rst b/README.rst index 0c9b842f..bf5daa73 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,7 @@ As a Django REST framework JSON:API (short DJA) we are trying to address followi Requirements ------------ -1. Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13) +1. Python (3.9, 3.10, 3.11, 3.12, 3.13) 2. Django (4.2, 5.0, 5.1) 3. Django REST framework (3.14, 3.15) diff --git a/docs/getting-started.md b/docs/getting-started.md index a7de353a..1799337b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -51,7 +51,7 @@ like the following: ## Requirements -1. Python (3.8, 3.9, 3.10, 3.11, 3.12, 3.13) +1. Python (3.9, 3.10, 3.11, 3.12, 3.13) 2. Django (4.2, 5.0, 5.1) 3. Django REST framework (3.14, 3.15) diff --git a/setup.py b/setup.py index 652ab85b..779d00c1 100755 --- a/setup.py +++ b/setup.py @@ -86,7 +86,6 @@ def get_package_data(package): "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -116,6 +115,6 @@ def get_package_data(package): "openapi": ["pyyaml>=5.4", "uritemplate>=3.0.1"], }, setup_requires=wheel, - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ) diff --git a/tox.ini b/tox.ini index a2accaad..504a9d2e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{38,39,310,311,312}-django42-drf{314,315,master}, + py{39,310,311,312}-django42-drf{314,315,master}, py{310,311,312}-django{50,51}-drf{314,315,master}, py313-django51-drf{master}, black, From d297405f79fab39b379e69da8fc93986c36105ed Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 20 Jan 2025 08:32:01 -0800 Subject: [PATCH 03/15] Scheduled biweekly dependency update for week 03 (#1268) * Update flake8-bugbear from 24.10.31 to 24.12.12 * Update twine from 5.1.1 to 6.0.1 * Update faker from 33.0.0 to 33.3.1 * Update pytest from 8.3.3 to 8.3.4 * Update pytest-cov from 5.0.0 to 6.0.0 * Update syrupy from 4.7.2 to 4.8.1 --- requirements/requirements-codestyle.txt | 2 +- requirements/requirements-packaging.txt | 2 +- requirements/requirements-testing.txt | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt index 83eab224..025c050b 100644 --- a/requirements/requirements-codestyle.txt +++ b/requirements/requirements-codestyle.txt @@ -1,5 +1,5 @@ black==24.10.0 flake8==7.1.1 -flake8-bugbear==24.10.31 +flake8-bugbear==24.12.12 flake8-isort==6.1.1 isort==5.13.2 diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index e957043a..09ce4dfb 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -1 +1 @@ -twine==5.1.1 +twine==6.0.1 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 63ed4a7c..e2f9b287 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,7 +1,7 @@ factory-boy==3.3.1 -Faker==33.0.0 -pytest==8.3.3 -pytest-cov==5.0.0 +Faker==33.3.1 +pytest==8.3.4 +pytest-cov==6.0.0 pytest-django==4.9.0 pytest-factoryboy==2.7.0 -syrupy==4.7.2 +syrupy==4.8.1 From abfda8fd48cb6758dcda5f60f95db7e09cb87ee4 Mon Sep 17 00:00:00 2001 From: Vitaliy Date: Thu, 23 Jan 2025 21:43:50 +0500 Subject: [PATCH 04/15] Update documentation to include --pythonpath to set up the example app (#1270) Add --pythonpath to instructions for setting up example app --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index bf5daa73..83d5d7ab 100644 --- a/README.rst +++ b/README.rst @@ -149,9 +149,9 @@ installed and activated: $ git clone https://github.com/django-json-api/django-rest-framework-json-api.git $ cd django-rest-framework-json-api $ pip install -Ur requirements.txt - $ django-admin migrate --settings=example.settings - $ django-admin loaddata drf_example --settings=example.settings - $ django-admin runserver --settings=example.settings + $ django-admin migrate --settings=example.settings --pythonpath . + $ django-admin loaddata drf_example --settings=example.settings --pythonpath . + $ django-admin runserver --settings=example.settings --pythonpath . Browse to From 0476cbc9f5f5bee226481506340a5f5c06479e24 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 3 Feb 2025 10:13:47 -0800 Subject: [PATCH 05/15] Scheduled biweekly dependency update for week 05 (#1271) * Update black from 24.10.0 to 25.1.0 * Update flake8-isort from 6.1.1 to 6.1.2 * Update isort from 5.13.2 to 6.0.0 * Update twine from 6.0.1 to 6.1.0 * Update factory-boy from 3.3.1 to 3.3.3 * Update faker from 33.3.1 to 35.2.0 --- requirements/requirements-codestyle.txt | 6 +++--- requirements/requirements-packaging.txt | 2 +- requirements/requirements-testing.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt index 025c050b..1799b010 100644 --- a/requirements/requirements-codestyle.txt +++ b/requirements/requirements-codestyle.txt @@ -1,5 +1,5 @@ -black==24.10.0 +black==25.1.0 flake8==7.1.1 flake8-bugbear==24.12.12 -flake8-isort==6.1.1 -isort==5.13.2 +flake8-isort==6.1.2 +isort==6.0.0 diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index 09ce4dfb..54882779 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -1 +1 @@ -twine==6.0.1 +twine==6.1.0 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index e2f9b287..0f58e66a 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,5 +1,5 @@ -factory-boy==3.3.1 -Faker==33.3.1 +factory-boy==3.3.3 +Faker==35.2.0 pytest==8.3.4 pytest-cov==6.0.0 pytest-django==4.9.0 From eac2f9f766aa97c6ff27c56b7bdae831366c10d7 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Wed, 16 Apr 2025 20:44:32 +0400 Subject: [PATCH 06/15] Added support for Django REST framework 3.16 (#1279) --- CHANGELOG.md | 9 +++++++-- README.rst | 2 +- docs/getting-started.md | 2 +- setup.py | 2 +- tox.ini | 6 +++--- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1825191..7a6ae7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note that in line with [Django REST framework policy](https://www.django-rest-framework.org/topics/release-notes/), any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. -## [unreleased] +## [Unreleased] + +### Added + +* Added support for Django REST framework 3.16. ### Removed * Removed support for Python 3.8. +* Removed support for Django REST framework 3.14. ## [7.1.0] - 2024-10-25 -This is the last release supporting Python 3.8. +This is the last release supporting Python 3.8 and Django REST framework 3.14. ### Fixed diff --git a/README.rst b/README.rst index 83d5d7ab..21b2e3f0 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ Requirements 1. Python (3.9, 3.10, 3.11, 3.12, 3.13) 2. Django (4.2, 5.0, 5.1) -3. Django REST framework (3.14, 3.15) +3. Django REST framework (3.15, 3.16) We **highly** recommend and only officially support the latest patch release of each Python, Django and REST framework series. diff --git a/docs/getting-started.md b/docs/getting-started.md index 1799337b..461419c7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -53,7 +53,7 @@ like the following: 1. Python (3.9, 3.10, 3.11, 3.12, 3.13) 2. Django (4.2, 5.0, 5.1) -3. Django REST framework (3.14, 3.15) +3. Django REST framework (3.15, 3.16) We **highly** recommend and only officially support the latest patch release of each Python, Django and REST framework series. diff --git a/setup.py b/setup.py index 779d00c1..de61b0d1 100755 --- a/setup.py +++ b/setup.py @@ -106,7 +106,7 @@ def get_package_data(package): }, install_requires=[ "inflection>=0.5.0", - "djangorestframework>=3.14", + "djangorestframework>=3.15", "django>=4.2", ], extras_require={ diff --git a/tox.ini b/tox.ini index 504a9d2e..45a4e20b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - py{39,310,311,312}-django42-drf{314,315,master}, - py{310,311,312}-django{50,51}-drf{314,315,master}, + py{39,310,311,312}-django42-drf{315,316,master}, + py{310,311,312}-django{50,51}-drf{315,316,master}, py313-django51-drf{master}, black, docs, @@ -12,8 +12,8 @@ deps = django42: Django>=4.2,<4.3 django50: Django>=5.0,<5.1 django51: Django>=5.1,<5.2 - drf314: djangorestframework>=3.14,<3.15 drf315: djangorestframework>=3.15,<3.16 + drf316: djangorestframework>=3.16,<3.17 drfmaster: https://github.com/encode/django-rest-framework/archive/master.zip -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 76b4c41944bc142ba4241d0fc258ce8070444b41 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 21 Apr 2025 09:32:44 -0700 Subject: [PATCH 07/15] Scheduled biweekly dependency update for week 16 (#1280) * Update flake8 from 7.1.1 to 7.2.0 * Update isort from 6.0.0 to 6.0.1 * Update faker from 35.2.0 to 37.1.0 * Update pytest from 8.3.4 to 8.3.5 * Update pytest-cov from 6.0.0 to 6.1.1 * Update pytest-django from 4.9.0 to 4.11.1 * Update syrupy from 4.8.1 to 4.9.1 --- requirements/requirements-codestyle.txt | 4 ++-- requirements/requirements-testing.txt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt index 1799b010..1d4184ad 100644 --- a/requirements/requirements-codestyle.txt +++ b/requirements/requirements-codestyle.txt @@ -1,5 +1,5 @@ black==25.1.0 -flake8==7.1.1 +flake8==7.2.0 flake8-bugbear==24.12.12 flake8-isort==6.1.2 -isort==6.0.0 +isort==6.0.1 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 0f58e66a..35caa0a9 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,7 +1,7 @@ factory-boy==3.3.3 -Faker==35.2.0 -pytest==8.3.4 -pytest-cov==6.0.0 -pytest-django==4.9.0 +Faker==37.1.0 +pytest==8.3.5 +pytest-cov==6.1.1 +pytest-django==4.11.1 pytest-factoryboy==2.7.0 -syrupy==4.8.1 +syrupy==4.9.1 From 0394cf91b87f6d488be3da8ef585df325786c73a Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Mon, 21 Apr 2025 21:21:50 +0400 Subject: [PATCH 08/15] Updated Django support (#1281) * Updated Django support Added support for Django 5.2 Removed outdated Django 5.0 * Python 3.13 should not run DRF 3.15 tests * Only DRF 3.16 supports Python 3.13 --- CHANGELOG.md | 4 +++- README.rst | 2 +- docs/getting-started.md | 2 +- tox.ini | 6 +++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6ae7e9..3ec7ff7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,16 +13,18 @@ any parts of the framework not mentioned in the documentation should generally b ### Added * Added support for Django REST framework 3.16. +* Added support for Django 5.2. ### Removed * Removed support for Python 3.8. * Removed support for Django REST framework 3.14. +* Removed support for Django 5.0. ## [7.1.0] - 2024-10-25 -This is the last release supporting Python 3.8 and Django REST framework 3.14. +This is the last release supporting Python 3.8, Django 5.0 and Django REST framework 3.14. ### Fixed diff --git a/README.rst b/README.rst index 21b2e3f0..c0e95a19 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,7 @@ Requirements ------------ 1. Python (3.9, 3.10, 3.11, 3.12, 3.13) -2. Django (4.2, 5.0, 5.1) +2. Django (4.2, 5.1, 5.2) 3. Django REST framework (3.15, 3.16) We **highly** recommend and only officially support the latest patch release of each Python, Django and REST framework series. diff --git a/docs/getting-started.md b/docs/getting-started.md index 461419c7..4052450b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -52,7 +52,7 @@ like the following: ## Requirements 1. Python (3.9, 3.10, 3.11, 3.12, 3.13) -2. Django (4.2, 5.0, 5.1) +2. Django (4.2, 5.1, 5.2) 3. Django REST framework (3.15, 3.16) We **highly** recommend and only officially support the latest patch release of each Python, Django and REST framework series. diff --git a/tox.ini b/tox.ini index 45a4e20b..8dd0e759 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = py{39,310,311,312}-django42-drf{315,316,master}, - py{310,311,312}-django{50,51}-drf{315,316,master}, - py313-django51-drf{master}, + py{310,311,312}-django{51,52}-drf{315,316,master}, + py{313}-django{51,52}-drf{316,master}, black, docs, lint @@ -10,8 +10,8 @@ envlist = [testenv] deps = django42: Django>=4.2,<4.3 - django50: Django>=5.0,<5.1 django51: Django>=5.1,<5.2 + django52: Django>=5.2,<5.3 drf315: djangorestframework>=3.15,<3.16 drf316: djangorestframework>=3.16,<3.17 drfmaster: https://github.com/encode/django-rest-framework/archive/master.zip From 87108d4d6472b0c76e8c8fff53a6f08a026dd319 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Mon, 14 Jul 2025 20:38:45 +0700 Subject: [PATCH 09/15] Ensured that `include` param is properly underscored (#1283) nsured that interpreting `include` query parameter is done in internal Python naming. --- CHANGELOG.md | 5 +++++ rest_framework_json_api/renderers.py | 4 ---- rest_framework_json_api/serializers.py | 3 +-- rest_framework_json_api/utils.py | 12 ++++++++++-- tests/conftest.py | 7 ++++++- tests/test_utils.py | 21 +++++++++++++++++++++ 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec7ff7d..eae89c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ any parts of the framework not mentioned in the documentation should generally b * Added support for Django REST framework 3.16. * Added support for Django 5.2. +### Fixed + +* Ensured that interpreting `include` query parameter is done in internal Python naming. + This adds full support for using multipart field names for includes while configuring `JSON_API_FORMAT_FIELD_NAMES`. + ### Removed * Removed support for Python 3.8. diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 8c19934f..b670338f 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -6,7 +6,6 @@ from collections import defaultdict from collections.abc import Iterable -import inflection from django.db.models import Manager from django.template import loader from django.utils.encoding import force_str @@ -277,9 +276,6 @@ def extract_included( current_serializer, "included_serializers", dict() ) included_resources = copy.copy(included_resources) - included_resources = [ - inflection.underscore(value) for value in included_resources - ] for field_name, field in iter(fields.items()): # Skip URL field diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index d59dbd88..75764a5d 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,6 +1,5 @@ from collections.abc import Mapping -import inflection from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet from django.utils.module_loading import import_string as import_class_from_dotted_path @@ -129,7 +128,7 @@ def validate_path(serializer_class, field_path, path): serializers = getattr(serializer_class, "included_serializers", None) if serializers is None: raise ParseError("This endpoint does not support the include parameter") - this_field_name = inflection.underscore(field_path[0]) + this_field_name = field_path[0] this_included_serializer = serializers.get(this_field_name) if this_included_serializer is None: raise ParseError( diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 805f5f09..2dd79677 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -316,10 +316,18 @@ def get_resource_id(resource_instance, resource): def get_included_resources(request, serializer=None): - """Build a list of included resources.""" + """ + Build a list of included resources. + + This method ensures that returned includes are in Python internally used + format. + """ include_resources_param = request.query_params.get("include") if request else None if include_resources_param: - return include_resources_param.split(",") + return [ + undo_format_field_name(include) + for include in include_resources_param.split(",") + ] else: return get_default_included_resources_from_serializer(serializer) diff --git a/tests/conftest.py b/tests/conftest.py index 865244e0..77b3676b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ import pytest -from rest_framework.test import APIClient +from rest_framework.test import APIClient, APIRequestFactory from tests.models import ( BasicModel, @@ -98,3 +98,8 @@ def nested_related_source( @pytest.fixture def client(): return APIClient() + + +@pytest.fixture +def rf(): + return APIRequestFactory() diff --git a/tests/test_utils.py b/tests/test_utils.py index a3beb12e..08e36b6a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,6 +2,7 @@ from rest_framework import status from rest_framework.fields import Field from rest_framework.generics import GenericAPIView +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -13,6 +14,7 @@ format_link_segment, format_resource_type, format_value, + get_included_resources, get_related_resource_type, get_resource_id, get_resource_name, @@ -456,3 +458,22 @@ def test_get_resource_id(resource_instance, resource, expected): ) def test_format_error_object(message, pointer, response, result): assert result == format_error_object(message, pointer, response) + + +@pytest.mark.parametrize( + "format_type,include_param,expected_includes", + [ + ("dasherize", "author-bio", ["author_bio"]), + ("dasherize", "author-bio,author-type", ["author_bio", "author_type"]), + ("dasherize", "author-bio.author-type", ["author_bio.author_type"]), + ("camelize", "authorBio", ["author_bio"]), + ], +) +def test_get_included_resources( + rf, include_param, expected_includes, format_type, settings +): + settings.JSON_API_FORMAT_FIELD_NAMES = format_type + + request = Request(rf.get("/test/", {"include": include_param})) + includes = get_included_resources(request) + assert includes == expected_includes From 2dc557144954c99f7a97e940b253358218917502 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Fri, 18 Jul 2025 17:35:40 +0700 Subject: [PATCH 10/15] Ensured that sparse fieldset support formatted field names (#1286) --- CHANGELOG.md | 1 + rest_framework_json_api/serializers.py | 6 +++++- tests/test_serializers.py | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eae89c70..9502f476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ any parts of the framework not mentioned in the documentation should generally b * Ensured that interpreting `include` query parameter is done in internal Python naming. This adds full support for using multipart field names for includes while configuring `JSON_API_FORMAT_FIELD_NAMES`. +* Ensured that sparse fieldset fully supports `JSON_API_FORMAT_FIELD_NAMES`. ### Removed diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 75764a5d..26f6b02e 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -26,6 +26,7 @@ get_resource_type_from_instance, get_resource_type_from_model, get_resource_type_from_serializer, + undo_format_field_name, ) @@ -89,7 +90,10 @@ def _readable_fields(self): sparse_fieldset_query_param ) if sparse_fieldset_value is not None: - sparse_fields = sparse_fieldset_value.split(",") + sparse_fields = [ + undo_format_field_name(sparse_field) + for sparse_field in sparse_fieldset_value.split(",") + ] return ( field for field in readable_fields diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 9d4200a3..98cb2850 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -1,5 +1,6 @@ import pytest from django.db import models +from rest_framework.request import Request from rest_framework.utils import model_meta from rest_framework_json_api import serializers @@ -84,3 +85,22 @@ class Meta: "verified", "uuid", ] + + +def test_readable_fields_with_sparse_fields(client, rf, settings): + class TestSerializer(serializers.Serializer): + name = serializers.CharField() + value = serializers.CharField() + multi_part_name = serializers.CharField() + + class Meta: + resource_name = "test" + + settings.JSON_API_FORMAT_FIELD_NAMES = "camelize" + request = Request(rf.get("/test/", {"fields[test]": "value,multiPartName"})) + context = {"request": request} + serializer = TestSerializer(context=context) + assert [field.field_name for field in serializer._readable_fields] == [ + "value", + "multi_part_name", + ] From 7bb4c086d8e682e771309bc11e01b48713737ad6 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Fri, 18 Jul 2025 20:08:54 +0700 Subject: [PATCH 11/15] Removed obsolete example requirements.txt file (#1287) --- example/requirements.txt | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 example/requirements.txt diff --git a/example/requirements.txt b/example/requirements.txt deleted file mode 100644 index 2f4213ee..00000000 --- a/example/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Requirements specifically for the example app -Django>=1.11 -django-debug-toolbar -django-polymorphic>=2.0 -djangorestframework -inflection -pluggy -py -pyparsing -pytz -sqlparse -django-filter>=2.0 From e9a7f5fbfda5860ffe9a69a7b494ca00cc2a1ec3 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Mon, 21 Jul 2025 21:47:16 +0700 Subject: [PATCH 12/15] Removed built-in OpenAPI support (#1288) * Removed built-in OpenAPI support * Removed django filter schema methods --- CHANGELOG.md | 2 +- README.rst | 3 - docs/getting-started.md | 3 - docs/usage.md | 133 -- example/settings/dev.py | 1 - example/templates/swagger-ui.html | 28 - example/tests/__snapshots__/test_openapi.ambr | 1414 ----------------- example/tests/test_openapi.py | 230 --- .../tests/unit/test_filter_schema_params.py | 107 -- example/urls.py | 22 - requirements/requirements-optionals.txt | 2 - .../django_filters/backends.py | 17 +- rest_framework_json_api/schemas/openapi.py | 905 ----------- setup.cfg | 3 - setup.py | 1 - tests/schemas/test_openapi.py | 11 - 16 files changed, 2 insertions(+), 2880 deletions(-) delete mode 100644 example/templates/swagger-ui.html delete mode 100644 example/tests/__snapshots__/test_openapi.ambr delete mode 100644 example/tests/test_openapi.py delete mode 100644 example/tests/unit/test_filter_schema_params.py delete mode 100644 rest_framework_json_api/schemas/openapi.py delete mode 100644 tests/schemas/test_openapi.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9502f476..63865fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ any parts of the framework not mentioned in the documentation should generally b * Removed support for Python 3.8. * Removed support for Django REST framework 3.14. * Removed support for Django 5.0. - +* Removed built-in support for generating OpenAPI schema. Use [drf-spectacular-json-api](https://github.com/jokiefer/drf-spectacular-json-api/) instead. ## [7.1.0] - 2024-10-25 diff --git a/README.rst b/README.rst index c0e95a19..05c01c04 100644 --- a/README.rst +++ b/README.rst @@ -114,7 +114,6 @@ Install using ``pip``... $ # for optional package integrations $ pip install djangorestframework-jsonapi['django-filter'] $ pip install djangorestframework-jsonapi['django-polymorphic'] - $ pip install djangorestframework-jsonapi['openapi'] or from source... @@ -156,8 +155,6 @@ installed and activated: Browse to * http://localhost:8000 for the list of available collections (in a non-JSON:API format!), -* http://localhost:8000/swagger-ui/ for a Swagger user interface to the dynamic schema view, or -* http://localhost:8000/openapi for the schema view's OpenAPI specification document. ----- diff --git a/docs/getting-started.md b/docs/getting-started.md index 4052450b..81040e8e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -69,7 +69,6 @@ Install using `pip`... # for optional package integrations pip install djangorestframework-jsonapi['django-filter'] pip install djangorestframework-jsonapi['django-polymorphic'] - pip install djangorestframework-jsonapi['openapi'] or from source... @@ -100,8 +99,6 @@ and add `rest_framework_json_api` to your `INSTALLED_APPS` setting below `rest_f Browse to * [http://localhost:8000](http://localhost:8000) for the list of available collections (in a non-JSON:API format!), -* [http://localhost:8000/swagger-ui/](http://localhost:8000/swagger-ui/) for a Swagger user interface to the dynamic schema view, or -* [http://localhost:8000/openapi](http://localhost:8000/openapi) for the schema view's OpenAPI specification document. ## Running Tests diff --git a/docs/usage.md b/docs/usage.md index 1a2cb195..00c12ee8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1054,139 +1054,6 @@ The `prefetch_related` case will issue 4 queries, but they will be small and fas ### Errors --> -## Generating an OpenAPI Specification (OAS) 3.0 schema document - -DRF has a [OAS schema functionality](https://www.django-rest-framework.org/api-guide/schemas/) to generate an -[OAS 3.0 schema](https://www.openapis.org/) as a YAML or JSON file. - -DJA extends DRF's schema support to generate an OAS schema in the JSON:API format. - ---- - -**Deprecation notice:** - -REST framework's built-in support for generating OpenAPI schemas is -**deprecated** in favor of 3rd party packages that can provide this -functionality instead. Therefore we have also deprecated the schema support in -Django REST framework JSON:API. The built-in support will be retired over the -next releases. - -As a full-fledged replacement, we recommend the [drf-spectacular-json-api] package. - ---- - -### AutoSchema Settings - -In order to produce an OAS schema that properly represents the JSON:API structure -you have to either add a `schema` attribute to each view class or set the `REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS']` -to DJA's version of AutoSchema. - -#### View-based - -```python -from rest_framework_json_api.schemas.openapi import AutoSchema - -class MyViewset(ModelViewSet): - schema = AutoSchema - ... -``` - -#### Default schema class - -```python -REST_FRAMEWORK = { - # ... - 'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema', -} -``` - -### Adding additional OAS schema content - -You can extend the OAS schema document by subclassing -[`SchemaGenerator`](https://www.django-rest-framework.org/api-guide/schemas/#schemagenerator) -and extending `get_schema`. - - -Here's an example that adds OAS `info` and `servers` objects. - -```python -from rest_framework_json_api.schemas.openapi import SchemaGenerator as JSONAPISchemaGenerator - - -class MySchemaGenerator(JSONAPISchemaGenerator): - """ - Describe my OAS schema info in detail (overriding what DRF put in) and list the servers where it can be found. - """ - def get_schema(self, request, public): - schema = super().get_schema(request, public) - schema['info'] = { - 'version': '1.0', - 'title': 'my demo API', - 'description': 'A demonstration of [OAS 3.0](https://www.openapis.org)', - 'contact': { - 'name': 'my name' - }, - 'license': { - 'name': 'BSD 2 clause', - 'url': 'https://github.com/django-json-api/django-rest-framework-json-api/blob/main/LICENSE', - } - } - schema['servers'] = [ - {'url': 'http://localhost/v1', 'description': 'local docker'}, - {'url': 'http://localhost:8000/v1', 'description': 'local dev'}, - {'url': 'https://api.example.com/v1', 'description': 'demo server'}, - {'url': '{serverURL}', 'description': 'provide your server URL', - 'variables': {'serverURL': {'default': 'http://localhost:8000/v1'}}} - ] - return schema -``` - -### Generate a Static Schema on Command Line - -See [DRF documentation for generateschema](https://www.django-rest-framework.org/api-guide/schemas/#generating-a-static-schema-with-the-generateschema-management-command) -To generate a static OAS schema document, using the `generateschema` management command, you **must override DRF's default** `generator_class` with the DJA-specific version: - -```text -$ ./manage.py generateschema --generator_class rest_framework_json_api.schemas.openapi.SchemaGenerator -``` - -You can then use any number of OAS tools such as -[swagger-ui-watcher](https://www.npmjs.com/package/swagger-ui-watcher) -to render the schema: -```text -$ swagger-ui-watcher myschema.yaml -``` - -Note: Swagger-ui-watcher will complain that "DELETE operations cannot have a requestBody" -but it will still work. This [error](https://github.com/OAI/OpenAPI-Specification/pull/2117) -in the OAS specification will be fixed when [OAS 3.1.0](https://www.openapis.org/blog/2020/06/18/openapi-3-1-0-rc0-its-here) -is published. - -([swagger-ui](https://www.npmjs.com/package/swagger-ui) will work silently.) - -### Generate a Dynamic Schema in a View - -See [DRF documentation for a Dynamic Schema](https://www.django-rest-framework.org/api-guide/schemas/#generating-a-dynamic-schema-with-schemaview). - -```python -from rest_framework.schemas import get_schema_view - -urlpatterns = [ - ... - path('openapi', get_schema_view( - title="Example API", - description="API for all things …", - version="1.0.0", - generator_class=MySchemaGenerator, - ), name='openapi-schema'), - path('swagger-ui/', TemplateView.as_view( - template_name='swagger-ui.html', - extra_context={'schema_url': 'openapi-schema'} - ), name='swagger-ui'), - ... -] -``` - ## Third Party Packages ### About Third Party Packages diff --git a/example/settings/dev.py b/example/settings/dev.py index 7b40e61f..05cab4d1 100644 --- a/example/settings/dev.py +++ b/example/settings/dev.py @@ -83,7 +83,6 @@ "rest_framework_json_api.renderers.BrowsableAPIRenderer", ), "DEFAULT_METADATA_CLASS": "rest_framework_json_api.metadata.JSONAPIMetadata", - "DEFAULT_SCHEMA_CLASS": "rest_framework_json_api.schemas.openapi.AutoSchema", "DEFAULT_FILTER_BACKENDS": ( "rest_framework_json_api.filters.OrderingFilter", "rest_framework_json_api.django_filters.DjangoFilterBackend", diff --git a/example/templates/swagger-ui.html b/example/templates/swagger-ui.html deleted file mode 100644 index 29776491..00000000 --- a/example/templates/swagger-ui.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - Swagger - - - - - -
- - - - \ No newline at end of file diff --git a/example/tests/__snapshots__/test_openapi.ambr b/example/tests/__snapshots__/test_openapi.ambr deleted file mode 100644 index f72c6ff8..00000000 --- a/example/tests/__snapshots__/test_openapi.ambr +++ /dev/null @@ -1,1414 +0,0 @@ -# serializer version: 1 -# name: test_delete_request - ''' - { - "description": "", - "operationId": "destroy/authors/{id}", - "parameters": [ - { - "description": "A unique integer value identifying this author.", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/onlymeta" - } - } - }, - "description": "[OK](https://jsonapi.org/format/#crud-deleting-responses-200)" - }, - "202": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/datum" - } - } - }, - "description": "Accepted for [asynchronous processing](https://jsonapi.org/recommendations/#asynchronous-processing)" - }, - "204": { - "description": "[no content](https://jsonapi.org/format/#crud-deleting-responses-204)" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Resource does not exist](https://jsonapi.org/format/#crud-deleting-responses-404)" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_patch_request - ''' - { - "description": "", - "operationId": "update/authors/{id}", - "parameters": [ - { - "description": "A unique integer value identifying this author.", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "additionalProperties": false, - "properties": { - "attributes": { - "properties": { - "defaults": { - "default": "default", - "description": "help for defaults", - "maxLength": 20, - "minLength": 3, - "type": "string", - "writeOnly": true - }, - "email": { - "format": "email", - "maxLength": 254, - "type": "string" - }, - "fullName": { - "maxLength": 50, - "type": "string" - }, - "name": { - "maxLength": 50, - "type": "string" - } - }, - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "properties": { - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationships": { - "properties": { - "authorType": { - "$ref": "#/components/schemas/reltoone" - }, - "bio": { - "$ref": "#/components/schemas/reltoone" - }, - "comments": { - "$ref": "#/components/schemas/reltomany" - }, - "entries": { - "$ref": "#/components/schemas/reltomany" - }, - "firstEntry": { - "$ref": "#/components/schemas/reltoone" - } - }, - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - } - }, - "required": [ - "data" - ] - } - } - } - }, - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "$ref": "#/components/schemas/Author" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "update/authors/{id}" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "403": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Forbidden](https://jsonapi.org/format/#crud-updating-responses-403)" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Related resource does not exist](https://jsonapi.org/format/#crud-updating-responses-404)" - }, - "409": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Conflict]([Conflict](https://jsonapi.org/format/#crud-updating-responses-409)" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_path_with_id_parameter - ''' - { - "description": "", - "operationId": "retrieve/authors/{id}/", - "parameters": [ - { - "description": "A unique integer value identifying this author.", - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "$ref": "#/components/parameters/include" - }, - { - "$ref": "#/components/parameters/fields" - }, - { - "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "author_type", - "in": "query", - "name": "filter[authorType]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "name", - "in": "query", - "name": "filter[name]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "A search term.", - "in": "query", - "name": "filter[search]", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "$ref": "#/components/schemas/AuthorDetail" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "retrieve/authors/{id}/" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not found" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_path_without_parameters - ''' - { - "description": "", - "operationId": "List/authors/", - "parameters": [ - { - "$ref": "#/components/parameters/include" - }, - { - "$ref": "#/components/parameters/fields" - }, - { - "description": "A page number within the paginated result set.", - "in": "query", - "name": "page[number]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "Number of results to return per page.", - "in": "query", - "name": "page[size]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "author_type", - "in": "query", - "name": "filter[authorType]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "name", - "in": "query", - "name": "filter[name]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "A search term.", - "in": "query", - "name": "filter[search]", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/AuthorList" - }, - "type": "array" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "List/authors/" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not found" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_post_request - ''' - { - "description": "", - "operationId": "create/authors/", - "parameters": [], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "additionalProperties": false, - "properties": { - "attributes": { - "properties": { - "defaults": { - "default": "default", - "description": "help for defaults", - "maxLength": 20, - "minLength": 3, - "type": "string", - "writeOnly": true - }, - "email": { - "format": "email", - "maxLength": 254, - "type": "string" - }, - "fullName": { - "maxLength": 50, - "type": "string" - }, - "name": { - "maxLength": 50, - "type": "string" - } - }, - "required": [ - "name", - "fullName", - "email" - ], - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "properties": { - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationships": { - "properties": { - "authorType": { - "$ref": "#/components/schemas/reltoone" - }, - "bio": { - "$ref": "#/components/schemas/reltoone" - }, - "comments": { - "$ref": "#/components/schemas/reltomany" - }, - "entries": { - "$ref": "#/components/schemas/reltomany" - }, - "firstEntry": { - "$ref": "#/components/schemas/reltoone" - } - }, - "required": [ - "bio", - "entries", - "comments" - ], - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type" - ], - "type": "object" - } - }, - "required": [ - "data" - ] - } - } - } - }, - "responses": { - "201": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "$ref": "#/components/schemas/Author" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-201). Assigned `id` and/or any other changes are in this response." - }, - "202": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/datum" - } - } - }, - "description": "Accepted for [asynchronous processing](https://jsonapi.org/recommendations/#asynchronous-processing)" - }, - "204": { - "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-204) with the supplied `id`. No other changes from what was POSTed." - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "403": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Forbidden](https://jsonapi.org/format/#crud-creating-responses-403)" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Related resource does not exist](https://jsonapi.org/format/#crud-creating-responses-404)" - }, - "409": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "[Conflict](https://jsonapi.org/format/#crud-creating-responses-409)" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - ''' -# --- -# name: test_schema_construction - ''' - { - "components": { - "parameters": { - "fields": { - "description": "[sparse fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets).\nUse fields[\\]=field1,field2,...,fieldN", - "explode": true, - "in": "query", - "name": "fields", - "required": false, - "schema": { - "type": "object" - }, - "style": "deepObject" - }, - "include": { - "description": "[list of included related resources](https://jsonapi.org/format/#fetching-includes)", - "in": "query", - "name": "include", - "required": false, - "schema": { - "type": "string" - }, - "style": "form" - } - }, - "schemas": { - "AuthorList": { - "additionalProperties": false, - "properties": { - "attributes": { - "properties": { - "defaults": { - "default": "default", - "description": "help for defaults", - "maxLength": 20, - "minLength": 3, - "type": "string", - "writeOnly": true - }, - "email": { - "format": "email", - "maxLength": 254, - "type": "string" - }, - "fullName": { - "maxLength": 50, - "type": "string" - }, - "initials": { - "readOnly": true, - "type": "string" - }, - "name": { - "maxLength": 50, - "type": "string" - } - }, - "required": [ - "name", - "fullName", - "email" - ], - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "properties": { - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationships": { - "properties": { - "authorType": { - "$ref": "#/components/schemas/reltoone" - }, - "bio": { - "$ref": "#/components/schemas/reltoone" - }, - "comments": { - "$ref": "#/components/schemas/reltomany" - }, - "entries": { - "$ref": "#/components/schemas/reltomany" - }, - "firstEntry": { - "$ref": "#/components/schemas/reltoone" - } - }, - "required": [ - "bio", - "entries", - "comments" - ], - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "ResourceIdentifierObject": { - "oneOf": [ - { - "$ref": "#/components/schemas/relationshipToOne" - }, - { - "$ref": "#/components/schemas/relationshipToMany" - } - ] - }, - "datum": { - "description": "singular item", - "properties": { - "data": { - "$ref": "#/components/schemas/resource" - } - } - }, - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "detail": { - "type": "string" - }, - "id": { - "type": "string" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "source": { - "properties": { - "meta": { - "$ref": "#/components/schemas/meta" - }, - "parameter": { - "description": "A string indicating which query parameter caused the error.", - "type": "string" - }, - "pointer": { - "description": "A [JSON Pointer](https://tools.ietf.org/html/rfc6901) to the associated entity in the request document [e.g. `/data` for a primary data object, or `/data/attributes/title` for a specific attribute.", - "type": "string" - } - }, - "type": "object" - }, - "status": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "type": "object" - }, - "errors": { - "items": { - "$ref": "#/components/schemas/error" - }, - "type": "array", - "uniqueItems": true - }, - "failure": { - "properties": { - "errors": { - "$ref": "#/components/schemas/errors" - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "required": [ - "errors" - ], - "type": "object" - }, - "id": { - "description": "Each resource object\u2019s type and id pair MUST [identify](https://jsonapi.org/format/#document-resource-object-identification) a single, unique resource.", - "type": "string" - }, - "include": { - "additionalProperties": false, - "properties": { - "attributes": { - "additionalProperties": true, - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "meta": { - "$ref": "#/components/schemas/meta" - }, - "relationships": { - "additionalProperties": true, - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "jsonapi": { - "additionalProperties": false, - "description": "The server's implementation", - "properties": { - "meta": { - "$ref": "#/components/schemas/meta" - }, - "version": { - "type": "string" - } - }, - "type": "object" - }, - "link": { - "oneOf": [ - { - "description": "a string containing the link's URL", - "format": "uri-reference", - "type": "string" - }, - { - "properties": { - "href": { - "description": "a string containing the link's URL", - "format": "uri-reference", - "type": "string" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "required": [ - "href" - ], - "type": "object" - } - ] - }, - "linkage": { - "description": "the 'type' and 'id'", - "properties": { - "id": { - "$ref": "#/components/schemas/id" - }, - "meta": { - "$ref": "#/components/schemas/meta" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "links": { - "additionalProperties": { - "$ref": "#/components/schemas/link" - }, - "type": "object" - }, - "meta": { - "additionalProperties": true, - "type": "object" - }, - "nulltype": { - "default": null, - "nullable": true, - "type": "object" - }, - "onlymeta": { - "additionalProperties": false, - "properties": { - "meta": { - "$ref": "#/components/schemas/meta" - } - } - }, - "pageref": { - "oneOf": [ - { - "format": "uri-reference", - "type": "string" - }, - { - "$ref": "#/components/schemas/nulltype" - } - ] - }, - "pagination": { - "properties": { - "first": { - "$ref": "#/components/schemas/pageref" - }, - "last": { - "$ref": "#/components/schemas/pageref" - }, - "next": { - "$ref": "#/components/schemas/pageref" - }, - "prev": { - "$ref": "#/components/schemas/pageref" - } - }, - "type": "object" - }, - "relationshipLinks": { - "additionalProperties": true, - "description": "optional references to other resource objects", - "properties": { - "related": { - "$ref": "#/components/schemas/link" - }, - "self": { - "$ref": "#/components/schemas/link" - } - }, - "type": "object" - }, - "relationshipToMany": { - "description": "An array of objects each containing the 'type' and 'id' for to-many relationships", - "items": { - "$ref": "#/components/schemas/linkage" - }, - "type": "array", - "uniqueItems": true - }, - "relationshipToOne": { - "anyOf": [ - { - "$ref": "#/components/schemas/nulltype" - }, - { - "$ref": "#/components/schemas/linkage" - } - ], - "description": "reference to other resource in a to-one relationship" - }, - "reltomany": { - "description": "a multiple 'to-many' relationship", - "properties": { - "data": { - "$ref": "#/components/schemas/relationshipToMany" - }, - "links": { - "$ref": "#/components/schemas/relationshipLinks" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "type": "object" - }, - "reltoone": { - "description": "a singular 'to-one' relationship", - "properties": { - "data": { - "$ref": "#/components/schemas/relationshipToOne" - }, - "links": { - "$ref": "#/components/schemas/relationshipLinks" - }, - "meta": { - "$ref": "#/components/schemas/meta" - } - }, - "type": "object" - }, - "resource": { - "additionalProperties": false, - "properties": { - "attributes": { - "type": "object" - }, - "id": { - "$ref": "#/components/schemas/id" - }, - "links": { - "$ref": "#/components/schemas/links" - }, - "meta": { - "$ref": "#/components/schemas/meta" - }, - "relationships": { - "type": "object" - }, - "type": { - "$ref": "#/components/schemas/type" - } - }, - "required": [ - "type", - "id" - ], - "type": "object" - }, - "type": { - "description": "The [type](https://jsonapi.org/format/#document-resource-object-identification) member is used to describe resource objects that share common attributes and relationships.", - "type": "string" - } - } - }, - "info": { - "title": "", - "version": "" - }, - "openapi": "3.0.2", - "paths": { - "/authors/": { - "get": { - "description": "", - "operationId": "List/authors/", - "parameters": [ - { - "$ref": "#/components/parameters/include" - }, - { - "$ref": "#/components/parameters/fields" - }, - { - "description": "A page number within the paginated result set.", - "in": "query", - "name": "page[number]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "Number of results to return per page.", - "in": "query", - "name": "page[size]", - "required": false, - "schema": { - "type": "integer" - } - }, - { - "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)", - "in": "query", - "name": "sort", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "author_type", - "in": "query", - "name": "filter[authorType]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "name", - "in": "query", - "name": "filter[name]", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "A search term.", - "in": "query", - "name": "filter[search]", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/vnd.api+json": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/AuthorList" - }, - "type": "array" - }, - "included": { - "items": { - "$ref": "#/components/schemas/include" - }, - "type": "array", - "uniqueItems": true - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi" - }, - "links": { - "allOf": [ - { - "$ref": "#/components/schemas/links" - }, - { - "$ref": "#/components/schemas/pagination" - } - ], - "description": "Link members related to primary data" - } - }, - "required": [ - "data" - ], - "type": "object" - } - } - }, - "description": "List/authors/" - }, - "400": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "bad request" - }, - "401": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not authorized" - }, - "404": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "not found" - }, - "429": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/failure" - } - } - }, - "description": "too many requests" - } - }, - "tags": [ - "authors" - ] - } - } - } - } - ''' -# --- diff --git a/example/tests/test_openapi.py b/example/tests/test_openapi.py deleted file mode 100644 index fa2f9c73..00000000 --- a/example/tests/test_openapi.py +++ /dev/null @@ -1,230 +0,0 @@ -# largely based on DRF's test_openapi -import json - -import pytest -from django.test import RequestFactory, override_settings -from django.urls import re_path -from rest_framework.request import Request - -from rest_framework_json_api.schemas.openapi import AutoSchema, SchemaGenerator - -from example import views - -pytestmark = pytest.mark.filterwarnings("ignore:Built-in support") - - -def create_request(path): - factory = RequestFactory() - request = Request(factory.get(path)) - return request - - -def create_view_with_kw(view_cls, method, request, initkwargs): - generator = SchemaGenerator() - view = generator.create_view(view_cls.as_view(initkwargs), method, request) - return view - - -def test_path_without_parameters(snapshot): - path = "/authors/" - method = "GET" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"get": "list"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_path_with_id_parameter(snapshot): - path = "/authors/{id}/" - method = "GET" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"get": "retrieve"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_post_request(snapshot): - method = "POST" - path = "/authors/" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"post": "create"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_patch_request(snapshot): - method = "PATCH" - path = "/authors/{id}" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"patch": "update"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -def test_delete_request(snapshot): - method = "DELETE" - path = "/authors/{id}" - - view = create_view_with_kw( - views.AuthorViewSet, method, create_request(path), {"delete": "delete"} - ) - inspector = AutoSchema() - inspector.view = view - - operation = inspector.get_operation(path, method) - assert snapshot == json.dumps(operation, indent=2, sort_keys=True) - - -@override_settings( - REST_FRAMEWORK={ - "DEFAULT_SCHEMA_CLASS": "rest_framework_json_api.schemas.openapi.AutoSchema" - } -) -def test_schema_construction(snapshot): - """Construction of the top level dictionary.""" - patterns = [ - re_path("^authors/?$", views.AuthorViewSet.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - assert snapshot == json.dumps(schema, indent=2, sort_keys=True) - - -def test_schema_id_field(): - """ID field is only included in the root, not the attributes.""" - patterns = [ - re_path("^companies/?$", views.CompanyViewset.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - company_properties = schema["components"]["schemas"]["Company"]["properties"] - assert company_properties["id"] == {"$ref": "#/components/schemas/id"} - assert "id" not in company_properties["attributes"]["properties"] - - -def test_schema_subserializers(): - """Schema for child Serializers reflects the actual response structure.""" - patterns = [ - re_path( - "^questionnaires/?$", views.QuestionnaireViewset.as_view({"get": "list"}) - ), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - assert { - "type": "object", - "properties": { - "metadata": { - "type": "object", - "properties": { - "author": {"type": "string"}, - "producer": {"type": "string"}, - }, - "required": ["author"], - }, - "questions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "text": {"type": "string"}, - "required": {"type": "boolean", "default": False}, - }, - "required": ["text"], - }, - }, - "name": {"type": "string", "maxLength": 100}, - }, - "required": ["name", "questions", "metadata"], - } == schema["components"]["schemas"]["Questionnaire"]["properties"]["attributes"] - - -def test_schema_parameters_include(): - """Include paramater is only used when serializer defines included_serializers.""" - patterns = [ - re_path("^authors/?$", views.AuthorViewSet.as_view({"get": "list"})), - re_path("^project-types/?$", views.ProjectTypeViewset.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = create_request("/") - schema = generator.get_schema(request=request) - - include_ref = {"$ref": "#/components/parameters/include"} - assert include_ref in schema["paths"]["/authors/"]["get"]["parameters"] - assert include_ref not in schema["paths"]["/project-types/"]["get"]["parameters"] - - -def test_schema_serializer_method_resource_related_field(): - """SerializerMethodResourceRelatedField fieds have the correct relation ref.""" - patterns = [ - re_path("^entries/?$", views.EntryViewSet.as_view({"get": "list"})), - ] - generator = SchemaGenerator(patterns=patterns) - - request = Request(RequestFactory().get("/", {"include": "featured"})) - schema = generator.get_schema(request=request) - - entry_schema = schema["components"]["schemas"]["Entry"] - entry_relationships = entry_schema["properties"]["relationships"]["properties"] - - rel_to_many_ref = {"$ref": "#/components/schemas/reltomany"} - assert entry_relationships["suggested"] == rel_to_many_ref - assert entry_relationships["suggestedHyperlinked"] == rel_to_many_ref - - rel_to_one_ref = {"$ref": "#/components/schemas/reltoone"} - assert entry_relationships["featured"] == rel_to_one_ref - assert entry_relationships["featuredHyperlinked"] == rel_to_one_ref - - -def test_schema_related_serializers(): - """ - Confirm that paths are generated for related fields. For example: - /authors/{pk}/{related_field>} - /authors/{id}/comments/ - /authors/{id}/entries/ - /authors/{id}/first_entry/ - and confirm that the schema for the related field is properly rendered - """ - generator = SchemaGenerator() - request = create_request("/") - schema = generator.get_schema(request=request) - # make sure the path's relationship and related {related_field}'s got expanded - assert "/authors/{id}/relationships/{related_field}" in schema["paths"] - assert "/authors/{id}/comments/" in schema["paths"] - assert "/authors/{id}/entries/" in schema["paths"] - assert "/authors/{id}/first_entry/" in schema["paths"] - first_get = schema["paths"]["/authors/{id}/first_entry/"]["get"]["responses"]["200"] - first_schema = first_get["content"]["application/vnd.api+json"]["schema"] - first_props = first_schema["properties"]["data"] - assert "$ref" in first_props - assert first_props["$ref"] == "#/components/schemas/Entry" diff --git a/example/tests/unit/test_filter_schema_params.py b/example/tests/unit/test_filter_schema_params.py deleted file mode 100644 index d7cb4fb8..00000000 --- a/example/tests/unit/test_filter_schema_params.py +++ /dev/null @@ -1,107 +0,0 @@ -from rest_framework import filters as drf_filters - -from rest_framework_json_api import filters as dja_filters -from rest_framework_json_api.django_filters import backends - -from example.views import EntryViewSet - - -class DummyEntryViewSet(EntryViewSet): - filter_backends = ( - dja_filters.QueryParameterValidationFilter, - dja_filters.OrderingFilter, - backends.DjangoFilterBackend, - drf_filters.SearchFilter, - ) - filterset_fields = { - "id": ("exact",), - "headline": ("exact", "contains"), - "blog__name": ("contains",), - } - - def __init__(self, **kwargs): - # dummy up self.request since PreloadIncludesMixin expects it to be defined - self.request = None - super().__init__(**kwargs) - - -def test_filters_get_schema_params(): - """ - test all my filters for `get_schema_operation_parameters()` - """ - # list of tuples: (filter, expected result) - filters = [ - (dja_filters.QueryParameterValidationFilter, []), - ( - backends.DjangoFilterBackend, - [ - { - "name": "filter[id]", - "required": False, - "in": "query", - "description": "id", - "schema": {"type": "string"}, - }, - { - "name": "filter[headline]", - "required": False, - "in": "query", - "description": "headline", - "schema": {"type": "string"}, - }, - { - "name": "filter[headline.contains]", - "required": False, - "in": "query", - "description": "headline__contains", - "schema": {"type": "string"}, - }, - { - "name": "filter[blog.name.contains]", - "required": False, - "in": "query", - "description": "blog__name__contains", - "schema": {"type": "string"}, - }, - ], - ), - ( - dja_filters.OrderingFilter, - [ - { - "name": "sort", - "required": False, - "in": "query", - "description": "[list of fields to sort by]" - "(https://jsonapi.org/format/#fetching-sorting)", - "schema": {"type": "string"}, - } - ], - ), - ( - drf_filters.SearchFilter, - [ - { - "name": "filter[search]", - "required": False, - "in": "query", - "description": "A search term.", - "schema": {"type": "string"}, - } - ], - ), - ] - view = DummyEntryViewSet() - - for c, expected in filters: - f = c() - result = f.get_schema_operation_parameters(view) - assert len(result) == len(expected) - if len(result) == 0: - continue - # py35: the result list/dict ordering isn't guaranteed - for res_item in result: - assert "name" in res_item - for exp_item in expected: - if res_item["name"] == exp_item["name"]: - assert res_item == exp_item diff --git a/example/urls.py b/example/urls.py index 413d058d..471fbe81 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,9 +1,5 @@ from django.urls import include, path, re_path -from django.views.generic import TemplateView from rest_framework import routers -from rest_framework.schemas import get_schema_view - -from rest_framework_json_api.schemas.openapi import SchemaGenerator from example.views import ( AuthorRelationshipView, @@ -87,22 +83,4 @@ AuthorRelationshipView.as_view(), name="author-relationships", ), - path( - "openapi", - get_schema_view( - title="Example API", - description="API for all things …", - version="1.0.0", - generator_class=SchemaGenerator, - ), - name="openapi-schema", - ), - path( - "swagger-ui/", - TemplateView.as_view( - template_name="swagger-ui.html", - extra_context={"schema_url": "openapi-schema"}, - ), - name="swagger-ui", - ), ] diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 589636e6..3db600e2 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -3,5 +3,3 @@ django-filter==24.3 # should be set to pinned version again # see https://github.com/django-polymorphic/django-polymorphic/pull/541 django-polymorphic@git+https://github.com/django-polymorphic/django-polymorphic@master # pyup: ignore -pyyaml==6.0.2 -uritemplate==4.1.1 diff --git a/rest_framework_json_api/django_filters/backends.py b/rest_framework_json_api/django_filters/backends.py index c0044839..70e543c1 100644 --- a/rest_framework_json_api/django_filters/backends.py +++ b/rest_framework_json_api/django_filters/backends.py @@ -4,7 +4,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.settings import api_settings -from rest_framework_json_api.utils import format_field_name, undo_format_field_name +from rest_framework_json_api.utils import undo_format_field_name class DjangoFilterBackend(DjangoFilterBackend): @@ -129,18 +129,3 @@ def get_filterset_kwargs(self, request, queryset, view): "request": request, "filter_keys": filter_keys, } - - def get_schema_operation_parameters(self, view): - """ - Convert backend filter `name` to JSON:API-style `filter[name]`. - For filters that are relationship paths, rewrite ORM-style `__` to our preferred `.`. - For example: `blog__name__contains` becomes `filter[blog.name.contains]`. - - This is basically the reverse of `get_filterset_kwargs` above. - """ - result = super().get_schema_operation_parameters(view) - for res in result: - if "name" in res: - name = format_field_name(res["name"].replace("__", ".")) - res["name"] = f"filter[{name}]" - return result diff --git a/rest_framework_json_api/schemas/openapi.py b/rest_framework_json_api/schemas/openapi.py deleted file mode 100644 index 6892e991..00000000 --- a/rest_framework_json_api/schemas/openapi.py +++ /dev/null @@ -1,905 +0,0 @@ -import warnings -from urllib.parse import urljoin - -from rest_framework.fields import empty -from rest_framework.relations import ManyRelatedField -from rest_framework.schemas import openapi as drf_openapi -from rest_framework.schemas.utils import is_list_view - -from rest_framework_json_api import serializers, views -from rest_framework_json_api.relations import ManySerializerMethodResourceRelatedField -from rest_framework_json_api.utils import format_field_name - - -class SchemaGenerator(drf_openapi.SchemaGenerator): - """ - Extend DRF's SchemaGenerator to implement JSON:API flavored generateschema command. - """ - - #: These JSON:API component definitions are referenced by the generated OAS schema. - #: If you need to add more or change these static component definitions, extend this dict. - jsonapi_components = { - "schemas": { - "jsonapi": { - "type": "object", - "description": "The server's implementation", - "properties": { - "version": {"type": "string"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - "additionalProperties": False, - }, - "resource": { - "type": "object", - "required": ["type", "id"], - "additionalProperties": False, - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "attributes": { - "type": "object", - # ... - }, - "relationships": { - "type": "object", - # ... - }, - "links": {"$ref": "#/components/schemas/links"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "include": { - "type": "object", - "required": ["type", "id"], - "additionalProperties": False, - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "attributes": { - "type": "object", - "additionalProperties": True, - # ... - }, - "relationships": { - "type": "object", - "additionalProperties": True, - # ... - }, - "links": {"$ref": "#/components/schemas/links"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "link": { - "oneOf": [ - { - "description": "a string containing the link's URL", - "type": "string", - "format": "uri-reference", - }, - { - "type": "object", - "required": ["href"], - "properties": { - "href": { - "description": "a string containing the link's URL", - "type": "string", - "format": "uri-reference", - }, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - ] - }, - "links": { - "type": "object", - "additionalProperties": {"$ref": "#/components/schemas/link"}, - }, - "reltoone": { - "description": "a singular 'to-one' relationship", - "type": "object", - "properties": { - "links": {"$ref": "#/components/schemas/relationshipLinks"}, - "data": {"$ref": "#/components/schemas/relationshipToOne"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "relationshipToOne": { - "description": "reference to other resource in a to-one relationship", - "anyOf": [ - {"$ref": "#/components/schemas/nulltype"}, - {"$ref": "#/components/schemas/linkage"}, - ], - }, - "reltomany": { - "description": "a multiple 'to-many' relationship", - "type": "object", - "properties": { - "links": {"$ref": "#/components/schemas/relationshipLinks"}, - "data": {"$ref": "#/components/schemas/relationshipToMany"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "relationshipLinks": { - "description": "optional references to other resource objects", - "type": "object", - "additionalProperties": True, - "properties": { - "self": {"$ref": "#/components/schemas/link"}, - "related": {"$ref": "#/components/schemas/link"}, - }, - }, - "relationshipToMany": { - "description": "An array of objects each containing the " - "'type' and 'id' for to-many relationships", - "type": "array", - "items": {"$ref": "#/components/schemas/linkage"}, - "uniqueItems": True, - }, - # A RelationshipView uses a ResourceIdentifierObjectSerializer (hence the name - # ResourceIdentifierObject returned by get_component_name()) which serializes type - # and id. These can be lists or individual items depending on whether the - # relationship is toMany or toOne so offer both options since we are not iterating - # over all the possible {related_field}'s but rather rendering one path schema - # which may represent toMany and toOne relationships. - "ResourceIdentifierObject": { - "oneOf": [ - {"$ref": "#/components/schemas/relationshipToOne"}, - {"$ref": "#/components/schemas/relationshipToMany"}, - ] - }, - "linkage": { - "type": "object", - "description": "the 'type' and 'id'", - "required": ["type", "id"], - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - "pagination": { - "type": "object", - "properties": { - "first": {"$ref": "#/components/schemas/pageref"}, - "last": {"$ref": "#/components/schemas/pageref"}, - "prev": {"$ref": "#/components/schemas/pageref"}, - "next": {"$ref": "#/components/schemas/pageref"}, - }, - }, - "pageref": { - "oneOf": [ - {"type": "string", "format": "uri-reference"}, - {"$ref": "#/components/schemas/nulltype"}, - ] - }, - "failure": { - "type": "object", - "required": ["errors"], - "properties": { - "errors": {"$ref": "#/components/schemas/errors"}, - "meta": {"$ref": "#/components/schemas/meta"}, - "jsonapi": {"$ref": "#/components/schemas/jsonapi"}, - "links": {"$ref": "#/components/schemas/links"}, - }, - }, - "errors": { - "type": "array", - "items": {"$ref": "#/components/schemas/error"}, - "uniqueItems": True, - }, - "error": { - "type": "object", - "additionalProperties": False, - "properties": { - "id": {"type": "string"}, - "status": {"type": "string"}, - "links": {"$ref": "#/components/schemas/links"}, - "code": {"type": "string"}, - "title": {"type": "string"}, - "detail": {"type": "string"}, - "source": { - "type": "object", - "properties": { - "pointer": { - "type": "string", - "description": ( - "A [JSON Pointer](https://tools.ietf.org/html/rfc6901) " - "to the associated entity in the request document " - "[e.g. `/data` for a primary data object, or " - "`/data/attributes/title` for a specific attribute." - ), - }, - "parameter": { - "type": "string", - "description": "A string indicating which query parameter " - "caused the error.", - }, - "meta": {"$ref": "#/components/schemas/meta"}, - }, - }, - }, - }, - "onlymeta": { - "additionalProperties": False, - "properties": {"meta": {"$ref": "#/components/schemas/meta"}}, - }, - "meta": {"type": "object", "additionalProperties": True}, - "datum": { - "description": "singular item", - "properties": {"data": {"$ref": "#/components/schemas/resource"}}, - }, - "nulltype": {"type": "object", "nullable": True, "default": None}, - "type": { - "type": "string", - "description": "The [type]" - "(https://jsonapi.org/format/#document-resource-object-identification) " - "member is used to describe resource objects that share common attributes " - "and relationships.", - }, - "id": { - "type": "string", - "description": "Each resource object’s type and id pair MUST " - "[identify]" - "(https://jsonapi.org/format/#document-resource-object-identification) " - "a single, unique resource.", - }, - }, - "parameters": { - "include": { - "name": "include", - "in": "query", - "description": "[list of included related resources]" - "(https://jsonapi.org/format/#fetching-includes)", - "required": False, - "style": "form", - "schema": {"type": "string"}, - }, - # TODO: deepObject not well defined/supported: - # https://github.com/OAI/OpenAPI-Specification/issues/1706 - "fields": { - "name": "fields", - "in": "query", - "description": "[sparse fieldsets]" - "(https://jsonapi.org/format/#fetching-sparse-fieldsets).\n" - "Use fields[\\]=field1,field2,...,fieldN", - "required": False, - "style": "deepObject", - "schema": { - "type": "object", - }, - "explode": True, - }, - }, - } - - def get_schema(self, request=None, public=False): - """ - Generate a JSON:API OpenAPI schema. - Overrides upstream DRF's get_schema. - """ - # TODO: avoid copying so much of upstream get_schema() - schema = super().get_schema(request, public) - - components_schemas = {} - - # Iterate endpoints generating per method path operations. - paths = {} - _, view_endpoints = self._get_paths_and_endpoints(None if public else request) - - #: `expanded_endpoints` is like view_endpoints with one extra field tacked on: - #: - 'action' copy of current view.action (list/fetch) as this gets reset for - # each request. - expanded_endpoints = [] - for path, method, view in view_endpoints: - if hasattr(view, "action") and view.action == "retrieve_related": - expanded_endpoints += self._expand_related( - path, method, view, view_endpoints - ) - else: - expanded_endpoints.append( - (path, method, view, getattr(view, "action", None)) - ) - - for path, method, view, action in expanded_endpoints: - if not self.has_view_permissions(path, method, view): - continue - # kludge to preserve view.action as it is 'list' for the parent ViewSet - # but the related viewset that was expanded may be either 'fetch' (to_one) or 'list' - # (to_many). This patches the view.action appropriately so that - # view.schema.get_operation() "does the right thing" for fetch vs. list. - current_action = None - if hasattr(view, "action"): - current_action = view.action - view.action = action - operation = view.schema.get_operation(path, method) - components = view.schema.get_components(path, method) - for k in components.keys(): - if k not in components_schemas: - continue - if components_schemas[k] == components[k]: - continue - warnings.warn( - f'Schema component "{k}" has been overriden with a different value.', - stacklevel=1, - ) - - components_schemas.update(components) - - if hasattr(view, "action"): - view.action = current_action - # Normalise path for any provided mount url. - if path.startswith("/"): - path = path[1:] - path = urljoin(self.url or "/", path) - - paths.setdefault(path, {}) - paths[path][method.lower()] = operation - - self.check_duplicate_operation_id(paths) - - # Compile final schema, overriding stuff from super class. - schema["paths"] = paths - schema["components"] = self.jsonapi_components - schema["components"]["schemas"].update(components_schemas) - - return schema - - def _expand_related(self, path, method, view, view_endpoints): - """ - Expand path containing .../{id}/{related_field} into list of related fields - and **their** views, making sure toOne relationship's views are a 'fetch' and toMany - relationship's are a 'list'. - :param path - :param method - :param view - :param view_endpoints - :return:list[tuple(path, method, view, action)] - """ - result = [] - serializer = view.get_serializer() - # It's not obvious if it's allowed to have both included_ and related_ serializers, - # so just merge both dicts. - serializers = {} - if hasattr(serializer, "included_serializers"): - serializers = {**serializers, **serializer.included_serializers} - if hasattr(serializer, "related_serializers"): - serializers = {**serializers, **serializer.related_serializers} - related_fields = [fs for fs in serializers.items()] - - for field, related_serializer in related_fields: - related_view = self._find_related_view( - view_endpoints, related_serializer, view - ) - if related_view: - action = self._field_is_one_or_many(field, view) - result.append( - ( - path.replace("{related_field}", field), - method, - related_view, - action, - ) - ) - - return result - - def _find_related_view(self, view_endpoints, related_serializer, parent_view): - """ - For a given related_serializer, try to find it's "parent" view instance. - - :param view_endpoints: list of all view endpoints - :param related_serializer: the related serializer for a given related field - :param parent_view: the parent view (used to find toMany vs. toOne). - TODO: not actually used. - :return:view - """ - for _path, _method, view in view_endpoints: - view_serializer = view.get_serializer() - if isinstance(view_serializer, related_serializer): - return view - - return None - - def _field_is_one_or_many(self, field, view): - serializer = view.get_serializer() - if isinstance(serializer.fields[field], ManyRelatedField): - return "list" - else: - return "fetch" - - -class AutoSchema(drf_openapi.AutoSchema): - """ - Extend DRF's openapi.AutoSchema for JSON:API serialization. - """ - - #: ignore all the media types and only generate a JSON:API schema. - content_types = ["application/vnd.api+json"] - - def get_operation(self, path, method): - """ - JSON:API adds some standard fields to the API response that are not in upstream DRF: - - some that only apply to GET/HEAD methods. - - collections - - special handling for POST, PATCH, DELETE - """ - - warnings.warn( - DeprecationWarning( - "Built-in support for generating OpenAPI schema is deprecated. " - "Use drf-spectacular-json-api instead see " - "https://github.com/jokiefer/drf-spectacular-json-api/" - ), - stacklevel=2, - ) - - operation = {} - operation["operationId"] = self.get_operation_id(path, method) - operation["description"] = self.get_description(path, method) - - serializer = self.get_response_serializer(path, method) - - parameters = [] - parameters += self.get_path_parameters(path, method) - # pagination, filters only apply to GET/HEAD of collections and items - if method in ["GET", "HEAD"]: - parameters += self._get_include_parameters(path, method, serializer) - parameters += self._get_fields_parameters(path, method) - parameters += self.get_pagination_parameters(path, method) - parameters += self.get_filter_parameters(path, method) - operation["parameters"] = parameters - operation["tags"] = self.get_tags(path, method) - - # get request and response code schemas - if method == "GET": - if is_list_view(path, method, self.view): - self._add_get_collection_response(operation, path) - else: - self._add_get_item_response(operation, path) - elif method == "POST": - self._add_post_item_response(operation, path) - elif method == "PATCH": - self._add_patch_item_response(operation, path) - elif method == "DELETE": - # should only allow deleting a resource, not a collection - # TODO: implement delete of a relationship in future release. - self._add_delete_item_response(operation, path) - return operation - - def get_operation_id(self, path, method): - """ - The upstream DRF version creates non-unique operationIDs, because the same view is - used for the main path as well as such as related and relationships. - This concatenates the (mapped) method name and path as the spec allows most any - """ - method_name = getattr(self.view, "action", method.lower()) - if is_list_view(path, method, self.view): - action = "List" - elif method_name not in self.method_mapping: - action = method_name - else: - action = self.method_mapping[method.lower()] - return action + path - - def _get_include_parameters(self, path, method, serializer): - """ - includes parameter: https://jsonapi.org/format/#fetching-includes - """ - if getattr(serializer, "included_serializers", {}): - return [{"$ref": "#/components/parameters/include"}] - return [] - - def _get_fields_parameters(self, path, method): - """ - sparse fieldsets https://jsonapi.org/format/#fetching-sparse-fieldsets - """ - # TODO: See if able to identify the specific types for fields[type]=... and return this: - # name: fields - # in: query - # description: '[sparse fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets)' # noqa: B950 - # required: true - # style: deepObject - # schema: - # type: object - # properties: - # hello: - # type: string # noqa F821 - # world: - # type: string # noqa F821 - # explode: true - return [{"$ref": "#/components/parameters/fields"}] - - def _add_get_collection_response(self, operation, path): - """ - Add GET 200 response for a collection to operation - """ - operation["responses"] = { - "200": self._get_toplevel_200_response( - operation, path, "GET", collection=True - ) - } - self._add_get_4xx_responses(operation) - - def _add_get_item_response(self, operation, path): - """ - add GET 200 response for an item to operation - """ - operation["responses"] = { - "200": self._get_toplevel_200_response( - operation, path, "GET", collection=False - ) - } - self._add_get_4xx_responses(operation) - - def _get_toplevel_200_response(self, operation, path, method, collection=True): - """ - return top-level JSON:API GET 200 response - - :param collection: True for collections; False for individual items. - - Uses a $ref to the components.schemas. component definition. - """ - if collection: - data = { - "type": "array", - "items": self.get_reference(self.get_response_serializer(path, method)), - } - else: - data = self.get_reference(self.get_response_serializer(path, method)) - - return { - "description": operation["operationId"], - "content": { - "application/vnd.api+json": { - "schema": { - "type": "object", - "required": ["data"], - "properties": { - "data": data, - "included": { - "type": "array", - "uniqueItems": True, - "items": {"$ref": "#/components/schemas/include"}, - }, - "links": { - "description": "Link members related to primary data", - "allOf": [ - {"$ref": "#/components/schemas/links"}, - {"$ref": "#/components/schemas/pagination"}, - ], - }, - "jsonapi": {"$ref": "#/components/schemas/jsonapi"}, - }, - } - } - }, - } - - def _add_post_item_response(self, operation, path): - """ - add response for POST of an item to operation - """ - operation["requestBody"] = self.get_request_body(path, "POST") - operation["responses"] = { - "201": self._get_toplevel_200_response( - operation, path, "POST", collection=False - ) - } - operation["responses"]["201"]["description"] = ( - "[Created](https://jsonapi.org/format/#crud-creating-responses-201). " - "Assigned `id` and/or any other changes are in this response." - ) - self._add_async_response(operation) - operation["responses"]["204"] = { - "description": "[Created](https://jsonapi.org/format/#crud-creating-responses-204) " - "with the supplied `id`. No other changes from what was POSTed." - } - self._add_post_4xx_responses(operation) - - def _add_patch_item_response(self, operation, path): - """ - Add PATCH response for an item to operation - """ - operation["requestBody"] = self.get_request_body(path, "PATCH") - operation["responses"] = { - "200": self._get_toplevel_200_response( - operation, path, "PATCH", collection=False - ) - } - self._add_patch_4xx_responses(operation) - - def _add_delete_item_response(self, operation, path): - """ - add DELETE response for item or relationship(s) to operation - """ - # Only DELETE of relationships has a requestBody - if isinstance(self.view, views.RelationshipView): - operation["requestBody"] = self.get_request_body(path, "DELETE") - self._add_delete_responses(operation) - - def get_request_body(self, path, method): - """ - A request body is required by JSON:API for POST, PATCH, and DELETE methods. - """ - serializer = self.get_request_serializer(path, method) - if not isinstance(serializer, (serializers.BaseSerializer,)): - return {} - is_relationship = isinstance(self.view, views.RelationshipView) - - # DRF uses a $ref to the component schema definition, but this - # doesn't work for JSON:API due to the different required fields based on - # the method, so make those changes and inline another copy of the schema. - - # TODO: A future improvement could make this DRYer with multiple component schemas: - # A base schema for each viewset that has no required fields - # One subclassed from the base that requires some fields (`type` but not `id` for POST) - # Another subclassed from base with required type/id but no required attributes (PATCH) - - if is_relationship: - item_schema = {"$ref": "#/components/schemas/ResourceIdentifierObject"} - else: - item_schema = self.map_serializer(serializer) - if method == "POST": - # 'type' and 'id' are both required for: - # - all relationship operations - # - regular PATCH or DELETE - # Only 'type' is required for POST: system may assign the 'id'. - item_schema["required"] = ["type"] - - if "properties" in item_schema and "attributes" in item_schema["properties"]: - # No required attributes for PATCH - if ( - method in ["PATCH", "PUT"] - and "required" in item_schema["properties"]["attributes"] - ): - del item_schema["properties"]["attributes"]["required"] - # No read_only fields for request. - for name, schema in ( - item_schema["properties"]["attributes"]["properties"].copy().items() - ): # noqa E501 - if "readOnly" in schema: - del item_schema["properties"]["attributes"]["properties"][name] - - if "properties" in item_schema and "relationships" in item_schema["properties"]: - # No required relationships for PATCH - if ( - method in ["PATCH", "PUT"] - and "required" in item_schema["properties"]["relationships"] - ): - del item_schema["properties"]["relationships"]["required"] - - return { - "content": { - ct: { - "schema": { - "required": ["data"], - "properties": {"data": item_schema}, - } - } - for ct in self.content_types - } - } - - def map_serializer(self, serializer): - """ - Custom map_serializer that serializes the schema using the JSON:API spec. - - Non-attributes like related and identity fields, are moved to 'relationships' - and 'links'. - """ - # TODO: remove attributes, etc. for relationshipView?? - if isinstance( - serializer.parent, (serializers.ListField, serializers.BaseSerializer) - ): - # Return plain non-JSON:API serializer schema for serializers nested inside - # a Serializer or a ListField, as those don't use the full JSON:API - # serializer schemas. - return super().map_serializer(serializer) - - required = [] - attributes = {} - relationships_required = [] - relationships = {} - - for field in serializer.fields.values(): - if isinstance(field, serializers.HyperlinkedIdentityField): - # the 'url' is not an attribute but rather a self.link, so don't map it here. - continue - if isinstance(field, serializers.HiddenField): - continue - if isinstance( - field, - ( - serializers.ManyRelatedField, - ManySerializerMethodResourceRelatedField, - ), - ): - if field.required: - relationships_required.append(format_field_name(field.field_name)) - relationships[format_field_name(field.field_name)] = { - "$ref": "#/components/schemas/reltomany" - } - continue - if isinstance(field, serializers.RelatedField): - if field.required: - relationships_required.append(format_field_name(field.field_name)) - relationships[format_field_name(field.field_name)] = { - "$ref": "#/components/schemas/reltoone" - } - continue - if field.field_name == "id": - # ID is always provided in the root of JSON:API and removed from the - # attributes in JSONRenderer. - continue - - if field.required: - required.append(format_field_name(field.field_name)) - - schema = self.map_field(field) - if field.read_only: - schema["readOnly"] = True - if field.write_only: - schema["writeOnly"] = True - if field.allow_null: - schema["nullable"] = True - if field.default and field.default != empty and not callable(field.default): - schema["default"] = field.default - if field.help_text: - # Ensure django gettext_lazy is rendered correctly - schema["description"] = str(field.help_text) - self.map_field_validators(field, schema) - - attributes[format_field_name(field.field_name)] = schema - - result = { - "type": "object", - "required": ["type", "id"], - "additionalProperties": False, - "properties": { - "type": {"$ref": "#/components/schemas/type"}, - "id": {"$ref": "#/components/schemas/id"}, - "links": { - "type": "object", - "properties": {"self": {"$ref": "#/components/schemas/link"}}, - }, - }, - } - if attributes: - result["properties"]["attributes"] = { - "type": "object", - "properties": attributes, - } - if required: - result["properties"]["attributes"]["required"] = required - - if relationships: - result["properties"]["relationships"] = { - "type": "object", - "properties": relationships, - } - if relationships_required: - result["properties"]["relationships"][ - "required" - ] = relationships_required - return result - - def _add_async_response(self, operation): - """ - Add async response to operation - """ - operation["responses"]["202"] = { - "description": "Accepted for [asynchronous processing]" - "(https://jsonapi.org/recommendations/#asynchronous-processing)", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/datum"} - } - }, - } - - def _failure_response(self, reason): - """ - Return failure response reason as the description - """ - return { - "description": reason, - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/failure"} - } - }, - } - - def _add_generic_failure_responses(self, operation): - """ - Add generic failure response(s) to operation - """ - for code, reason in [ - ("400", "bad request"), - ("401", "not authorized"), - ("429", "too many requests"), - ]: - operation["responses"][code] = self._failure_response(reason) - - def _add_get_4xx_responses(self, operation): - """ - Add generic 4xx GET responses to operation - """ - self._add_generic_failure_responses(operation) - for code, reason in [("404", "not found")]: - operation["responses"][code] = self._failure_response(reason) - - def _add_post_4xx_responses(self, operation): - """ - Add POST 4xx error responses to operation - """ - self._add_generic_failure_responses(operation) - for code, reason in [ - ( - "403", - "[Forbidden](https://jsonapi.org/format/#crud-creating-responses-403)", - ), - ( - "404", - "[Related resource does not exist]" - "(https://jsonapi.org/format/#crud-creating-responses-404)", - ), - ( - "409", - "[Conflict](https://jsonapi.org/format/#crud-creating-responses-409)", - ), - ]: - operation["responses"][code] = self._failure_response(reason) - - def _add_patch_4xx_responses(self, operation): - """ - Add PATCH 4xx error responses to operation - """ - self._add_generic_failure_responses(operation) - for code, reason in [ - ( - "403", - "[Forbidden](https://jsonapi.org/format/#crud-updating-responses-403)", - ), - ( - "404", - "[Related resource does not exist]" - "(https://jsonapi.org/format/#crud-updating-responses-404)", - ), - ( - "409", - "[Conflict]([Conflict]" - "(https://jsonapi.org/format/#crud-updating-responses-409)", - ), - ]: - operation["responses"][code] = self._failure_response(reason) - - def _add_delete_responses(self, operation): - """ - Add generic DELETE responses to operation - """ - # the 2xx statuses: - operation["responses"] = { - "200": { - "description": "[OK](https://jsonapi.org/format/#crud-deleting-responses-200)", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/onlymeta"} - } - }, - } - } - self._add_async_response(operation) - operation["responses"]["204"] = { - "description": "[no content](https://jsonapi.org/format/#crud-deleting-responses-204)", # noqa: B950 - } - # the 4xx errors: - self._add_generic_failure_responses(operation) - for code, reason in [ - ( - "404", - "[Resource does not exist]" - "(https://jsonapi.org/format/#crud-deleting-responses-404)", - ), - ]: - operation["responses"][code] = self._failure_response(reason) diff --git a/setup.cfg b/setup.cfg index 4230dcbb..92606700 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,9 +63,6 @@ DJANGO_SETTINGS_MODULE=example.settings.test filterwarnings = error::DeprecationWarning error::PendingDeprecationWarning - # Django filter schema generation. Can be removed once we remove - # schema support - ignore:Built-in schema generation is deprecated. testpaths = example tests diff --git a/setup.py b/setup.py index de61b0d1..0b88f4c9 100755 --- a/setup.py +++ b/setup.py @@ -112,7 +112,6 @@ def get_package_data(package): extras_require={ "django-polymorphic": ["django-polymorphic>=3.0"], "django-filter": ["django-filter>=2.4"], - "openapi": ["pyyaml>=5.4", "uritemplate>=3.0.1"], }, setup_requires=wheel, python_requires=">=3.9", diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py deleted file mode 100644 index 427f18fc..00000000 --- a/tests/schemas/test_openapi.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework_json_api.schemas.openapi import AutoSchema -from tests.serializers import CallableDefaultSerializer - - -class TestAutoSchema: - def test_schema_callable_default(self): - inspector = AutoSchema() - result = inspector.map_serializer(CallableDefaultSerializer()) - assert result["properties"]["attributes"]["properties"]["field"] == { - "type": "string", - } From 8d209d9ab402d40639a0a107bf648c57b5aba342 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Mon, 21 Jul 2025 21:57:07 +0700 Subject: [PATCH 13/15] Increase required version of optional Polymorphic Models for Django (#1289) Co-authored-by: Alan Crosswell --- CHANGELOG.md | 4 ++++ requirements/requirements-optionals.txt | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63865fbc..f13d267f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ any parts of the framework not mentioned in the documentation should generally b This adds full support for using multipart field names for includes while configuring `JSON_API_FORMAT_FIELD_NAMES`. * Ensured that sparse fieldset fully supports `JSON_API_FORMAT_FIELD_NAMES`. +### Changed + +* Set minimum required version of optional Polymorphic Models for Django to 4.0.0. + ### Removed * Removed support for Python 3.8. diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 3db600e2..279fd95c 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -2,4 +2,4 @@ django-filter==24.3 # once next version has been released (>3.1.0) this # should be set to pinned version again # see https://github.com/django-polymorphic/django-polymorphic/pull/541 -django-polymorphic@git+https://github.com/django-polymorphic/django-polymorphic@master # pyup: ignore +django-polymorphic@git+https://github.com/django-polymorphic/django-polymorphic@master # pyup: ignore \ No newline at end of file diff --git a/setup.py b/setup.py index 0b88f4c9..2a2a28dd 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def get_package_data(package): "django>=4.2", ], extras_require={ - "django-polymorphic": ["django-polymorphic>=3.0"], + "django-polymorphic": ["django-polymorphic>=4.0.0"], "django-filter": ["django-filter>=2.4"], }, setup_requires=wheel, From fa23bd5bb5a695ea09cfd2106f59e59251cf794d Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Tue, 22 Jul 2025 17:01:21 +0700 Subject: [PATCH 14/15] Set django polymorphic version to newest (#1290) --- requirements/requirements-optionals.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 279fd95c..afbc4754 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,2 @@ django-filter==24.3 -# once next version has been released (>3.1.0) this -# should be set to pinned version again -# see https://github.com/django-polymorphic/django-polymorphic/pull/541 -django-polymorphic@git+https://github.com/django-polymorphic/django-polymorphic@master # pyup: ignore \ No newline at end of file +django-polymorphic==4.1.0 From a6f1f44aeb286af748ac538fe765a097fc48c444 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Thu, 24 Jul 2025 18:37:04 +0400 Subject: [PATCH 15/15] Release 8.0.0 (#1291) --- CHANGELOG.md | 7 +++---- rest_framework_json_api/__init__.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f13d267f..fe7c9e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note that in line with [Django REST framework policy](https://www.django-rest-framework.org/topics/release-notes/), any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. -## [Unreleased] +## [8.0.0] - 2025-07-24 ### Added @@ -17,9 +17,8 @@ any parts of the framework not mentioned in the documentation should generally b ### Fixed -* Ensured that interpreting `include` query parameter is done in internal Python naming. - This adds full support for using multipart field names for includes while configuring `JSON_API_FORMAT_FIELD_NAMES`. -* Ensured that sparse fieldset fully supports `JSON_API_FORMAT_FIELD_NAMES`. +* Ensured that compound documents' `include` query parameter fully support `JSON_API_FORMAT_FIELD_NAMES`. +* Ensured that sparse fieldset's `fields` query parameter fully supports `JSON_API_FORMAT_FIELD_NAMES`. ### Changed diff --git a/rest_framework_json_api/__init__.py b/rest_framework_json_api/__init__.py index a69daa9f..23f6b8f9 100644 --- a/rest_framework_json_api/__init__.py +++ b/rest_framework_json_api/__init__.py @@ -1,5 +1,5 @@ __title__ = "djangorestframework-jsonapi" -__version__ = "7.1.0" +__version__ = "8.0.0" __author__ = "" __license__ = "BSD" __copyright__ = ""