Skip to content

Correct error responses for projects with different DRF-configurations #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions example/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import json

from django.test import RequestFactory
from django.utils import timezone
from rest_framework.reverse import reverse

from rest_framework.test import APITestCase
from rest_framework.test import force_authenticate

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

from .. import views
from . import TestBase


class TestRelationshipView(APITestCase):
def setUp(self):
Expand Down Expand Up @@ -184,3 +189,33 @@ def test_delete_to_many_relationship_with_change(self):
}
response = self.client.delete(url, data=json.dumps(request_data), content_type='application/vnd.api+json')
assert response.status_code == 200, response.content.decode()


class TestValidationErrorResponses(TestBase):
def test_if_returns_error_on_empty_post(self):
view = views.BlogViewSet.as_view({'post': 'create'})
response = self._get_create_response("{}", view)
self.assertEqual(400, response.status_code)
expected = [{'detail': 'Received document does not contain primary data', 'status': '400', 'source': {'pointer': '/data'}}]
self.assertEqual(expected, response.data)

def test_if_returns_error_on_missing_form_data_post(self):
view = views.BlogViewSet.as_view({'post': 'create'})
response = self._get_create_response('{"data":{"attributes":{},"type":"blogs"}}', view)
self.assertEqual(400, response.status_code)
expected = [{'status': '400', 'detail': 'This field is required.', 'source': {'pointer': '/data/attributes/name'}}]
self.assertEqual(expected, response.data)

def test_if_returns_error_on_bad_endpoint_name(self):
view = views.BlogViewSet.as_view({'post': 'create'})
response = self._get_create_response('{"data":{"attributes":{},"type":"bad"}}', view)
self.assertEqual(409, response.status_code)
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'}]
self.assertEqual(expected, response.data)

def _get_create_response(self, data, view):
factory = RequestFactory()
request = factory.post('/', data, content_type='application/vnd.api+json')
user = self.create_user('user', 'pass')
force_authenticate(request, user)
return view(request)
44 changes: 44 additions & 0 deletions example/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,59 @@
from rest_framework import exceptions
from rest_framework import viewsets
import rest_framework.parsers
import rest_framework.renderers
import rest_framework_json_api.metadata
import rest_framework_json_api.parsers
import rest_framework_json_api.renderers
from rest_framework_json_api.views import RelationshipView
from example.models import Blog, Entry, Author, Comment
from example.serializers import (
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)

from rest_framework_json_api.utils import format_drf_errors

HTTP_422_UNPROCESSABLE_ENTITY = 422


class BlogViewSet(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer


class JsonApiViewSet(viewsets.ModelViewSet):
"""
This is an example on how to configure DRF-jsonapi from
within a class. It allows using DRF-jsonapi alongside
vanilla DRF API views.
"""
parser_classes = [
rest_framework_json_api.parsers.JSONParser,
rest_framework.parsers.FormParser,
rest_framework.parsers.MultiPartParser,
]
renderer_classes = [
rest_framework_json_api.renderers.JSONRenderer,
rest_framework.renderers.BrowsableAPIRenderer,
]
metadata_class = rest_framework_json_api.metadata.JSONAPIMetadata

def handle_exception(self, exc):
if isinstance(exc, exceptions.ValidationError):
# some require that validation errors return 422 status
# for example ember-data (isInvalid method on adapter)
exc.status_code = HTTP_422_UNPROCESSABLE_ENTITY
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good example for setting it manually. jsonapi.org says they don't take a stand on 400 vs 422 so the Ember addons should really support both in my opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a method in ember adapter where the code can be customized.

# exception handler can't be set on class so you have to
# override the error response in this method
response = super(JsonApiViewSet, self).handle_exception(exc)
context = self.get_exception_handler_context()
return format_drf_errors(response, context, exc)


class BlogCustomViewSet(JsonApiViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer


class EntryViewSet(viewsets.ModelViewSet):
queryset = Entry.objects.all()
resource_name = 'posts'
Expand Down
60 changes: 2 additions & 58 deletions rest_framework_json_api/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import inspect
from django.utils import six, encoding
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, exceptions

from rest_framework_json_api.utils import format_value
from rest_framework_json_api import utils


def exception_handler(exc, context):
Expand All @@ -18,63 +16,9 @@ def exception_handler(exc, context):

if not response:
return response

errors = []
# handle generic errors. ValidationError('test') in a view for example
if isinstance(response.data, list):
for message in response.data:
errors.append({
'detail': message,
'source': {
'pointer': '/data',
},
'status': encoding.force_text(response.status_code),
})
# handle all errors thrown from serializers
else:
for field, error in response.data.items():
field = format_value(field)
pointer = '/data/attributes/{}'.format(field)
# see if they passed a dictionary to ValidationError manually
if isinstance(error, dict):
errors.append(error)
elif isinstance(error, six.string_types):
classes = inspect.getmembers(exceptions, inspect.isclass)
# DRF sets the `field` to 'detail' for its own exceptions
if isinstance(exc, tuple(x[1] for x in classes)):
pointer = '/data'
errors.append({
'detail': error,
'source': {
'pointer': pointer,
},
'status': encoding.force_text(response.status_code),
})
elif isinstance(error, list):
for message in error:
errors.append({
'detail': message,
'source': {
'pointer': pointer,
},
'status': encoding.force_text(response.status_code),
})
else:
errors.append({
'detail': error,
'source': {
'pointer': pointer,
},
'status': encoding.force_text(response.status_code),
})


context['view'].resource_name = 'errors'
response.data = errors
return response
return utils.format_drf_errors(response, context, exc)


class Conflict(exceptions.APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = _('Conflict.')

58 changes: 58 additions & 0 deletions rest_framework_json_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
"""
import copy
from collections import OrderedDict
import inspect

import inflection
from django.conf import settings
from django.utils import encoding
from django.utils import six
from django.utils.module_loading import import_string as import_class_from_dotted_path
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import APIException
from rest_framework import exceptions

try:
from rest_framework.serializers import ManyRelatedField
Expand Down Expand Up @@ -249,3 +252,58 @@ def __new__(self, url, name):
return ret

is_hyperlink = True


def format_drf_errors(response, context, exc):
errors = []
# handle generic errors. ValidationError('test') in a view for example
if isinstance(response.data, list):
for message in response.data:
errors.append({
'detail': message,
'source': {
'pointer': '/data',
},
'status': encoding.force_text(response.status_code),
})
# handle all errors thrown from serializers
else:
for field, error in response.data.items():
field = format_value(field)
pointer = '/data/attributes/{}'.format(field)
# see if they passed a dictionary to ValidationError manually
if isinstance(error, dict):
errors.append(error)
elif isinstance(error, six.string_types):
classes = inspect.getmembers(exceptions, inspect.isclass)
# DRF sets the `field` to 'detail' for its own exceptions
if isinstance(exc, tuple(x[1] for x in classes)):
pointer = '/data'
errors.append({
'detail': error,
'source': {
'pointer': pointer,
},
'status': encoding.force_text(response.status_code),
})
elif isinstance(error, list):
for message in error:
errors.append({
'detail': message,
'source': {
'pointer': pointer,
},
'status': encoding.force_text(response.status_code),
})
else:
errors.append({
'detail': error,
'source': {
'pointer': pointer,
},
'status': encoding.force_text(response.status_code),
})

context['view'].resource_name = 'errors'
response.data = errors
return response