From 7642228cf87d621befb8929df3ad2e9534e63d2e Mon Sep 17 00:00:00 2001 From: Kozhenin Nick Date: Wed, 13 Jan 2021 15:38:52 +0300 Subject: [PATCH 1/6] Render meta fields of included objects with tests --- example/models.py | 4 ++++ example/serializers.py | 8 ++++++++ example/tests/integration/test_includes.py | 17 +++++++++++++++++ example/tests/test_format_keys.py | 1 + example/tests/test_serializers.py | 1 + rest_framework_json_api/renderers.py | 8 ++++++++ 6 files changed, 39 insertions(+) diff --git a/example/models.py b/example/models.py index 47537b57..9bbe646c 100644 --- a/example/models.py +++ b/example/models.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals +from datetime import datetime from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models @@ -130,6 +131,9 @@ def __str__(self): class Meta: ordering = ("id",) + def modified_days_ago(self): + return (datetime.now() - self.modified_at).days + class ProjectType(BaseModel): name = models.CharField(max_length=50) diff --git a/example/serializers.py b/example/serializers.py index 7a923d4e..6db436d6 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -251,6 +251,7 @@ class AuthorSerializer(serializers.ModelSerializer): write_only=True, help_text="help for defaults", ) + initials = serializers.SerializerMethodField() included_serializers = {"bio": AuthorBioSerializer, "type": AuthorTypeSerializer} related_serializers = { "bio": "example.serializers.AuthorBioSerializer", @@ -272,11 +273,16 @@ class Meta: "type", "secrets", "defaults", + "initials", ) + meta_fields = ("initials",) def get_first_entry(self, obj): return obj.entries.first() + def get_initials(self, obj): + return ' '.join([word[0] for word in [obj.name]]) + class AuthorListSerializer(AuthorSerializer): pass @@ -298,6 +304,7 @@ class Meta: class CommentSerializer(serializers.ModelSerializer): # testing remapping of related name writer = relations.ResourceRelatedField(source="author", read_only=True) + modified_days_ago = serializers.IntegerField(read_only=True) included_serializers = { "entry": EntrySerializer, @@ -312,6 +319,7 @@ class Meta: "modified_at", ) # fields = ('entry', 'body', 'author',) + meta_fields = ("modified_days_ago",) class ProjectTypeSerializer(serializers.ModelSerializer): diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index 0ee0d4fd..09b8ea5a 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -261,3 +261,20 @@ def test_data_resource_not_included_again(single_comment, client): # The comment in the data attribute must not be included again. expected_comment_count -= 1 assert comment_count == expected_comment_count, "Comment count incorrect" + + +def test_meta_object_added_to_included_resource_on_list(single_entry, client): + # Add metadata to included object + response = client.get( + reverse("entry-detail", kwargs={"pk": single_entry.pk}) + + "?include=comments" + ) + meta = response.json()['included'][0].get('meta', False) + assert meta, 'list has no meta object' + + response = client.get( + reverse("entry-detail", kwargs={"pk": single_entry.pk}) + + "?include=comments.author" + ) + meta = response.json()['included'][0].get('meta', False) + assert meta, 'detail has no meta object' diff --git a/example/tests/test_format_keys.py b/example/tests/test_format_keys.py index 23a8a325..54533436 100644 --- a/example/tests/test_format_keys.py +++ b/example/tests/test_format_keys.py @@ -63,5 +63,6 @@ def test_options_format_field_names(db, client): "comments", "secrets", "defaults", + "initials" } assert expected_keys == data["actions"]["POST"].keys() diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py index ebadbedd..f6d6c42d 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -195,6 +195,7 @@ def test_model_serializer_with_implicit_fields(self, comment, client): "data": {"type": "writers", "id": str(comment.author.pk)} }, }, + "meta": {"modifiedDaysAgo": (datetime.now() - comment.modified_at).days} } } diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 4733288f..40bbb36c 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -378,6 +378,10 @@ def extract_included( included_cache[new_item["type"]][ new_item["id"] ] = utils.format_field_names(new_item) + meta = cls.extract_meta(field, serializer_resource) + if meta: + included_cache[new_item['type']][new_item['id']]['meta'] = \ + utils.format_field_names(meta) cls.extract_included( serializer_fields, serializer_resource, @@ -402,6 +406,10 @@ def extract_included( included_cache[new_item["type"]][ new_item["id"] ] = utils.format_field_names(new_item) + meta = cls.extract_meta(field, serializer_data) + if meta: + included_cache[new_item['type']][new_item['id']]['meta'] = \ + utils.format_field_names(meta) cls.extract_included( serializer_fields, serializer_data, From e7a9293f1a14675cea8d80f30fc3d841f676416f Mon Sep 17 00:00:00 2001 From: Kozhenin Nick Date: Wed, 13 Jan 2021 15:48:27 +0300 Subject: [PATCH 2/6] Added name to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 11d8db8c..b135699c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -36,3 +36,4 @@ Tim Selman Tom Glowka Ulrich Schuster Yaniv Peer +Nick Kozhenin From 5566fa7bc345161a0dcf271fe7b813603926697b Mon Sep 17 00:00:00 2001 From: Kozhenin Nick Date: Wed, 13 Jan 2021 16:42:31 +0300 Subject: [PATCH 3/6] Reformatted with black --- example/serializers.py | 2 +- example/tests/integration/test_includes.py | 11 +++++------ example/tests/test_format_keys.py | 2 +- example/tests/test_serializers.py | 4 +++- rest_framework_json_api/renderers.py | 10 ++++++---- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/example/serializers.py b/example/serializers.py index 6db436d6..c1fd6288 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -281,7 +281,7 @@ def get_first_entry(self, obj): return obj.entries.first() def get_initials(self, obj): - return ' '.join([word[0] for word in [obj.name]]) + return " ".join([word[0] for word in [obj.name]]) class AuthorListSerializer(AuthorSerializer): diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index 09b8ea5a..c26e30e6 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -266,15 +266,14 @@ def test_data_resource_not_included_again(single_comment, client): def test_meta_object_added_to_included_resource_on_list(single_entry, client): # Add metadata to included object response = client.get( - reverse("entry-detail", kwargs={"pk": single_entry.pk}) - + "?include=comments" + reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=comments" ) - meta = response.json()['included'][0].get('meta', False) - assert meta, 'list has no meta object' + meta = response.json()["included"][0].get("meta", False) + assert meta, "list has no meta object" response = client.get( reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=comments.author" ) - meta = response.json()['included'][0].get('meta', False) - assert meta, 'detail has no meta object' + meta = response.json()["included"][0].get("meta", False) + assert meta, "detail has no meta object" diff --git a/example/tests/test_format_keys.py b/example/tests/test_format_keys.py index 54533436..698a8bb1 100644 --- a/example/tests/test_format_keys.py +++ b/example/tests/test_format_keys.py @@ -63,6 +63,6 @@ def test_options_format_field_names(db, client): "comments", "secrets", "defaults", - "initials" + "initials", } assert expected_keys == data["actions"]["POST"].keys() diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py index f6d6c42d..7550fd2c 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -195,7 +195,9 @@ def test_model_serializer_with_implicit_fields(self, comment, client): "data": {"type": "writers", "id": str(comment.author.pk)} }, }, - "meta": {"modifiedDaysAgo": (datetime.now() - comment.modified_at).days} + "meta": { + "modifiedDaysAgo": (datetime.now() - comment.modified_at).days + }, } } diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 40bbb36c..3e071df5 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -380,8 +380,9 @@ def extract_included( ] = utils.format_field_names(new_item) meta = cls.extract_meta(field, serializer_resource) if meta: - included_cache[new_item['type']][new_item['id']]['meta'] = \ - utils.format_field_names(meta) + included_cache[new_item["type"]][new_item["id"]][ + "meta" + ] = utils.format_field_names(meta) cls.extract_included( serializer_fields, serializer_resource, @@ -408,8 +409,9 @@ def extract_included( ] = utils.format_field_names(new_item) meta = cls.extract_meta(field, serializer_data) if meta: - included_cache[new_item['type']][new_item['id']]['meta'] = \ - utils.format_field_names(meta) + included_cache[new_item["type"]][new_item["id"]][ + "meta" + ] = utils.format_field_names(meta) cls.extract_included( serializer_fields, serializer_data, From b223c4750a699434b136ffd72ffb2f5517af3e12 Mon Sep 17 00:00:00 2001 From: Kozhenin Nick Date: Thu, 14 Jan 2021 20:30:36 +0300 Subject: [PATCH 4/6] Moved meta logic into function. Moved name in Authors and some small fixes --- AUTHORS | 2 +- example/models.py | 4 ---- example/serializers.py | 7 +++++-- rest_framework_json_api/renderers.py | 24 ++++++++++++++---------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/AUTHORS b/AUTHORS index b135699c..c1a273c4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Matt Layman Michael Haselton Mohammed Ali Zubair Nathanael Gordon +Nick Kozhenin Ola Tarkowska Oliver Sauder Raphael Cohen @@ -36,4 +37,3 @@ Tim Selman Tom Glowka Ulrich Schuster Yaniv Peer -Nick Kozhenin diff --git a/example/models.py b/example/models.py index 9bbe646c..47537b57 100644 --- a/example/models.py +++ b/example/models.py @@ -1,7 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals -from datetime import datetime from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models @@ -131,9 +130,6 @@ def __str__(self): class Meta: ordering = ("id",) - def modified_days_ago(self): - return (datetime.now() - self.modified_at).days - class ProjectType(BaseModel): name = models.CharField(max_length=50) diff --git a/example/serializers.py b/example/serializers.py index c1fd6288..4d80c87c 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -281,7 +281,7 @@ def get_first_entry(self, obj): return obj.entries.first() def get_initials(self, obj): - return " ".join([word[0] for word in [obj.name]]) + return "".join([word[0] for word in obj.name.split(" ")]) class AuthorListSerializer(AuthorSerializer): @@ -304,7 +304,7 @@ class Meta: class CommentSerializer(serializers.ModelSerializer): # testing remapping of related name writer = relations.ResourceRelatedField(source="author", read_only=True) - modified_days_ago = serializers.IntegerField(read_only=True) + modified_days_ago = serializers.SerializerMethodField() included_serializers = { "entry": EntrySerializer, @@ -321,6 +321,9 @@ class Meta: # fields = ('entry', 'body', 'author',) meta_fields = ("modified_days_ago",) + def get_modified_days_ago(self, obj): + return (datetime.now() - obj.modified_at).days + class ProjectTypeSerializer(serializers.ModelSerializer): class Meta: diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 3e071df5..9915231f 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -378,11 +378,9 @@ def extract_included( included_cache[new_item["type"]][ new_item["id"] ] = utils.format_field_names(new_item) - meta = cls.extract_meta(field, serializer_resource) - if meta: - included_cache[new_item["type"]][new_item["id"]][ - "meta" - ] = utils.format_field_names(meta) + cls.add_meta_to_included( + included_cache, field, serializer_resource, new_item + ) cls.extract_included( serializer_fields, serializer_resource, @@ -407,11 +405,9 @@ def extract_included( included_cache[new_item["type"]][ new_item["id"] ] = utils.format_field_names(new_item) - meta = cls.extract_meta(field, serializer_data) - if meta: - included_cache[new_item["type"]][new_item["id"]][ - "meta" - ] = utils.format_field_names(meta) + cls.add_meta_to_included( + included_cache, field, serializer_data, new_item + ) cls.extract_included( serializer_fields, serializer_data, @@ -420,6 +416,14 @@ def extract_included( included_cache, ) + @classmethod + def add_meta_to_included(cls, included_cache, field, resource, new_item): + meta = cls.extract_meta(field, resource) + if meta: + included_cache[new_item["type"]][new_item["id"]][ + "meta" + ] = utils.format_field_names(meta) + @classmethod def extract_meta(cls, serializer, resource): """ From bdff4a883b530d6bb73b0428b00cbc94c900181e Mon Sep 17 00:00:00 2001 From: Kozhenin Nick Date: Thu, 14 Jan 2021 20:38:13 +0300 Subject: [PATCH 5/6] Add a changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13be06bd..aef32b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ any parts of the framework not mentioned in the documentation should generally b * Ability for the user to select `included_serializers` to apply when using `BrowsableAPI`, based on available `included_serializers` defined for the current endpoint. * Ability for the user to format serializer properties in URL segments using the `JSON_API_FORMAT_RELATED_LINKS` setting. +* Ability to render meta_fields of included resources ### Fixed From 3a3032adb3f25b188c0a1d9699b6216049a56c95 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Wed, 10 Feb 2021 22:33:04 +0400 Subject: [PATCH 6/6] Move extracing of meta logic into build_json_resource_obj --- CHANGELOG.md | 2 +- example/tests/integration/test_includes.py | 10 ++--- .../tests/unit/test_renderer_class_methods.py | 25 ++++++----- rest_framework_json_api/renderers.py | 43 ++++++------------- 4 files changed, 34 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aef32b20..4f2a1a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,12 @@ any parts of the framework not mentioned in the documentation should generally b * Ability for the user to select `included_serializers` to apply when using `BrowsableAPI`, based on available `included_serializers` defined for the current endpoint. * Ability for the user to format serializer properties in URL segments using the `JSON_API_FORMAT_RELATED_LINKS` setting. -* Ability to render meta_fields of included resources ### Fixed * Allow users to overwrite a view's `get_serializer_class()` method when using [related urls](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html#related-urls) * Correctly resolve the resource type of `ResourceRelatedField(many=True)` fields on plain serializers +* Render `meta_fields` in included resources ## [4.0.0] - 2020-10-31 diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index c26e30e6..223645d2 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -263,17 +263,15 @@ def test_data_resource_not_included_again(single_comment, client): assert comment_count == expected_comment_count, "Comment count incorrect" -def test_meta_object_added_to_included_resource_on_list(single_entry, client): - # Add metadata to included object +def test_meta_object_added_to_included_resources(single_entry, client): response = client.get( reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=comments" ) - meta = response.json()["included"][0].get("meta", False) - assert meta, "list has no meta object" + assert response.json()["included"][0].get("meta") response = client.get( reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=comments.author" ) - meta = response.json()["included"][0].get("meta", False) - assert meta, "detail has no meta object" + assert response.json()["included"][0].get("meta") + assert response.json()["included"][1].get("meta") diff --git a/example/tests/unit/test_renderer_class_methods.py b/example/tests/unit/test_renderer_class_methods.py index c599abbd..51cc2481 100644 --- a/example/tests/unit/test_renderer_class_methods.py +++ b/example/tests/unit/test_renderer_class_methods.py @@ -3,6 +3,7 @@ from rest_framework_json_api import serializers from rest_framework_json_api.renderers import JSONRenderer +from rest_framework_json_api.utils import get_serializer_fields pytestmark = pytest.mark.django_db @@ -20,10 +21,7 @@ class Meta: def test_build_json_resource_obj(): - resource = { - "pk": 1, - "username": "Alice", - } + resource = {"username": "Alice", "version": "1.0.0"} serializer = ResourceSerializer(data={"username": "Alice"}) serializer.is_valid() @@ -33,11 +31,16 @@ def test_build_json_resource_obj(): "type": "user", "id": "1", "attributes": {"username": "Alice"}, + "meta": {"version": "1.0.0"}, } assert ( JSONRenderer.build_json_resource_obj( - serializer.fields, resource, resource_instance, "user" + get_serializer_fields(serializer), + resource, + resource_instance, + "user", + serializer, ) == output ) @@ -47,11 +50,8 @@ def test_can_override_methods(): """ Make sure extract_attributes and extract_relationships can be overriden. """ - resource = { - "pk": 1, - "username": "Alice", - } + resource = {"username": "Alice", "version": "1.0.0"} serializer = ResourceSerializer(data={"username": "Alice"}) serializer.is_valid() resource_instance = serializer.save() @@ -60,6 +60,7 @@ def test_can_override_methods(): "type": "user", "id": "1", "attributes": {"username": "Alice"}, + "meta": {"version": "1.0.0"}, } class CustomRenderer(JSONRenderer): @@ -80,7 +81,11 @@ def extract_relationships(cls, fields, resource, resource_instance): assert ( CustomRenderer.build_json_resource_obj( - serializer.fields, resource, resource_instance, "user" + get_serializer_fields(serializer), + resource, + resource_instance, + "user", + serializer, ) == output ) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 9915231f..e84c562b 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -373,14 +373,11 @@ def extract_included( serializer_resource, nested_resource_instance, resource_type, + serializer, getattr(serializer, "_poly_force_type_resolution", False), ) - included_cache[new_item["type"]][ - new_item["id"] - ] = utils.format_field_names(new_item) - cls.add_meta_to_included( - included_cache, field, serializer_resource, new_item - ) + included_cache[new_item["type"]][new_item["id"]] = new_item + cls.extract_included( serializer_fields, serializer_resource, @@ -400,14 +397,11 @@ def extract_included( serializer_data, relation_instance, relation_type, + field, getattr(field, "_poly_force_type_resolution", False), ) - included_cache[new_item["type"]][ - new_item["id"] - ] = utils.format_field_names(new_item) - cls.add_meta_to_included( - included_cache, field, serializer_data, new_item - ) + included_cache[new_item["type"]][new_item["id"]] = new_item + cls.extract_included( serializer_fields, serializer_data, @@ -416,14 +410,6 @@ def extract_included( included_cache, ) - @classmethod - def add_meta_to_included(cls, included_cache, field, resource, new_item): - meta = cls.extract_meta(field, resource) - if meta: - included_cache[new_item["type"]][new_item["id"]][ - "meta" - ] = utils.format_field_names(meta) - @classmethod def extract_meta(cls, serializer, resource): """ @@ -464,6 +450,7 @@ def build_json_resource_obj( resource, resource_instance, resource_name, + serializer, force_type_resolution=False, ): """ @@ -490,6 +477,11 @@ def build_json_resource_obj( resource_data.append( ("links", {"self": resource[api_settings.URL_FIELD_NAME]}) ) + + meta = cls.extract_meta(serializer, resource) + if meta: + resource_data.append(("meta", utils.format_field_names(meta))) + return OrderedDict(resource_data) def render_relationship_view( @@ -596,13 +588,9 @@ def render(self, data, accepted_media_type=None, renderer_context=None): resource, resource_instance, resource_name, + serializer, force_type_resolution, ) - meta = self.extract_meta(serializer, resource) - if meta: - json_resource_obj.update( - {"meta": utils.format_field_names(meta)} - ) json_api_data.append(json_resource_obj) self.extract_included( @@ -624,13 +612,10 @@ def render(self, data, accepted_media_type=None, renderer_context=None): serializer_data, resource_instance, resource_name, + serializer, force_type_resolution, ) - meta = self.extract_meta(serializer, serializer_data) - if meta: - json_api_data.update({"meta": utils.format_field_names(meta)}) - self.extract_included( fields, serializer_data,