From 9d42d9b7018b08e2df399e598d75f057676b3870 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Fri, 17 Jan 2020 18:42:27 +0400 Subject: [PATCH 01/12] Add support for Python 3.8, Django 3.0 and DRF 3.11 (#752) Fixes #751 Fixes #732 Test matrix build by looking to what is officially supported by Django and DRF in there respective release notes. --- .travis.yml | 51 ++++++++++++++++++------ CHANGELOG.md | 8 ++++ README.rst | 6 +-- docs/getting-started.md | 6 +-- example/api/resources/identity.py | 4 +- example/tests/test_format_keys.py | 2 +- example/tests/test_model_viewsets.py | 12 +++--- example/tests/test_sideload_resources.py | 4 +- rest_framework_json_api/exceptions.py | 2 +- rest_framework_json_api/metadata.py | 6 +-- rest_framework_json_api/relations.py | 2 +- rest_framework_json_api/renderers.py | 12 +++--- rest_framework_json_api/serializers.py | 2 +- rest_framework_json_api/utils.py | 4 +- tox.ini | 8 +++- 15 files changed, 84 insertions(+), 45 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d6ccfb2..301ed0cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +dist: xenial sudo: required cache: pip # Favor explicit over implicit and use an explicit build matrix. @@ -12,6 +13,10 @@ matrix: - env: TOXENV=py35-django22-drfmaster - env: TOXENV=py36-django22-drfmaster - env: TOXENV=py37-django22-drfmaster + - env: TOXENV=py38-django22-drfmaster + - env: TOXENV=py36-django30-drfmaster + - env: TOXENV=py37-django30-drfmaster + - env: TOXENV=py38-django30-drfmaster include: - python: 3.6 @@ -21,50 +26,72 @@ matrix: - python: 3.5 env: TOXENV=py35-django111-drf310 + - python: 3.5 + env: TOXENV=py35-django111-drf311 - python: 3.5 env: TOXENV=py35-django111-drfmaster - python: 3.5 env: TOXENV=py35-django21-drf310 + - python: 3.5 + env: TOXENV=py35-django21-drf311 - python: 3.5 env: TOXENV=py35-django21-drfmaster - python: 3.5 - dist: xenial env: TOXENV=py35-django22-drf310 - python: 3.5 - dist: xenial + env: TOXENV=py35-django22-drf311 + - python: 3.5 env: TOXENV=py35-django22-drfmaster - python: 3.6 env: TOXENV=py36-django111-drf310 + - python: 3.6 + env: TOXENV=py36-django111-drf311 - python: 3.6 env: TOXENV=py36-django111-drfmaster - python: 3.6 env: TOXENV=py36-django21-drf310 + - python: 3.6 + env: TOXENV=py36-django21-drf311 - python: 3.6 env: TOXENV=py36-django21-drfmaster - python: 3.6 - dist: xenial env: TOXENV=py36-django22-drf310 - python: 3.6 - dist: xenial + env: TOXENV=py36-django22-drf311 + - python: 3.6 env: TOXENV=py36-django22-drfmaster + - python: 3.6 + env: TOXENV=py36-django30-drf311 + - python: 3.6 + env: TOXENV=py36-django30-drfmaster - python: 3.7 - dist: xenial - sudo: required env: TOXENV=py37-django21-drf310 - python: 3.7 - dist: xenial - sudo: required + env: TOXENV=py37-django21-drf311 + - python: 3.7 env: TOXENV=py37-django21-drfmaster - python: 3.7 - dist: xenial - sudo: required env: TOXENV=py37-django22-drf310 - python: 3.7 - dist: xenial - sudo: required + env: TOXENV=py37-django22-drf311 + - python: 3.7 env: TOXENV=py37-django22-drfmaster + - python: 3.7 + env: TOXENV=py37-django30-drf311 + - python: 3.7 + env: TOXENV=py37-django30-drfmaster + + - python: 3.8 + env: TOXENV=py38-django22-drf311 + - python: 3.8 + env: TOXENV=py38-django22-drfmaster + - python: 3.8 + env: TOXENV=py38-django30-drf311 + - python: 3.8 + env: TOXENV=py38-django30-drfmaster + install: - pip install tox script: diff --git a/CHANGELOG.md b/CHANGELOG.md index d82de744..eb93ab14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Note that in line with [Django REST Framework policy](http://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] + +### Added + +* Added support for Python 3.8 +* Added support for Django REST framework 3.11 +* Added support for Django 3.0 + ## [3.0.0] - 2019-10-14 This release is not backwards compatible. For easy migration best upgrade first to version diff --git a/README.rst b/README.rst index aaa6678c..5541eba9 100644 --- a/README.rst +++ b/README.rst @@ -87,9 +87,9 @@ As a Django REST Framework JSON API (short DJA) we are trying to address followi Requirements ------------ -1. Python (3.5, 3.6, 3.7) -2. Django (1.11, 2.1, 2.2) -3. Django REST Framework (3.10) +1. Python (3.5, 3.6, 3.7, 3.8) +2. Django (1.11, 2.1, 2.2, 3.0) +3. Django REST Framework (3.10, 3.11) 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 6f5d60ab..00d77c61 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -51,9 +51,9 @@ like the following: ## Requirements -1. Python (3.5, 3.6, 3.7) -2. Django (1.11, 2.1, 2.2) -3. Django REST Framework (3.10) +1. Python (3.5, 3.6, 3.7, 3.8) +2. Django (1.11, 2.1, 2.2, 3.0) +3. Django REST Framework (3.10, 3.11) We **highly** recommend and only officially support the latest patch release of each Python, Django and REST Framework series. diff --git a/example/api/resources/identity.py b/example/api/resources/identity.py index 5f5a71f2..6785e5d9 100644 --- a/example/api/resources/identity.py +++ b/example/api/resources/identity.py @@ -23,8 +23,8 @@ def posts(self, request): posts = [{'id': 1, 'title': 'Test Blog Post'}] data = { - encoding.force_text('identities'): IdentitySerializer(identities, many=True).data, - encoding.force_text('posts'): PostSerializer(posts, many=True).data, + encoding.force_str('identities'): IdentitySerializer(identities, many=True).data, + encoding.force_str('posts'): PostSerializer(posts, many=True).data, } return Response(utils.format_field_names(data, format_type='camelize')) diff --git a/example/tests/test_format_keys.py b/example/tests/test_format_keys.py index bfa61b54..ba3f4920 100644 --- a/example/tests/test_format_keys.py +++ b/example/tests/test_format_keys.py @@ -28,7 +28,7 @@ def test_camelization(self): 'data': [ { 'type': 'users', - 'id': encoding.force_text(user.pk), + 'id': encoding.force_str(user.pk), 'attributes': { 'firstName': user.first_name, 'lastName': user.last_name, diff --git a/example/tests/test_model_viewsets.py b/example/tests/test_model_viewsets.py index e1a16206..820c8931 100644 --- a/example/tests/test_model_viewsets.py +++ b/example/tests/test_model_viewsets.py @@ -34,7 +34,7 @@ def test_key_in_list_result(self): 'data': [ { 'type': 'users', - 'id': encoding.force_text(user.pk), + 'id': encoding.force_str(user.pk), 'attributes': { 'first-name': user.first_name, 'last-name': user.last_name, @@ -72,7 +72,7 @@ def test_page_two_in_list_result(self): 'data': [ { 'type': 'users', - 'id': encoding.force_text(user.pk), + 'id': encoding.force_str(user.pk), 'attributes': { 'first-name': user.first_name, 'last-name': user.last_name, @@ -112,7 +112,7 @@ def test_page_range_in_list_result(self): 'data': [ { 'type': 'users', - 'id': encoding.force_text(users[0].pk), + 'id': encoding.force_str(users[0].pk), 'attributes': { 'first-name': users[0].first_name, 'last-name': users[0].last_name, @@ -121,7 +121,7 @@ def test_page_range_in_list_result(self): }, { 'type': 'users', - 'id': encoding.force_text(users[1].pk), + 'id': encoding.force_str(users[1].pk), 'attributes': { 'first-name': users[1].first_name, 'last-name': users[1].last_name, @@ -157,7 +157,7 @@ def test_key_in_detail_result(self): expected = { 'data': { 'type': 'users', - 'id': encoding.force_text(self.miles.pk), + 'id': encoding.force_str(self.miles.pk), 'attributes': { 'first-name': self.miles.first_name, 'last-name': self.miles.last_name, @@ -193,7 +193,7 @@ def test_key_in_post(self): data = { 'data': { 'type': 'users', - 'id': encoding.force_text(self.miles.pk), + 'id': encoding.force_str(self.miles.pk), 'attributes': { 'first-name': self.miles.first_name, 'last-name': self.miles.last_name, diff --git a/example/tests/test_sideload_resources.py b/example/tests/test_sideload_resources.py index 4c9c1525..69641af7 100644 --- a/example/tests/test_sideload_resources.py +++ b/example/tests/test_sideload_resources.py @@ -25,5 +25,5 @@ def test_get_sideloaded_data(self): self.assertEqual( sorted(content.keys()), - [encoding.force_text('identities'), - encoding.force_text('posts')]) + [encoding.force_str('identities'), + encoding.force_str('posts')]) diff --git a/rest_framework_json_api/exceptions.py b/rest_framework_json_api/exceptions.py index 38ff527b..938a0c77 100644 --- a/rest_framework_json_api/exceptions.py +++ b/rest_framework_json_api/exceptions.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions, status from rest_framework_json_api import utils diff --git a/rest_framework_json_api/metadata.py b/rest_framework_json_api/metadata.py index ed0a86ce..ef3356fe 100644 --- a/rest_framework_json_api/metadata.py +++ b/rest_framework_json_api/metadata.py @@ -1,7 +1,7 @@ from collections import OrderedDict from django.db.models.fields import related -from django.utils.encoding import force_text +from django.utils.encoding import force_str from rest_framework import serializers from rest_framework.metadata import SimpleMetadata from rest_framework.settings import api_settings @@ -123,7 +123,7 @@ def get_field_info(self, field): for attr in attrs: value = getattr(field, attr, None) if value is not None and value != '': - field_info[attr] = force_text(value, strings_only=True) + field_info[attr] = force_str(value, strings_only=True) if getattr(field, 'child', None): field_info['child'] = self.get_field_info(field.child) @@ -138,7 +138,7 @@ def get_field_info(self, field): field_info['choices'] = [ { 'value': choice_value, - 'display_name': force_text(choice_name, strings_only=True) + 'display_name': force_str(choice_name, strings_only=True) } for choice_value, choice_name in field.choices.items() ] diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 54226e5a..9fbfb98f 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -5,7 +5,7 @@ import inflection from django.core.exceptions import ImproperlyConfigured from django.urls import NoReverseMatch -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework.fields import MISSING_ERROR_MESSAGE, SkipField from rest_framework.relations import MANY_RELATION_KWARGS from rest_framework.relations import ManyRelatedField as DRFManyRelatedField diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index ced826b0..8da333ed 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -129,7 +129,7 @@ def extract_relationships(cls, fields, resource, resource_instance): relation_data.append( OrderedDict([ ('type', relation_type), - ('id', encoding.force_text(related_object.pk)) + ('id', encoding.force_str(related_object.pk)) ]) ) @@ -168,7 +168,7 @@ def extract_relationships(cls, fields, resource, resource_instance): relation_data = { 'data': ( OrderedDict([ - ('type', relation_type), ('id', encoding.force_text(relation_id)) + ('type', relation_type), ('id', encoding.force_str(relation_id)) ]) if relation_id is not None else None) } @@ -231,7 +231,7 @@ def extract_relationships(cls, fields, resource, resource_instance): relation_data.append(OrderedDict([ ('type', nested_resource_instance_type), - ('id', encoding.force_text(nested_resource_instance.pk)) + ('id', encoding.force_str(nested_resource_instance.pk)) ])) data.update({ field_name: { @@ -264,7 +264,7 @@ def extract_relationships(cls, fields, resource, resource_instance): relation_data.append(OrderedDict([ ('type', nested_resource_instance_type), - ('id', encoding.force_text(nested_resource_instance.pk)) + ('id', encoding.force_str(nested_resource_instance.pk)) ])) data.update({field_name: {'data': relation_data}}) @@ -287,7 +287,7 @@ def extract_relationships(cls, fields, resource, resource_instance): 'data': ( OrderedDict([ ('type', relation_type), - ('id', encoding.force_text(relation_instance_id)) + ('id', encoding.force_str(relation_instance_id)) ]) if resource.get(field_name) else None) } }) @@ -486,7 +486,7 @@ def build_json_resource_obj(cls, fields, resource, resource_instance, resource_n resource_name = utils.get_resource_type_from_instance(resource_instance) resource_data = [ ('type', resource_name), - ('id', encoding.force_text(resource_instance.pk) if resource_instance else None), + ('id', encoding.force_str(resource_instance.pk) if resource_instance else None), ('attributes', cls.extract_attributes(fields, resource)), ] relationships = cls.extract_relationships(fields, resource, resource_instance) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index be0dcace..5f7a31e8 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,7 +1,7 @@ import inflection from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import ParseError from rest_framework.serializers import * # noqa: F403 diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index dd92ba23..90b9116b 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -13,7 +13,7 @@ from django.http import Http404 from django.utils import encoding from django.utils.module_loading import import_string as import_class_from_dotted_path -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions from rest_framework.exceptions import APIException @@ -341,7 +341,7 @@ def format_drf_errors(response, context, exc): def format_error_object(message, pointer, response): error_obj = { 'detail': message, - 'status': encoding.force_text(response.status_code), + 'status': encoding.force_str(response.status_code), } if pointer is not None: error_obj['source'] = { diff --git a/tox.ini b/tox.ini index 13992946..04b970ac 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,9 @@ [tox] envlist = - py{35,36}-django111-drf{310,master}, - py{35,36,37}-django{21,22}-drf{310,master}, + py{35,36}-django{111}-drf{310,311,master}, + py{35,36,37}-django{21,22}-drf{310,311,master}, + py38-django22-drf{311,master}, + py{36,37,38}-django{30}-drf{311,master}, lint,docs [testenv] @@ -9,7 +11,9 @@ deps = django111: Django>=1.11,<1.12 django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 + django30: Django>=3.0,<3.1 drf310: djangorestframework>=3.10.2,<3.11 + drf311: djangorestframework>=3.11,<3.12 drfmaster: https://github.com/encode/django-rest-framework/archive/master.zip -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 0c587ac335890ad1f17038e4792f46e2eb89ba9b Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Fri, 17 Jan 2020 19:06:38 +0400 Subject: [PATCH 02/12] Adjust invalid field_name_mapping example for related fieds (#756) --- docs/usage.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 52d73b98..fc6aa6ad 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -682,11 +682,10 @@ Also we can override `related_field` in the url. Let's say we want the url to be dict to the class: ```python field_name_mapping = { - 'line_items': 'order_items' + 'order_items': 'line_items' } ``` - ### Working with polymorphic resources Polymorphic resources allow you to use specialized subclasses without requiring From e3ae420303b3f8fd7e8a03081f2e3354be638823 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Fri, 17 Jan 2020 20:51:56 +0400 Subject: [PATCH 03/12] Bundle PR on a bi-weekly basis (#757) Currently pyup causes a lot of noise so when looking at the closed PR the real PRs can be easily overlooked. As we only have test dependencies simply updating on a bi-weekly basis should be sufficient. --- .pyup.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pyup.yml b/.pyup.yml index c4b52e37..7ee57975 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -1,4 +1,5 @@ search: False +schedule: "every two weeks" requirements: - requirements/requirements-codestyle.txt: update: all From 96aaa138a9e2970d1df94fee336ca38836fb696f Mon Sep 17 00:00:00 2001 From: Boris Pleshakov Date: Thu, 23 Jan 2020 20:28:12 +0300 Subject: [PATCH 04/12] Enforce conflict response when requested PATCH resource object is unequal to endpoint (#763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A server MUST return 409 Conflict when processing a PATCH request in which the resource object’s type and id do not match the server’s endpoint. --- AUTHORS | 1 + CHANGELOG.md | 4 ++++ example/tests/test_model_viewsets.py | 18 ++++++++++++++++++ rest_framework_json_api/parsers.py | 11 +++++++++++ 4 files changed, 34 insertions(+) diff --git a/AUTHORS b/AUTHORS index 0e2e1902..1619377a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,6 +2,7 @@ Adam Wróbel Adam Ziolkowski Alan Crosswell Anton Shutik +Boris Pleshakov Christian Zosel David Vogt Greg Aker diff --git a/CHANGELOG.md b/CHANGELOG.md index eb93ab14..57cef3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ any parts of the framework not mentioned in the documentation should generally b * Added support for Django REST framework 3.11 * Added support for Django 3.0 +### Fixed + +* Ensure that `409 Conflict` is returned when processing a `PATCH` request in which the resource object’s type and id do not match the server’s endpoint properly as outlined in [JSON:API](https://jsonapi.org/format/#crud-updating-responses-409) spec. + ## [3.0.0] - 2019-10-14 This release is not backwards compatible. For easy migration best upgrade first to version diff --git a/example/tests/test_model_viewsets.py b/example/tests/test_model_viewsets.py index 820c8931..1ce8336d 100644 --- a/example/tests/test_model_viewsets.py +++ b/example/tests/test_model_viewsets.py @@ -185,6 +185,24 @@ def test_patch_requires_id(self): self.assertEqual(response.status_code, 400) + def test_patch_requires_correct_id(self): + """ + Verify that 'id' is the same then in url + """ + data = { + 'data': { + 'type': 'users', + 'id': self.miles.pk + 1, + 'attributes': { + 'first-name': 'DifferentName' + } + } + } + + response = self.client.patch(self.detail_url, data=data) + + self.assertEqual(response.status_code, 409) + def test_key_in_post(self): """ Ensure a key is in the post. diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 2742302c..5b024499 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -139,6 +139,17 @@ def parse(self, stream, media_type=None, parser_context=None): if not data.get('id') and request.method in ('PATCH', 'PUT'): raise ParseError("The resource identifier object must contain an 'id' member") + if request.method in ('PATCH', 'PUT'): + lookup_url_kwarg = view.lookup_url_kwarg or view.lookup_field + if str(data.get('id')) != str(view.kwargs[lookup_url_kwarg]): + raise exceptions.Conflict( + "The resource object's id ({data_id}) does not match url's " + "lookup id ({url_id})".format( + data_id=data.get('id'), + url_id=view.kwargs[view.lookup_field] + ) + ) + # Construct the return data serializer_class = getattr(view, 'serializer_class', None) parsed_data = {'id': data.get('id')} if 'id' in data else {} From d5d72153b391d4f575c99b1dcd6dd2ac0c8f30a1 Mon Sep 17 00:00:00 2001 From: Boris Pleshakov Date: Thu, 23 Jan 2020 21:01:23 +0300 Subject: [PATCH 05/12] Properly return parser error when primary data is of invalid type (#762) If you submit a list with one object, for example, DJA will respond you with unobvious errors. --- CHANGELOG.md | 1 + example/tests/test_parsers.py | 17 +++++++++++++++++ rest_framework_json_api/parsers.py | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57cef3d5..e9b41f3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ any parts of the framework not mentioned in the documentation should generally b ### Fixed * Ensure that `409 Conflict` is returned when processing a `PATCH` request in which the resource object’s type and id do not match the server’s endpoint properly as outlined in [JSON:API](https://jsonapi.org/format/#crud-updating-responses-409) spec. +* Properly return parser error when primary data is of invalid type ## [3.0.0] - 2019-10-14 diff --git a/example/tests/test_parsers.py b/example/tests/test_parsers.py index 41ad05ac..6ff2cfa7 100644 --- a/example/tests/test_parsers.py +++ b/example/tests/test_parsers.py @@ -52,3 +52,20 @@ def test_parse_invalid_data(self): with self.assertRaises(ParseError): parser.parse(stream, None, self.parser_context) + + def test_parse_invalid_data_key(self): + parser = JSONParser() + + string = json.dumps({ + 'data': [{ + 'id': 123, + 'type': 'Blog', + 'attributes': { + 'json-value': {'JsonKey': 'JsonValue'} + }, + }] + }) + stream = BytesIO(string.encode('utf-8')) + + with self.assertRaises(ParseError): + parser.parse(stream, None, self.parser_context) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 5b024499..7a940b6c 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -116,6 +116,10 @@ def parse(self, stream, media_type=None, parser_context=None): request = parser_context.get('request') + # Sanity check + if not isinstance(data, dict): + raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') + # Check for inconsistencies if request.method in ('PUT', 'POST', 'PATCH'): resource_name = utils.get_resource_name( From 47fc77f91dcb8186eef7a2ba785e5c343bfefc7c Mon Sep 17 00:00:00 2001 From: Joseba Mendivil Date: Thu, 30 Jan 2020 17:27:25 +0000 Subject: [PATCH 06/12] Pass on instance when using polymorphic serializers (#764) Fixes #759 Pass self.instance as the first parameter when initializing the child serializer from a polymorphic serialiser. This does not affect basic functionality, but makes the child serializer instance consistent with its usage of instance and data, which can fix potential issues when extending serilaizers that are part of a polymorphic serializer. --- CHANGELOG.md | 1 + example/tests/test_serializers.py | 58 +++++++++++++++++++++++++- rest_framework_json_api/serializers.py | 2 +- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b41f3b..1318b9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ any parts of the framework not mentioned in the documentation should generally b * Ensure that `409 Conflict` is returned when processing a `PATCH` request in which the resource object’s type and id do not match the server’s endpoint properly as outlined in [JSON:API](https://jsonapi.org/format/#crud-updating-responses-409) spec. * Properly return parser error when primary data is of invalid type +* Pass instance to child serializer when `PolymorphicModelSerializer` inits it in `to_internal_value` ## [3.0.0] - 2019-10-14 diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py index e1296e2f..50a84f4d 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -7,15 +7,21 @@ from rest_framework.request import Request from rest_framework.test import APIRequestFactory +from example.factories import ArtProjectFactory from rest_framework_json_api.serializers import ( DateField, ModelSerializer, - ResourceIdentifierObjectSerializer + ResourceIdentifierObjectSerializer, + empty, ) from rest_framework_json_api.utils import format_resource_type from example.models import Author, Blog, Entry -from example.serializers import BlogSerializer +from example.serializers import ( + BlogSerializer, + ProjectSerializer, + ArtProjectSerializer, +) request_factory = APIRequestFactory() pytestmark = pytest.mark.django_db @@ -193,3 +199,51 @@ def test_model_serializer_with_implicit_fields(self, comment, client): assert response.status_code == 200 assert expected == response.json() + + +class TestPolymorphicModelSerializer(TestCase): + def setUp(self): + self.project = ArtProjectFactory.create() + self.child_init_args = {} + + # Override `__init__` with our own method + def overridden_init(child_self, instance=None, data=empty, **kwargs): + """ + Override `ArtProjectSerializer.__init__` with the same signature that + `BaseSerializer.__init__` has to assert that it receives the parameters + that `BaseSerializer` expects + """ + self.child_init_args = dict(instance=instance, data=data, **kwargs) + + return super(ArtProjectSerializer, child_self).__init__( + instance, data, **kwargs + ) + + self.child_serializer_init = ArtProjectSerializer.__init__ + ArtProjectSerializer.__init__ = overridden_init + + def tearDown(self): + # Restore original init to avoid affecting other tests + ArtProjectSerializer.__init__ = self.child_serializer_init + + def test_polymorphic_model_serializer_passes_instance_to_child(self): + """ + Ensure that `PolymorphicModelSerializer` is passing the instance to the + child serializer when initializing them + """ + # Initialize a serializer that would partially update a model instance + initial_data = {"artist": "Mark Bishop", "type": "artProjects"} + parent_serializer = ProjectSerializer( + instance=self.project, data=initial_data, partial=True + ) + + parent_serializer.is_valid(raise_exception=True) + + # Run save to force `ProjectSerializer` to init `ArtProjectSerializer` + parent_serializer.save() + + # Assert that child init received the expected arguments + assert self.child_init_args["instance"] == self.project + assert self.child_init_args["data"] == initial_data + assert self.child_init_args["partial"] == parent_serializer.partial + assert self.child_init_args["context"] == parent_serializer.context diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 5f7a31e8..56688bd9 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -352,5 +352,5 @@ def to_internal_value(self, data): expected_types=', '.join(expected_types), received_type=received_type)) serializer_class = self.get_polymorphic_serializer_for_type(received_type) self.__class__ = serializer_class - return serializer_class(data, context=self.context, + return serializer_class(self.instance, data, context=self.context, partial=self.partial).to_internal_value(data) From c8c5cf11e7a28a536628ffdab38f57f931e004a8 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Mon, 3 Feb 2020 18:51:19 +0200 Subject: [PATCH 07/12] Scheduled biweekly dependency update for week 05 (#765) * Update django-debug-toolbar from 2.1 to 2.2 * Update faker from 3.0.0 to 4.0.0 * Update pytest from 5.3.1 to 5.3.5 * Update pytest-django from 3.7.0 to 3.8.0 --- requirements/requirements-testing.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 5a5199af..736394eb 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,7 +1,7 @@ -django-debug-toolbar==2.1 +django-debug-toolbar==2.2 factory-boy==2.12.0 -Faker==3.0.0 -pytest==5.3.1 +Faker==4.0.0 +pytest==5.3.5 pytest-cov==2.8.1 -pytest-django==3.7.0 +pytest-django==3.8.0 pytest-factoryboy==2.0.3 From 9d11e8a4400281cc90dd5efbd9a20e15d1666a5c Mon Sep 17 00:00:00 2001 From: Felix Viernickel <57354511+4nickel@users.noreply.github.com> Date: Fri, 7 Feb 2020 19:38:11 +0100 Subject: [PATCH 08/12] Fix related resource on inherited polymorphic model (#767) Fixes #621 When adding a ResourceRelatedField to a serializer that is included in the polymorphic_serializers list of a polymorphic serializer, the parent_model is not correctly resolved in get_related_resource_type: instead of resolving to the inherited type, the base type is resolved. This can cause an AttributeError if the field in question is not present on the base model. We ran into this, and then found the closed and seemingly abandoned issue referenced above. This PR implements the fix suggested by the original author, who deserves all the credit. The example code has been expanded to trigger the error when running the test-suite. Thanks in advance for your time and please let us know if you have any suggestions for improvement. --- AUTHORS | 1 + CHANGELOG.md | 1 + example/migrations/0008_labresults.py | 23 +++++++++++++++++++++++ example/models.py | 7 +++++++ example/serializers.py | 10 ++++++++++ rest_framework_json_api/utils.py | 6 +++++- 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 example/migrations/0008_labresults.py diff --git a/AUTHORS b/AUTHORS index 1619377a..8ecb85d2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,3 +27,4 @@ Stas S. Nathanael Gordon Charlie Allatson Joseba Mendivil +Felix Viernickel diff --git a/CHANGELOG.md b/CHANGELOG.md index 1318b9ba..7b2b34f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ any parts of the framework not mentioned in the documentation should generally b * Ensure that `409 Conflict` is returned when processing a `PATCH` request in which the resource object’s type and id do not match the server’s endpoint properly as outlined in [JSON:API](https://jsonapi.org/format/#crud-updating-responses-409) spec. * Properly return parser error when primary data is of invalid type * Pass instance to child serializer when `PolymorphicModelSerializer` inits it in `to_internal_value` +* Handle serialization of related resources on inherited polymorphic models that are absent on the base model ## [3.0.0] - 2019-10-14 diff --git a/example/migrations/0008_labresults.py b/example/migrations/0008_labresults.py new file mode 100644 index 00000000..89323d77 --- /dev/null +++ b/example/migrations/0008_labresults.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-02-06 10:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('example', '0007_artproject_description'), + ] + + operations = [ + migrations.CreateModel( + name='LabResults', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('measurements', models.TextField()), + ('research_project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lab_results', to='example.ResearchProject')), + ], + ), + ] diff --git a/example/models.py b/example/models.py index 601e0788..4df4dc27 100644 --- a/example/models.py +++ b/example/models.py @@ -151,6 +151,13 @@ class ResearchProject(Project): supervisor = models.CharField(max_length=30) +class LabResults(models.Model): + research_project = models.ForeignKey( + ResearchProject, related_name='lab_results', on_delete=models.CASCADE) + date = models.DateField() + measurements = models.TextField() + + class Company(models.Model): name = models.CharField(max_length=100) current_project = models.ForeignKey( diff --git a/example/serializers.py b/example/serializers.py index 9ed60e90..cc24efb0 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -15,6 +15,7 @@ Comment, Company, Entry, + LabResults, Project, ProjectType, ResearchProject, @@ -303,11 +304,20 @@ class Meta: class ResearchProjectSerializer(BaseProjectSerializer): + # testing exclusive related field on inherited polymorphic model + lab_results = relations.ResourceRelatedField(many=True, read_only=True) + class Meta: model = ResearchProject exclude = ('polymorphic_ctype',) +class LabResultsSerializer(serializers.ModelSerializer): + class Meta: + model = LabResults + fields = ('date', 'measurements') + + class ProjectSerializer(serializers.PolymorphicModelSerializer): included_serializers = { 'project_type': ProjectTypeSerializer, diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 90b9116b..b3932651 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -143,6 +143,7 @@ def format_resource_type(value, format_type=None, pluralize=None): def get_related_resource_type(relation): + from rest_framework_json_api.serializers import PolymorphicModelSerializer try: return get_resource_type_from_serializer(relation) except AttributeError: @@ -165,7 +166,10 @@ def get_related_resource_type(relation): else: parent_serializer = relation.parent parent_model = None - if hasattr(parent_serializer, 'Meta'): + if isinstance(parent_serializer, PolymorphicModelSerializer): + parent_model = parent_serializer.get_polymorphic_serializer_for_instance( + parent_serializer.instance).Meta.model + elif hasattr(parent_serializer, 'Meta'): parent_model = getattr(parent_serializer.Meta, 'model', None) elif hasattr(parent_serializer, 'parent') and hasattr(parent_serializer.parent, 'Meta'): parent_model = getattr(parent_serializer.parent.Meta, 'model', None) From 8a02676127952508e2940109f41d03100a23b983 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Tue, 11 Feb 2020 18:03:15 +0400 Subject: [PATCH 09/12] Release version 3.1.0 (#768) --- CHANGELOG.md | 8 ++++---- rest_framework_json_api/__init__.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b2b34f9..f2dc90b7 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](http://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] +## [3.1.0] - 2020-02-08 ### Added @@ -18,10 +18,10 @@ any parts of the framework not mentioned in the documentation should generally b ### Fixed -* Ensure that `409 Conflict` is returned when processing a `PATCH` request in which the resource object’s type and id do not match the server’s endpoint properly as outlined in [JSON:API](https://jsonapi.org/format/#crud-updating-responses-409) spec. +* Ensured that `409 Conflict` is returned when processing a `PATCH` request in which the resource object’s type and id do not match the server’s endpoint as outlined in [JSON:API](https://jsonapi.org/format/#crud-updating-responses-409) spec. * Properly return parser error when primary data is of invalid type -* Pass instance to child serializer when `PolymorphicModelSerializer` inits it in `to_internal_value` -* Handle serialization of related resources on inherited polymorphic models that are absent on the base model +* Pass instance to child serializers when using `PolymorphicModelSerializer` +* Properly resolve related resource type when using `PolymorphicModelSerializer` ## [3.0.0] - 2019-10-14 diff --git a/rest_framework_json_api/__init__.py b/rest_framework_json_api/__init__.py index 619fd5ad..a15ece29 100644 --- a/rest_framework_json_api/__init__.py +++ b/rest_framework_json_api/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __title__ = 'djangorestframework-jsonapi' -__version__ = '3.0.0' +__version__ = '3.1.0' __author__ = '' __license__ = 'BSD' __copyright__ = '' From 27b7b9fa1ffdf28577a899317347474ecfcb18d4 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 18 Feb 2020 18:47:19 +0200 Subject: [PATCH 10/12] Update sphinx from 2.3.1 to 2.4.1 (#770) --- requirements/requirements-documentation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index d8e46cd0..5756f88a 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,3 +1,3 @@ recommonmark==0.6.0 -Sphinx==2.3.1 +Sphinx==2.4.1 sphinx_rtd_theme==0.4.3 From 46967c5478293ba42faa2e670464c97a6debf178 Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Sun, 8 Mar 2020 19:55:59 +0200 Subject: [PATCH 11/12] Scheduled biweekly dependency update for week 09 (#773) * Update sphinx from 2.4.1 to 2.4.3 * Update faker from 4.0.0 to 4.0.1 --- requirements/requirements-documentation.txt | 2 +- requirements/requirements-testing.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index 5756f88a..b2d3f3ec 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,3 +1,3 @@ recommonmark==0.6.0 -Sphinx==2.4.1 +Sphinx==2.4.3 sphinx_rtd_theme==0.4.3 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 736394eb..914f25ea 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,6 +1,6 @@ django-debug-toolbar==2.2 factory-boy==2.12.0 -Faker==4.0.0 +Faker==4.0.1 pytest==5.3.5 pytest-cov==2.8.1 pytest-django==3.8.0 From 5f19ef0b642ae5d525396dc89fb5cfd9251f02af Mon Sep 17 00:00:00 2001 From: "pyup.io bot" Date: Tue, 17 Mar 2020 09:24:25 +0200 Subject: [PATCH 12/12] Scheduled biweekly dependency update for week 11 (#774) * Update sphinx from 2.4.3 to 2.4.4 * Update faker from 4.0.1 to 4.0.2 * Update pytest from 5.3.5 to 5.4.1 --- requirements/requirements-documentation.txt | 2 +- requirements/requirements-testing.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index b2d3f3ec..a752286c 100644 --- a/requirements/requirements-documentation.txt +++ b/requirements/requirements-documentation.txt @@ -1,3 +1,3 @@ recommonmark==0.6.0 -Sphinx==2.4.3 +Sphinx==2.4.4 sphinx_rtd_theme==0.4.3 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 914f25ea..3256a1b6 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,7 +1,7 @@ django-debug-toolbar==2.2 factory-boy==2.12.0 -Faker==4.0.1 -pytest==5.3.5 +Faker==4.0.2 +pytest==5.4.1 pytest-cov==2.8.1 pytest-django==3.8.0 pytest-factoryboy==2.0.3