Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 043eeb5

Browse files
Use jsonapi.org syntax for errors with included syntax
1 parent 364db35 commit 043eeb5

File tree

3 files changed

+159
-64
lines changed

3 files changed

+159
-64
lines changed

‎example/serializers.py‎

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,73 @@ class Meta:
4949
meta_fields = ('copyright',)
5050

5151

52-
class EntrySerializer(serializers.ModelSerializer):
52+
53+
class AuthorTypeSerializer(serializers.ModelSerializer):
54+
class Meta:
55+
model = AuthorType
56+
fields = ('name', )
57+
58+
59+
class AuthorBioSerializer(serializers.ModelSerializer):
60+
class Meta:
61+
model = AuthorBio
62+
fields = ('author', 'body')
63+
64+
65+
class AuthorSerializer(serializers.ModelSerializer):
66+
bio = relations.ResourceRelatedField(
67+
related_link_view_name='author-related',
68+
self_link_view_name='author-relationships',
69+
queryset=AuthorBio.objects,
70+
)
71+
entries = relations.ResourceRelatedField(
72+
related_link_view_name='author-related',
73+
self_link_view_name='author-relationships',
74+
queryset=Entry.objects,
75+
many=True
76+
)
77+
first_entry = relations.SerializerMethodResourceRelatedField(
78+
related_link_view_name='author-related',
79+
self_link_view_name='author-relationships',
80+
model=Entry,
81+
read_only=True,
82+
source='get_first_entry'
83+
)
84+
included_serializers = {
85+
'bio': AuthorBioSerializer,
86+
'type': AuthorTypeSerializer
87+
}
88+
related_serializers = {
89+
'bio': 'example.serializers.AuthorBioSerializer',
90+
'entries': 'example.serializers.EntrySerializer',
91+
'first_entry': 'example.serializers.EntrySerializer'
92+
}
93+
94+
class Meta:
95+
model = Author
96+
fields = ('name', 'email', 'bio', 'entries', 'first_entry', 'type')
97+
98+
def get_first_entry(self, obj):
99+
return obj.entries.first()
100+
101+
102+
class WriterSerializer(serializers.ModelSerializer):
103+
included_serializers = {
104+
'bio': AuthorBioSerializer
105+
}
106+
107+
class Meta:
108+
model = Author
109+
fields = ('name', 'email', 'bio')
110+
resource_name = 'writers'
111+
112+
class IncludedEntrySerializer(serializers.IncludedResourcesMixin, serializers.Serializer):
113+
blogs = BlogSerializer(many=True, required=False)
114+
authors = AuthorSerializer(many=True, required=False)
115+
116+
117+
class EntrySerializer(serializers.IncludingResourcesMixin, serializers.ModelSerializer):
118+
53119
def __init__(self, *args, **kwargs):
54120
super(EntrySerializer, self).__init__(*args, **kwargs)
55121
# to make testing more concise we'll only output the
@@ -65,6 +131,7 @@ def __init__(self, *args, **kwargs):
65131
'suggested': 'example.serializers.EntrySerializer',
66132
'tags': 'example.serializers.TaggedItemSerializer',
67133
}
134+
_included = IncludedEntrySerializer(write_only=True)
68135

69136
body_format = serializers.SerializerMethodField()
70137
# single related from model
@@ -134,74 +201,13 @@ class Meta:
134201
model = Entry
135202
fields = ('blog', 'blog_hyperlinked', 'headline', 'body_text', 'pub_date', 'mod_date',
136203
'authors', 'comments', 'comments_hyperlinked', 'featured', 'suggested',
137-
'suggested_hyperlinked', 'tags', 'featured_hyperlinked')
204+
'suggested_hyperlinked', 'tags', 'featured_hyperlinked', '_included')
138205
read_only_fields = ('tags',)
139206
meta_fields = ('body_format',)
140207

141208
class JSONAPIMeta:
142209
included_resources = ['comments']
143210

144-
145-
class AuthorTypeSerializer(serializers.ModelSerializer):
146-
class Meta:
147-
model = AuthorType
148-
fields = ('name', )
149-
150-
151-
class AuthorBioSerializer(serializers.ModelSerializer):
152-
class Meta:
153-
model = AuthorBio
154-
fields = ('author', 'body')
155-
156-
157-
class AuthorSerializer(serializers.ModelSerializer):
158-
bio = relations.ResourceRelatedField(
159-
related_link_view_name='author-related',
160-
self_link_view_name='author-relationships',
161-
queryset=AuthorBio.objects,
162-
)
163-
entries = relations.ResourceRelatedField(
164-
related_link_view_name='author-related',
165-
self_link_view_name='author-relationships',
166-
queryset=Entry.objects,
167-
many=True
168-
)
169-
first_entry = relations.SerializerMethodResourceRelatedField(
170-
related_link_view_name='author-related',
171-
self_link_view_name='author-relationships',
172-
model=Entry,
173-
read_only=True,
174-
source='get_first_entry'
175-
)
176-
included_serializers = {
177-
'bio': AuthorBioSerializer,
178-
'type': AuthorTypeSerializer
179-
}
180-
related_serializers = {
181-
'bio': 'example.serializers.AuthorBioSerializer',
182-
'entries': 'example.serializers.EntrySerializer',
183-
'first_entry': 'example.serializers.EntrySerializer'
184-
}
185-
186-
class Meta:
187-
model = Author
188-
fields = ('name', 'email', 'bio', 'entries', 'first_entry', 'type')
189-
190-
def get_first_entry(self, obj):
191-
return obj.entries.first()
192-
193-
194-
class WriterSerializer(serializers.ModelSerializer):
195-
included_serializers = {
196-
'bio': AuthorBioSerializer
197-
}
198-
199-
class Meta:
200-
model = Author
201-
fields = ('name', 'email', 'bio')
202-
resource_name = 'writers'
203-
204-
205211
class CommentSerializer(serializers.ModelSerializer):
206212
# testing remapping of related name
207213
writer = relations.ResourceRelatedField(source='author', read_only=True)

‎example/tests/test_generic_viewset.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,60 @@ def test_custom_validation_exceptions(self):
116116
})
117117

118118
assert expected == response.json()
119+
120+
def test_validation_exceptions_with_included(self):
121+
"""
122+
Exceptions should be able to be formatted manually
123+
"""
124+
expected_error = {
125+
'status': '400',
126+
'meta': {
127+
'source': '/included/authors/1/bio',
128+
},
129+
'detail': 'This field is required.',
130+
}
131+
response = self.client.post('/entries', dump_json({
132+
'data': {
133+
'type': 'posts',
134+
'id': 1,
135+
'attributes': {
136+
'headline': 'A headline',
137+
'body_text': 'A body text',
138+
},
139+
'relationships': {
140+
'blog': {
141+
'data': {
142+
'type': 'blogs',
143+
'id': 1,
144+
},
145+
},
146+
'authors': {
147+
'data': [{
148+
'type': 'authors',
149+
'id': 1,
150+
}]
151+
}
152+
},
153+
},
154+
'included': [
155+
{
156+
'type': 'blogs',
157+
'id': 1,
158+
'attributes': {
159+
'name': 'A blog name',
160+
'tagline': 'A blog tagline',
161+
},
162+
},
163+
{
164+
'type': 'authors',
165+
'id': 1,
166+
'attributes': {
167+
'name': 'Author name',
168+
'email': 'author@example.org',
169+
},
170+
},
171+
]
172+
}), content_type='application/vnd.api+json')
173+
174+
assert expected_error in json.loads(response.content)['errors']
175+

‎rest_framework_json_api/utils.py‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,25 @@ def __new__(self, url, name):
391391
is_hyperlink = True
392392

393393

394+
def _format_nested_error(obj, source, **kwargs):
395+
errors = []
396+
if isinstance(obj, dict):
397+
for field, value in obj.items():
398+
errors.extend(_format_nested_error(value, source='%s/%s' % (source, field),
399+
**kwargs))
400+
elif isinstance(obj, list):
401+
for value in obj:
402+
errors.extend(_format_nested_error(value, source=source, **kwargs))
403+
else:
404+
errors.append(dict({
405+
'detail': obj,
406+
'meta': {
407+
'source': source,
408+
},
409+
}, **kwargs))
410+
return errors
411+
412+
394413
def format_drf_errors(response, context, exc):
395414
errors = []
396415
# handle generic errors. ValidationError('test') in a view for example
@@ -410,7 +429,20 @@ def format_drf_errors(response, context, exc):
410429
pointer = '/data/attributes/{}'.format(field)
411430
# see if they passed a dictionary to ValidationError manually
412431
if isinstance(error, dict):
413-
errors.append(error)
432+
if field == '-included':
433+
for type, nested_errors in error.items():
434+
for index, nested_error in enumerate(nested_errors):
435+
try:
436+
identifier = '%s/%s' % (type, context['request'].data['_included'][type][index]['id'])
437+
except KeyError, IndexError:
438+
identifier = '%s' % type
439+
errors.extend(_format_nested_error(nested_error,
440+
status=encoding.force_text(response.status_code),
441+
source='/included/%s' % identifier))
442+
else:
443+
errors.extend(_format_nested_error(error,
444+
status=encoding.force_text(response.status_code),
445+
source='/data/%s' % field))
414446
elif isinstance(error, six.string_types):
415447
classes = inspect.getmembers(exceptions, inspect.isclass)
416448
# DRF sets the `field` to 'detail' for its own exceptions

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /