From ef936f427f1a7021481b1908a90c9671ae375326 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Thu, 16 May 2019 15:27:27 +0300 Subject: [PATCH 1/4] Use DRF code to fetch related instance --- rest_framework_json_api/renderers.py | 32 +++++++++------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 0023399a..65081c83 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -8,6 +8,8 @@ from django.db.models import Manager from django.utils import encoding, six from rest_framework import relations, renderers +from rest_framework.fields import SkipField, get_attribute +from rest_framework.relations import PKOnlyObject from rest_framework.serializers import BaseSerializer, ListSerializer, Serializer from rest_framework.settings import api_settings @@ -301,30 +303,16 @@ def extract_relation_instance(cls, field_name, field, resource_instance, seriali """ 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 is determined exactly same way as it determined + in parent serializer """ - 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 + res = field.get_attribute(resource_instance) + if isinstance(res, PKOnlyObject): + return get_attribute(resource_instance, field.source_attrs) + return res + except SkipField: + return None @classmethod def extract_included(cls, fields, resource, resource_instance, included_resources, From 9c2afb048ceb0d826570b1bbc0d44bc5e9546e60 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Tue, 28 May 2019 16:05:59 +0300 Subject: [PATCH 2/4] Added test, CHANGELOG --- CHANGELOG.md | 1 + example/tests/test_renderers.py | 49 ++++++++++++++++++++++++++++ rest_framework_json_api/renderers.py | 4 +-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 example/tests/test_renderers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d5e074..18bb1db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ any parts of the framework not mentioned in the documentation should generally b * Don't swallow `filter[]` params when there are several * Fix DeprecationWarning regarding collections.abc import in Python 3.7 * Allow OPTIONS request to be used on RelationshipView +* Use DRF code when extracting relation instance. [PR](https://github.com/django-json-api/django-rest-framework-json-api/pull/632) ### Deprecated diff --git a/example/tests/test_renderers.py b/example/tests/test_renderers.py new file mode 100644 index 00000000..ab59744a --- /dev/null +++ b/example/tests/test_renderers.py @@ -0,0 +1,49 @@ +from django.test import TestCase + +from example.models import Blog, Entry, Comment +from example.serializers import EntrySerializer +from rest_framework_json_api import serializers +from rest_framework_json_api.renderers import JSONRenderer + + +class TestJSONRenderer(TestCase): + + def setUp(self): + pass + + def test_extract_relation_instance(self): + + class NewEntySerializer(EntrySerializer): + blog_name = serializers.ReadOnlyField(source='blog.name') + + class Meta(EntrySerializer.Meta): + fields = EntrySerializer.Meta.fields + ('blog_name',) + + blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") + entry = Entry.objects.create( + blog=blog, + headline='headline', + body_text='body_text', + ) + suggested_enty = Entry.objects.create( + blog=blog, + headline='suggested headline', + body_text='suggested body_text', + ) + Comment.objects.create(entry=entry, body='foo') + serializer = NewEntySerializer(instance=entry) + + got = JSONRenderer.extract_relation_instance( + field=serializer.fields['featured'], resource_instance=entry + ) + assert got == suggested_enty + + got = JSONRenderer.extract_relation_instance( + field=serializer.fields['comments'], resource_instance=entry + ) + assert str(got.query) == str(Comment.objects.filter(entry=entry).query) + + got = JSONRenderer.extract_relation_instance( + field=serializer.fields['blog_name'], resource_instance=entry + ) + assert got == blog.name diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 65081c83..2d1430a7 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -299,7 +299,7 @@ def extract_relationships(cls, fields, resource, resource_instance): return utils._format_object(data) @classmethod - def extract_relation_instance(cls, field_name, field, resource_instance, serializer): + def extract_relation_instance(cls, field, resource_instance): """ Determines what instance represents given relation and extracts it. @@ -351,7 +351,7 @@ def extract_included(cls, fields, resource, resource_instance, included_resource continue relation_instance = cls.extract_relation_instance( - field_name, field, resource_instance, current_serializer + field, resource_instance ) if isinstance(relation_instance, Manager): relation_instance = relation_instance.all() From 0afeccf2d0ac9c3d20bc725d46bce7154fa0cc85 Mon Sep 17 00:00:00 2001 From: Anton-Shutik Date: Fri, 31 May 2019 11:39:11 +0300 Subject: [PATCH 3/4] Changed tests --- example/tests/test_renderers.py | 49 ---------------------------- example/tests/unit/test_renderers.py | 14 +++++++- 2 files changed, 13 insertions(+), 50 deletions(-) delete mode 100644 example/tests/test_renderers.py diff --git a/example/tests/test_renderers.py b/example/tests/test_renderers.py deleted file mode 100644 index ab59744a..00000000 --- a/example/tests/test_renderers.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.test import TestCase - -from example.models import Blog, Entry, Comment -from example.serializers import EntrySerializer -from rest_framework_json_api import serializers -from rest_framework_json_api.renderers import JSONRenderer - - -class TestJSONRenderer(TestCase): - - def setUp(self): - pass - - def test_extract_relation_instance(self): - - class NewEntySerializer(EntrySerializer): - blog_name = serializers.ReadOnlyField(source='blog.name') - - class Meta(EntrySerializer.Meta): - fields = EntrySerializer.Meta.fields + ('blog_name',) - - blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") - entry = Entry.objects.create( - blog=blog, - headline='headline', - body_text='body_text', - ) - suggested_enty = Entry.objects.create( - blog=blog, - headline='suggested headline', - body_text='suggested body_text', - ) - Comment.objects.create(entry=entry, body='foo') - serializer = NewEntySerializer(instance=entry) - - got = JSONRenderer.extract_relation_instance( - field=serializer.fields['featured'], resource_instance=entry - ) - assert got == suggested_enty - - got = JSONRenderer.extract_relation_instance( - field=serializer.fields['comments'], resource_instance=entry - ) - assert str(got.query) == str(Comment.objects.filter(entry=entry).query) - - got = JSONRenderer.extract_relation_instance( - field=serializer.fields['blog_name'], resource_instance=entry - ) - assert got == blog.name diff --git a/example/tests/unit/test_renderers.py b/example/tests/unit/test_renderers.py index 6126cff0..1452aac8 100644 --- a/example/tests/unit/test_renderers.py +++ b/example/tests/unit/test_renderers.py @@ -10,9 +10,11 @@ # serializers class RelatedModelSerializer(serializers.ModelSerializer): + blog = serializers.ReadOnlyField(source='entry.blog') + class Meta: model = Comment - fields = ('id',) + fields = ('id', 'blog') class DummyTestSerializer(serializers.ModelSerializer): @@ -137,3 +139,13 @@ class EmptyRelationshipViewSet(views.ReadOnlyModelViewSet): assert 'relationships' in result['data'] assert 'bio' in result['data']['relationships'] assert result['data']['relationships']['bio'] == {'data': None} + + +@pytest.mark.django_db +def test_extract_relation_instance(comment): + serializer = RelatedModelSerializer(instance=comment) + + got = JSONRenderer.extract_relation_instance( + field=serializer.fields['blog'], resource_instance=comment + ) + assert got == comment.entry.blog From 82a68b6d5882f9497e21b3901cf6a5fffe55757f Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Tue, 4 Jun 2019 13:22:27 +0200 Subject: [PATCH 4/4] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18bb1db0..c3a9c9f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ any parts of the framework not mentioned in the documentation should generally b * Allow to define `select_related` per include using [select_for_includes](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#performance-improvements) * Reduce number of queries to calculate includes by using `select_related` when possible +* Use REST framework serializer functionality to extract includes. This adds support like using + dotted notations in source attribute in `ResourceRelatedField`. ### Fixed @@ -25,7 +27,6 @@ any parts of the framework not mentioned in the documentation should generally b * Don't swallow `filter[]` params when there are several * Fix DeprecationWarning regarding collections.abc import in Python 3.7 * Allow OPTIONS request to be used on RelationshipView -* Use DRF code when extracting relation instance. [PR](https://github.com/django-json-api/django-rest-framework-json-api/pull/632) ### Deprecated