diff --git a/AUTHORS b/AUTHORS index 11d8db8c..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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 13be06bd..4f2a1a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ any parts of the framework not mentioned in the documentation should generally b * 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/serializers.py b/example/serializers.py index 7a923d4e..4d80c87c 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.split(" ")]) + 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.SerializerMethodField() included_serializers = { "entry": EntrySerializer, @@ -312,6 +319,10 @@ class Meta: "modified_at", ) # 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): diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index 0ee0d4fd..223645d2 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -261,3 +261,17 @@ 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_resources(single_entry, client): + response = client.get( + reverse("entry-detail", kwargs={"pk": single_entry.pk}) + "?include=comments" + ) + assert response.json()["included"][0].get("meta") + + response = client.get( + reverse("entry-detail", kwargs={"pk": single_entry.pk}) + + "?include=comments.author" + ) + assert response.json()["included"][0].get("meta") + assert response.json()["included"][1].get("meta") diff --git a/example/tests/test_format_keys.py b/example/tests/test_format_keys.py index 23a8a325..698a8bb1 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..7550fd2c 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -195,6 +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 + }, } } 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 4733288f..e84c562b 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -373,11 +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) + included_cache[new_item["type"]][new_item["id"]] = new_item + cls.extract_included( serializer_fields, serializer_resource, @@ -397,11 +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) + included_cache[new_item["type"]][new_item["id"]] = new_item + cls.extract_included( serializer_fields, serializer_data, @@ -450,6 +450,7 @@ def build_json_resource_obj( resource, resource_instance, resource_name, + serializer, force_type_resolution=False, ): """ @@ -476,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( @@ -582,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( @@ -610,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,