Skip to content

Commit ec0f8b8

Browse files
committed
Ensured that sparse fields are only dropped when rendering but not when parsing
1 parent 743274d commit ec0f8b8

File tree

7 files changed

+147
-60
lines changed

7 files changed

+147
-60
lines changed

example/tests/integration/test_non_paginated_responses.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_multiple_entries_no_pagination(multiple_entries, client):
1414
expected = {
1515
"data": [
1616
{
17-
"type": "posts",
17+
"type": "entries",
1818
"id": "1",
1919
"attributes": {
2020
"headline": multiple_entries[0].headline,
@@ -70,7 +70,7 @@ def test_multiple_entries_no_pagination(multiple_entries, client):
7070
},
7171
},
7272
{
73-
"type": "posts",
73+
"type": "entries",
7474
"id": "2",
7575
"attributes": {
7676
"headline": multiple_entries[1].headline,

example/tests/integration/test_pagination.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_pagination_with_single_entry(single_entry, client):
1414
expected = {
1515
"data": [
1616
{
17-
"type": "posts",
17+
"type": "entries",
1818
"id": "1",
1919
"attributes": {
2020
"headline": single_entry.headline,

example/tests/test_filters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ def test_search_keywords(self):
470470
expected_result = {
471471
"data": [
472472
{
473-
"type": "posts",
473+
"type": "entries",
474474
"id": "7",
475475
"attributes": {
476476
"headline": "ANTH3868X",

example/views.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ class BlogCustomViewSet(JsonApiViewSet):
112112

113113
class EntryViewSet(ModelViewSet):
114114
queryset = Entry.objects.all()
115-
resource_name = "posts"
115+
# TODO it should not be supported to overwrite resource name
116+
# of endpoints with serializers as includes and sparse fields
117+
# cannot be aware of it
118+
# resource_name = "posts"
116119

117120
def get_serializer_class(self):
118121
return EntrySerializer

rest_framework_json_api/renderers.py

+81-31
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,26 @@
1717
from rest_framework.settings import api_settings
1818

1919
import rest_framework_json_api
20-
from rest_framework_json_api import utils
2120
from rest_framework_json_api.relations import (
2221
HyperlinkedMixin,
2322
ManySerializerMethodResourceRelatedField,
2423
ResourceRelatedField,
2524
SkipDataMixin,
2625
)
26+
from rest_framework_json_api.utils import (
27+
format_errors,
28+
format_field_name,
29+
format_field_names,
30+
get_included_resources,
31+
get_related_resource_type,
32+
get_relation_instance,
33+
get_resource_id,
34+
get_resource_name,
35+
get_resource_type_from_instance,
36+
get_resource_type_from_serializer,
37+
get_serializer_fields,
38+
is_relationship_field,
39+
)
2740

2841

2942
class JSONRenderer(renderers.JSONRenderer):
@@ -53,6 +66,26 @@ class JSONRenderer(renderers.JSONRenderer):
5366
media_type = "application/vnd.api+json"
5467
format = "vnd.api+json"
5568

69+
@classmethod
70+
def extract_attributes_test(cls, fields, resource):
71+
"""
72+
Builds the `attributes` object of the JSON:API resource object.
73+
74+
Ensures that ID which is always provided in a JSON:API resource object
75+
and relationships are not returned.
76+
"""
77+
78+
# TODO check why this is not working
79+
invalid_fields = {"id", api_settings.URL_FIELD_NAME}
80+
81+
return {
82+
format_field_name(field_name): value
83+
for field_name, value in resource.items()
84+
if field_name in fields
85+
and field_name not in invalid_fields
86+
and not is_relationship_field(fields[field_name])
87+
}
88+
5689
@classmethod
5790
def extract_attributes(cls, fields, resource):
5891
"""
@@ -67,7 +100,7 @@ def extract_attributes(cls, fields, resource):
67100
if fields[field_name].write_only:
68101
continue
69102
# Skip fields with relations
70-
if utils.is_relationship_field(field):
103+
if is_relationship_field(field):
71104
continue
72105

73106
# Skip read_only attribute fields when `resource` is an empty
@@ -81,7 +114,7 @@ def extract_attributes(cls, fields, resource):
81114

82115
data.update({field_name: resource.get(field_name)})
83116

84-
return utils.format_field_names(data)
117+
return format_field_names(data)
85118

86119
@classmethod
87120
def extract_relationships(cls, fields, resource, resource_instance):
@@ -107,14 +140,14 @@ def extract_relationships(cls, fields, resource, resource_instance):
107140
continue
108141

109142
# Skip fields without relations
110-
if not utils.is_relationship_field(field):
143+
if not is_relationship_field(field):
111144
continue
112145

113146
source = field.source
114-
relation_type = utils.get_related_resource_type(field)
147+
relation_type = get_related_resource_type(field)
115148

116149
if isinstance(field, relations.HyperlinkedIdentityField):
117-
resolved, relation_instance = utils.get_relation_instance(
150+
resolved, relation_instance = get_relation_instance(
118151
resource_instance, source, field.parent
119152
)
120153
if not resolved:
@@ -166,7 +199,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
166199
field,
167200
(relations.PrimaryKeyRelatedField, relations.HyperlinkedRelatedField),
168201
):
169-
resolved, relation = utils.get_relation_instance(
202+
resolved, relation = get_relation_instance(
170203
resource_instance, f"{source}_id", field.parent
171204
)
172205
if not resolved:
@@ -189,7 +222,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
189222
continue
190223

191224
if isinstance(field, relations.ManyRelatedField):
192-
resolved, relation_instance = utils.get_relation_instance(
225+
resolved, relation_instance = get_relation_instance(
193226
resource_instance, source, field.parent
194227
)
195228
if not resolved:
@@ -222,9 +255,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
222255
for nested_resource_instance in relation_instance:
223256
nested_resource_instance_type = (
224257
relation_type
225-
or utils.get_resource_type_from_instance(
226-
nested_resource_instance
227-
)
258+
or get_resource_type_from_instance(nested_resource_instance)
228259
)
229260

230261
relation_data.append(
@@ -243,7 +274,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
243274
)
244275
continue
245276

246-
return utils.format_field_names(data)
277+
return format_field_names(data)
247278

248279
@classmethod
249280
def extract_relation_instance(cls, field, resource_instance):
@@ -289,7 +320,7 @@ def extract_included(
289320
continue
290321

291322
# Skip fields without relations
292-
if not utils.is_relationship_field(field):
323+
if not is_relationship_field(field):
293324
continue
294325

295326
try:
@@ -341,7 +372,7 @@ def extract_included(
341372

342373
if isinstance(field, ListSerializer):
343374
serializer = field.child
344-
relation_type = utils.get_resource_type_from_serializer(serializer)
375+
relation_type = get_resource_type_from_serializer(serializer)
345376
relation_queryset = list(relation_instance)
346377

347378
if serializer_data:
@@ -350,11 +381,9 @@ def extract_included(
350381
nested_resource_instance = relation_queryset[position]
351382
resource_type = (
352383
relation_type
353-
or utils.get_resource_type_from_instance(
354-
nested_resource_instance
355-
)
384+
or get_resource_type_from_instance(nested_resource_instance)
356385
)
357-
serializer_fields = utils.get_serializer_fields(
386+
serializer_fields = get_serializer_fields(
358387
serializer.__class__(
359388
nested_resource_instance, context=serializer.context
360389
)
@@ -378,10 +407,10 @@ def extract_included(
378407
)
379408

380409
if isinstance(field, Serializer):
381-
relation_type = utils.get_resource_type_from_serializer(field)
410+
relation_type = get_resource_type_from_serializer(field)
382411

383412
# Get the serializer fields
384-
serializer_fields = utils.get_serializer_fields(field)
413+
serializer_fields = get_serializer_fields(field)
385414
if serializer_data:
386415
new_item = cls.build_json_resource_obj(
387416
serializer_fields,
@@ -414,7 +443,8 @@ def extract_meta(cls, serializer, resource):
414443
meta_fields = getattr(meta, "meta_fields", [])
415444
data = {}
416445
for field_name in meta_fields:
417-
data.update({field_name: resource.get(field_name)})
446+
if field_name in resource:
447+
data.update({field_name: resource[field_name]})
418448
return data
419449

420450
@classmethod
@@ -434,6 +464,24 @@ def extract_root_meta(cls, serializer, resource):
434464
data.update(json_api_meta)
435465
return data
436466

467+
@classmethod
468+
def _filter_sparse_fields(cls, serializer, fields, resource_name):
469+
request = serializer.context.get("request")
470+
if request:
471+
sparse_fieldset_query_param = f"fields[{resource_name}]"
472+
sparse_fieldset_value = request.query_params.get(
473+
sparse_fieldset_query_param
474+
)
475+
if sparse_fieldset_value:
476+
sparse_fields = sparse_fieldset_value.split(",")
477+
return {
478+
field_name: field
479+
for field_name, field, in fields.items()
480+
if field_name in sparse_fields
481+
}
482+
483+
return fields
484+
437485
@classmethod
438486
def build_json_resource_obj(
439487
cls,
@@ -449,11 +497,13 @@ def build_json_resource_obj(
449497
"""
450498
# Determine type from the instance if the underlying model is polymorphic
451499
if force_type_resolution:
452-
resource_name = utils.get_resource_type_from_instance(resource_instance)
500+
resource_name = get_resource_type_from_instance(resource_instance)
453501
resource_data = {
454502
"type": resource_name,
455-
"id": utils.get_resource_id(resource_instance, resource),
503+
"id": get_resource_id(resource_instance, resource),
456504
}
505+
506+
fields = cls._filter_sparse_fields(serializer, fields, resource_name)
457507
attributes = cls.extract_attributes(fields, resource)
458508
if attributes:
459509
resource_data["attributes"] = attributes
@@ -466,9 +516,10 @@ def build_json_resource_obj(
466516
):
467517
resource_data["links"] = {"self": resource[api_settings.URL_FIELD_NAME]}
468518

519+
# TODO write test that it checks that meta field is removed
469520
meta = cls.extract_meta(serializer, resource)
470521
if meta:
471-
resource_data["meta"] = utils.format_field_names(meta)
522+
resource_data["meta"] = format_field_names(meta)
472523

473524
return resource_data
474525

@@ -485,7 +536,7 @@ def render_relationship_view(
485536

486537
def render_errors(self, data, accepted_media_type=None, renderer_context=None):
487538
return super().render(
488-
utils.format_errors(data), accepted_media_type, renderer_context
539+
format_errors(data), accepted_media_type, renderer_context
489540
)
490541

491542
def render(self, data, accepted_media_type=None, renderer_context=None):
@@ -495,7 +546,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
495546
request = renderer_context.get("request", None)
496547

497548
# Get the resource name.
498-
resource_name = utils.get_resource_name(renderer_context)
549+
resource_name = get_resource_name(renderer_context)
499550

500551
# If this is an error response, skip the rest.
501552
if resource_name == "errors":
@@ -531,7 +582,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
531582

532583
serializer = getattr(serializer_data, "serializer", None)
533584

534-
included_resources = utils.get_included_resources(request, serializer)
585+
included_resources = get_included_resources(request, serializer)
535586

536587
if serializer is not None:
537588
# Extract root meta for any type of serializer
@@ -558,7 +609,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
558609
else:
559610
resource_serializer_class = serializer.child
560611

561-
fields = utils.get_serializer_fields(resource_serializer_class)
612+
fields = get_serializer_fields(resource_serializer_class)
562613
force_type_resolution = getattr(
563614
resource_serializer_class, "_poly_force_type_resolution", False
564615
)
@@ -581,7 +632,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
581632
included_cache,
582633
)
583634
else:
584-
fields = utils.get_serializer_fields(serializer)
635+
fields = get_serializer_fields(serializer)
585636
force_type_resolution = getattr(
586637
serializer, "_poly_force_type_resolution", False
587638
)
@@ -640,7 +691,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
640691
)
641692

642693
if json_api_meta:
643-
render_data["meta"] = utils.format_field_names(json_api_meta)
694+
render_data["meta"] = format_field_names(json_api_meta)
644695

645696
return super().render(render_data, accepted_media_type, renderer_context)
646697

@@ -690,7 +741,6 @@ def get_includes_form(self, view):
690741
serializer_class = view.get_serializer_class()
691742
except AttributeError:
692743
return
693-
694744
if not hasattr(serializer_class, "included_serializers"):
695745
return
696746

0 commit comments

Comments
 (0)