diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c157642..4540ed7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v2.3.0 * Fix for apps that don't use `django.contrib.contenttypes`. * Fix `resource_name` support for POST requests and nested serializers * Enforcing flake8 linting +* Added nested included serializer support for remapped relations v2.2.0 diff --git a/example/serializers.py b/example/serializers.py index e8ee53ca..29202ce5 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -105,10 +105,25 @@ class Meta: fields = ('name', 'email', 'bio') +class WriterSerializer(serializers.ModelSerializer): + included_serializers = { + 'bio': AuthorBioSerializer + } + + class Meta: + model = Author + fields = ('name', 'email', 'bio') + resource_name = 'writers' + + class CommentSerializer(serializers.ModelSerializer): + # testing remapping of related name + writer = relations.ResourceRelatedField(source='author', read_only=True) + included_serializers = { 'entry': EntrySerializer, - 'author': AuthorSerializer + 'author': AuthorSerializer, + 'writer': WriterSerializer } class Meta: diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index 55f86986..a75310bc 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -82,14 +82,15 @@ def test_missing_field_not_included(author_bio_factory, author_factory, client): def test_deep_included_data_on_list(multiple_entries, client): response = client.get(reverse("entry-list") + '?include=comments,comments.author,' - 'comments.author.bio&page_size=5') + 'comments.author.bio,comments.writer&page_size=5') included = load_json(response.content).get('included') assert len(load_json(response.content)['data']) == len(multiple_entries), ( 'Incorrect entry count' ) assert [x.get('type') for x in included] == [ - 'authorBios', 'authorBios', 'authors', 'authors', 'comments', 'comments' + 'authorBios', 'authorBios', 'authors', 'authors', + 'comments', 'comments', 'writers', 'writers' ], 'List included types are incorrect' comment_count = len([resource for resource in included if resource["type"] == "comments"]) @@ -106,6 +107,13 @@ def test_deep_included_data_on_list(multiple_entries, client): author__bio__isnull=False).count() for entry in multiple_entries]) assert author_bio_count == expected_author_bio_count, 'List author bio count is incorrect' + writer_count = len( + [resource for resource in included if resource["type"] == "writers"] + ) + expected_writer_count = sum( + [entry.comments.filter(author__isnull=False).count() for entry in multiple_entries]) + assert writer_count == expected_writer_count, 'List writer count is incorrect' + # Also include entry authors response = client.get(reverse("entry-list") + '?include=authors,comments,comments.author,' 'comments.author.bio&page_size=5') diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py index 74b7d860..6360b583 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -102,6 +102,12 @@ def test_model_serializer_with_implicit_fields(self, comment, client): "id": str(comment.author.pk) } }, + "writer": { + "data": { + "type": "writers", + "id": str(comment.author.pk) + } + }, } } } diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 61a9238c..b3da0c54 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -272,6 +272,36 @@ def extract_relationships(cls, fields, resource, resource_instance): return utils.format_keys(data) + @classmethod + def extract_relation_instance(cls, field_name, field, resource_instance, serializer): + """ + Determines what instance represents given relation and extracts it. + + Relation instance is determined by given field_name or source configured on + field. As fallback is a serializer method called with name of field's source. + """ + relation_instance = None + + try: + relation_instance = getattr(resource_instance, field_name) + except AttributeError: + try: + # For ManyRelatedFields if `related_name` is not set + # we need to access `foo_set` from `source` + relation_instance = getattr(resource_instance, field.child_relation.source) + except AttributeError: + if hasattr(serializer, field.source): + serializer_method = getattr(serializer, field.source) + relation_instance = serializer_method(resource_instance) + else: + # case when source is a simple remap on resource_instance + try: + relation_instance = getattr(resource_instance, field.source) + except AttributeError: + pass + + return relation_instance + @classmethod def extract_included(cls, fields, resource, resource_instance, included_resources): # this function may be called with an empty record (example: Browsable Interface) @@ -304,19 +334,9 @@ def extract_included(cls, fields, resource, resource_instance, included_resource if field_name not in [node.split('.')[0] for node in included_resources]: continue - try: - relation_instance = getattr(resource_instance, field_name) - except AttributeError: - try: - # For ManyRelatedFields if `related_name` is not set we need to access `foo_set` - # from `source` - relation_instance = getattr(resource_instance, field.child_relation.source) - except AttributeError: - if not hasattr(current_serializer, field.source): - continue - serializer_method = getattr(current_serializer, field.source) - relation_instance = serializer_method(resource_instance) - + relation_instance = cls.extract_relation_instance( + field_name, field, resource_instance, current_serializer + ) if isinstance(relation_instance, Manager): relation_instance = relation_instance.all()