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 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/AUTHORS b/AUTHORS index 0e2e1902..8ecb85d2 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 @@ -26,3 +27,4 @@ Stas S. Nathanael Gordon Charlie Allatson Joseba Mendivil +Felix Viernickel diff --git a/CHANGELOG.md b/CHANGELOG.md index d82de744..f2dc90b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +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](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. +## [3.1.0] - 2020-02-08 + +### Added + +* Added support for Python 3.8 +* Added support for Django REST framework 3.11 +* Added support for Django 3.0 + +### Fixed + +* 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 serializers when using `PolymorphicModelSerializer` +* Properly resolve related resource type when using `PolymorphicModelSerializer` + ## [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/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 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/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/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..1ce8336d 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, @@ -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. @@ -193,7 +211,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_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/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/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/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt index d8e46cd0..a752286c 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.4 sphinx_rtd_theme==0.4.3 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 5a5199af..3256a1b6 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.2 +pytest==5.4.1 pytest-cov==2.8.1 -pytest-django==3.7.0 +pytest-django==3.8.0 pytest-factoryboy==2.0.3 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__ = '' 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/parsers.py b/rest_framework_json_api/parsers.py index 2742302c..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( @@ -139,6 +143,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 {} 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..56688bd9 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 @@ -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) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index dd92ba23..b3932651 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 @@ -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) @@ -341,7 +345,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