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 d174b3a

Browse files
riklaunimjerel
authored andcommitted
Correct error responses for projects with different DRF-configurations (django-json-api#222)
* [django-json-api#214] Add error messages tests. * [django-json-api#214] Extract formatting DRF errors. * Add example view with custom handle_exception. * Use HTTP 422 for validation error responses. * Add full example of class-configured json api view.
1 parent b2728e4 commit d174b3a

File tree

4 files changed

+139
-58
lines changed

4 files changed

+139
-58
lines changed

‎example/tests/test_views.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import json
22

3+
from django.test import RequestFactory
34
from django.utils import timezone
45
from rest_framework.reverse import reverse
56

67
from rest_framework.test import APITestCase
8+
from rest_framework.test import force_authenticate
79

810
from rest_framework_json_api.utils import format_relation_name
911
from example.models import Blog, Entry, Comment, Author
1012

13+
from .. import views
14+
from . import TestBase
15+
1116

1217
class TestRelationshipView(APITestCase):
1318
def setUp(self):
@@ -184,3 +189,33 @@ def test_delete_to_many_relationship_with_change(self):
184189
}
185190
response = self.client.delete(url, data=json.dumps(request_data), content_type='application/vnd.api+json')
186191
assert response.status_code == 200, response.content.decode()
192+
193+
194+
class TestValidationErrorResponses(TestBase):
195+
def test_if_returns_error_on_empty_post(self):
196+
view = views.BlogViewSet.as_view({'post': 'create'})
197+
response = self._get_create_response("{}", view)
198+
self.assertEqual(400, response.status_code)
199+
expected = [{'detail': 'Received document does not contain primary data', 'status': '400', 'source': {'pointer': '/data'}}]
200+
self.assertEqual(expected, response.data)
201+
202+
def test_if_returns_error_on_missing_form_data_post(self):
203+
view = views.BlogViewSet.as_view({'post': 'create'})
204+
response = self._get_create_response('{"data":{"attributes":{},"type":"blogs"}}', view)
205+
self.assertEqual(400, response.status_code)
206+
expected = [{'status': '400', 'detail': 'This field is required.', 'source': {'pointer': '/data/attributes/name'}}]
207+
self.assertEqual(expected, response.data)
208+
209+
def test_if_returns_error_on_bad_endpoint_name(self):
210+
view = views.BlogViewSet.as_view({'post': 'create'})
211+
response = self._get_create_response('{"data":{"attributes":{},"type":"bad"}}', view)
212+
self.assertEqual(409, response.status_code)
213+
expected = [{'detail': "The resource object's type (bad) is not the type that constitute the collection represented by the endpoint (blogs).", 'source': {'pointer': '/data'}, 'status': '409'}]
214+
self.assertEqual(expected, response.data)
215+
216+
def _get_create_response(self, data, view):
217+
factory = RequestFactory()
218+
request = factory.post('/', data, content_type='application/vnd.api+json')
219+
user = self.create_user('user', 'pass')
220+
force_authenticate(request, user)
221+
return view(request)

‎example/views.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,59 @@
1+
from rest_framework import exceptions
12
from rest_framework import viewsets
3+
import rest_framework.parsers
4+
import rest_framework.renderers
5+
import rest_framework_json_api.metadata
6+
import rest_framework_json_api.parsers
7+
import rest_framework_json_api.renderers
28
from rest_framework_json_api.views import RelationshipView
39
from example.models import Blog, Entry, Author, Comment
410
from example.serializers import (
511
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
612

13+
from rest_framework_json_api.utils import format_drf_errors
14+
15+
HTTP_422_UNPROCESSABLE_ENTITY = 422
16+
717

818
class BlogViewSet(viewsets.ModelViewSet):
919
queryset = Blog.objects.all()
1020
serializer_class = BlogSerializer
1121

1222

23+
class JsonApiViewSet(viewsets.ModelViewSet):
24+
"""
25+
This is an example on how to configure DRF-jsonapi from
26+
within a class. It allows using DRF-jsonapi alongside
27+
vanilla DRF API views.
28+
"""
29+
parser_classes = [
30+
rest_framework_json_api.parsers.JSONParser,
31+
rest_framework.parsers.FormParser,
32+
rest_framework.parsers.MultiPartParser,
33+
]
34+
renderer_classes = [
35+
rest_framework_json_api.renderers.JSONRenderer,
36+
rest_framework.renderers.BrowsableAPIRenderer,
37+
]
38+
metadata_class = rest_framework_json_api.metadata.JSONAPIMetadata
39+
40+
def handle_exception(self, exc):
41+
if isinstance(exc, exceptions.ValidationError):
42+
# some require that validation errors return 422 status
43+
# for example ember-data (isInvalid method on adapter)
44+
exc.status_code = HTTP_422_UNPROCESSABLE_ENTITY
45+
# exception handler can't be set on class so you have to
46+
# override the error response in this method
47+
response = super(JsonApiViewSet, self).handle_exception(exc)
48+
context = self.get_exception_handler_context()
49+
return format_drf_errors(response, context, exc)
50+
51+
52+
class BlogCustomViewSet(JsonApiViewSet):
53+
queryset = Blog.objects.all()
54+
serializer_class = BlogSerializer
55+
56+
1357
class EntryViewSet(viewsets.ModelViewSet):
1458
queryset = Entry.objects.all()
1559
resource_name = 'posts'
Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import inspect
2-
from django.utils import six, encoding
31
from django.utils.translation import ugettext_lazy as _
42
from rest_framework import status, exceptions
53

6-
from rest_framework_json_api.utils import format_value
4+
from rest_framework_json_api import utils
75

86

97
def exception_handler(exc, context):
@@ -18,63 +16,9 @@ def exception_handler(exc, context):
1816

1917
if not response:
2018
return response
21-
22-
errors = []
23-
# handle generic errors. ValidationError('test') in a view for example
24-
if isinstance(response.data, list):
25-
for message in response.data:
26-
errors.append({
27-
'detail': message,
28-
'source': {
29-
'pointer': '/data',
30-
},
31-
'status': encoding.force_text(response.status_code),
32-
})
33-
# handle all errors thrown from serializers
34-
else:
35-
for field, error in response.data.items():
36-
field = format_value(field)
37-
pointer = '/data/attributes/{}'.format(field)
38-
# see if they passed a dictionary to ValidationError manually
39-
if isinstance(error, dict):
40-
errors.append(error)
41-
elif isinstance(error, six.string_types):
42-
classes = inspect.getmembers(exceptions, inspect.isclass)
43-
# DRF sets the `field` to 'detail' for its own exceptions
44-
if isinstance(exc, tuple(x[1] for x in classes)):
45-
pointer = '/data'
46-
errors.append({
47-
'detail': error,
48-
'source': {
49-
'pointer': pointer,
50-
},
51-
'status': encoding.force_text(response.status_code),
52-
})
53-
elif isinstance(error, list):
54-
for message in error:
55-
errors.append({
56-
'detail': message,
57-
'source': {
58-
'pointer': pointer,
59-
},
60-
'status': encoding.force_text(response.status_code),
61-
})
62-
else:
63-
errors.append({
64-
'detail': error,
65-
'source': {
66-
'pointer': pointer,
67-
},
68-
'status': encoding.force_text(response.status_code),
69-
})
70-
71-
72-
context['view'].resource_name = 'errors'
73-
response.data = errors
74-
return response
19+
return utils.format_drf_errors(response, context, exc)
7520

7621

7722
class Conflict(exceptions.APIException):
7823
status_code = status.HTTP_409_CONFLICT
7924
default_detail = _('Conflict.')
80-

‎rest_framework_json_api/utils.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
"""
44
import copy
55
from collections import OrderedDict
6+
import inspect
67

78
import inflection
89
from django.conf import settings
10+
from django.utils import encoding
911
from django.utils import six
1012
from django.utils.module_loading import import_string as import_class_from_dotted_path
1113
from django.utils.translation import ugettext_lazy as _
1214
from rest_framework.exceptions import APIException
15+
from rest_framework import exceptions
1316

1417
try:
1518
from rest_framework.serializers import ManyRelatedField
@@ -249,3 +252,58 @@ def __new__(self, url, name):
249252
return ret
250253

251254
is_hyperlink = True
255+
256+
257+
def format_drf_errors(response, context, exc):
258+
errors = []
259+
# handle generic errors. ValidationError('test') in a view for example
260+
if isinstance(response.data, list):
261+
for message in response.data:
262+
errors.append({
263+
'detail': message,
264+
'source': {
265+
'pointer': '/data',
266+
},
267+
'status': encoding.force_text(response.status_code),
268+
})
269+
# handle all errors thrown from serializers
270+
else:
271+
for field, error in response.data.items():
272+
field = format_value(field)
273+
pointer = '/data/attributes/{}'.format(field)
274+
# see if they passed a dictionary to ValidationError manually
275+
if isinstance(error, dict):
276+
errors.append(error)
277+
elif isinstance(error, six.string_types):
278+
classes = inspect.getmembers(exceptions, inspect.isclass)
279+
# DRF sets the `field` to 'detail' for its own exceptions
280+
if isinstance(exc, tuple(x[1] for x in classes)):
281+
pointer = '/data'
282+
errors.append({
283+
'detail': error,
284+
'source': {
285+
'pointer': pointer,
286+
},
287+
'status': encoding.force_text(response.status_code),
288+
})
289+
elif isinstance(error, list):
290+
for message in error:
291+
errors.append({
292+
'detail': message,
293+
'source': {
294+
'pointer': pointer,
295+
},
296+
'status': encoding.force_text(response.status_code),
297+
})
298+
else:
299+
errors.append({
300+
'detail': error,
301+
'source': {
302+
'pointer': pointer,
303+
},
304+
'status': encoding.force_text(response.status_code),
305+
})
306+
307+
context['view'].resource_name = 'errors'
308+
response.data = errors
309+
return response

0 commit comments

Comments
(0)

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