Skip to content

Commit 93406e3

Browse files
committed
Merge pull request #44 from django-json-api/feature/errors
Added error formating and fixed the tests
2 parents 1179637 + 3cb57d8 commit 93406e3

File tree

12 files changed

+196
-38
lines changed

12 files changed

+196
-38
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ language: python
22
sudo: false
33
python:
44
- "2.7"
5+
- "3.3"
6+
- "3.4"
57
install:
68
- pip install -e .
79
script: python runtests.py

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ requests and responses from the python/rest_framework's preferred underscore to
155155
a format of your choice. To hook this up include the following in your project
156156
settings::
157157

158-
JSON_API_FORMAT_KEYS = True
158+
JSON_API_FORMAT_KEYS = 'dasherize'
159159

160160
Note: due to the way the inflector works address_1 can camelize to address1
161161
on output but it cannot convert address1 back to address_1 on POST or PUT. Keep

example/api/resources/identity.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.contrib.auth import models as auth_models
2-
from rest_framework import viewsets, generics, renderers, parsers
2+
from rest_framework import viewsets, generics, renderers, parsers, serializers
33
from rest_framework.decorators import list_route, detail_route
44
from rest_framework.response import Response
55
from rest_framework_json_api import mixins, utils
@@ -41,6 +41,10 @@ def manual_resource_name(self, request, *args, **kwargs):
4141
self.resource_name = 'data'
4242
return super(Identity, self).retrieve(request, args, kwargs)
4343

44+
@detail_route()
45+
def validation(self, request, *args, **kwargs):
46+
raise serializers.ValidationError('Oh nohs!')
47+
4448

4549
class GenericIdentity(generics.GenericAPIView):
4650
"""
@@ -63,4 +67,3 @@ def get(self, request, pk=None):
6367
"""
6468
obj = self.get_object()
6569
return Response(IdentitySerializer(obj).data)
66-

example/api/serializers/identity.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,24 @@ class IdentitySerializer(serializers.ModelSerializer):
66
"""
77
Identity Serializer
88
"""
9+
def validate_first_name(self, data):
10+
if len(data) > 10:
11+
raise serializers.ValidationError(
12+
'There\'s a problem with first name')
13+
return data
14+
15+
def validate_last_name(self, data):
16+
if len(data) > 10:
17+
raise serializers.ValidationError(
18+
{
19+
'id': 'armageddon101',
20+
'detail': 'Hey! You need a last name!',
21+
'meta': 'something',
22+
}
23+
)
24+
return data
25+
926
class Meta:
1027
model = auth_models.User
1128
fields = (
1229
'id', 'first_name', 'last_name', 'email', )
13-

example/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
'PAGINATE_BY': 1,
3939
'PAGINATE_BY_PARAM': 'page_size',
4040
'MAX_PAGINATE_BY': 100,
41+
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
4142
# DRF v3.1+
4243
'DEFAULT_PAGINATION_CLASS':
4344
'rest_framework_json_api.pagination.PageNumberPagination',

example/tests/test_format_keys.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ def setUp(self):
1818
self.detail_url = reverse('user-detail', kwargs={'pk': self.miles.pk})
1919

2020
# Set the format keys settings.
21-
setattr(settings, 'JSON_API_FORMAT_KEYS', True)
21+
setattr(settings, 'JSON_API_FORMAT_KEYS', 'camelization')
2222

2323
def tearDown(self):
2424
# Remove the format keys settings.
25-
delattr(settings, 'JSON_API_FORMAT_KEYS')
25+
setattr(settings, 'JSON_API_FORMAT_KEYS', 'dasherize')
2626

2727

2828
def test_camelization(self):
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import json
2+
from example.tests import TestBase
3+
from django.core.urlresolvers import reverse
4+
from django.conf import settings
5+
from rest_framework.serializers import ValidationError
6+
7+
8+
class GenericValidationTest(TestBase):
9+
"""
10+
Test that a non serializer specific validation can be thrown and formatted
11+
"""
12+
def setUp(self):
13+
super(GenericValidationTest, self).setUp()
14+
self.url = reverse('user-validation', kwargs={'pk': self.miles.pk})
15+
16+
def test_generic_validation_error(self):
17+
"""
18+
Check error formatting
19+
"""
20+
response = self.client.get(self.url)
21+
self.assertEqual(response.status_code, 400)
22+
23+
result = json.loads(response.content.decode('utf8'))
24+
expected = {
25+
'errors': [{
26+
'status': '400',
27+
'source': {
28+
'pointer': '/data'
29+
},
30+
'detail': 'Oh nohs!'
31+
}]
32+
}
33+
self.assertEqual(result, expected)

example/tests/test_generic_viewset.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,63 @@ def test_ember_expected_renderer(self):
4040
json.loads(response.content.decode('utf8')),
4141
{
4242
'data': {
43-
'id': 2,
44-
'first_name': u'Miles',
45-
'last_name': u'Davis',
46-
'email': u'miles@example.com'
43+
'type': 'data',
44+
'id': '2',
45+
'attributes': {
46+
'first-name': u'Miles',
47+
'last-name': u'Davis',
48+
'email': u'miles@example.com'
49+
}
4750
}
4851
}
4952
)
5053

54+
def test_default_validation_exceptions(self):
55+
"""
56+
Default validation exceptions should conform to json api spec
57+
"""
58+
expected = {
59+
'errors': [
60+
{
61+
'status': '400',
62+
'source': {
63+
'pointer': '/data/attributes/email',
64+
},
65+
'detail': 'Enter a valid email address.',
66+
},
67+
{
68+
'status': '400',
69+
'source': {
70+
'pointer': '/data/attributes/first-name',
71+
},
72+
'detail': 'There\'s a problem with first name',
73+
}
74+
]
75+
}
76+
response = self.client.post('/identities', {
77+
'email': 'bar', 'first_name': 'alajflajaljalajlfjafljalj'})
78+
self.assertEqual(json.loads(response.content.decode('utf8')), expected)
5179

80+
def test_custom_validation_exceptions(self):
81+
"""
82+
Exceptions should be able to be formatted manually
83+
"""
84+
expected = {
85+
'errors': [
86+
{
87+
'id': 'armageddon101',
88+
'detail': 'Hey! You need a last name!',
89+
'meta': 'something',
90+
},
91+
{
92+
'status': '400',
93+
'source': {
94+
'pointer': '/data/attributes/email',
95+
},
96+
'detail': 'Enter a valid email address.',
97+
},
98+
]
99+
}
100+
response = self.client.post('/identities', {
101+
'email': 'bar', 'last_name': 'alajflajaljalajlfjafljalj'})
102+
self.assertEqual(json.loads(response.content.decode('utf8')), expected)

example/tests/test_multiple_id_mixin.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from example.tests import TestBase
3+
from django.utils import encoding
34
from django.contrib.auth import get_user_model
45
from django.core.urlresolvers import reverse
56
from django.conf import settings
@@ -23,21 +24,24 @@ def test_single_id_in_query_params(self):
2324
self.assertEqual(response.status_code, 200)
2425

2526
expected = {
26-
'user': [{
27-
'id': self.miles.pk,
28-
'first_name': self.miles.first_name,
29-
'last_name': self.miles.last_name,
30-
'email': self.miles.email
31-
}]
27+
'data': {
28+
'type': 'users',
29+
'id': encoding.force_text(self.miles.pk),
30+
'attributes': {
31+
'first_name': self.miles.first_name,
32+
'last_name': self.miles.last_name,
33+
'email': self.miles.email
34+
}
35+
}
3236
}
3337

3438
json_content = json.loads(response.content.decode('utf8'))
35-
meta = json_content.get("meta")
39+
links = json_content.get("links")
40+
meta = json_content.get("meta").get('pagination')
3641

3742
self.assertEquals(expected.get('user'), json_content.get('user'))
3843
self.assertEquals(meta.get('count', 0), 1)
39-
self.assertEquals(meta.get("next"), None)
40-
self.assertEqual(None, meta.get("next_link"))
44+
self.assertEquals(links.get("next"), None)
4145
self.assertEqual(meta.get("page"), 1)
4246

4347
def test_multiple_ids_in_query_params(self):
@@ -50,28 +54,29 @@ def test_multiple_ids_in_query_params(self):
5054
self.assertEqual(response.status_code, 200)
5155

5256
expected = {
53-
'user': [{
54-
'id': self.john.pk,
55-
'first_name': self.john.first_name,
56-
'last_name': self.john.last_name,
57-
'email': self.john.email
58-
}]
57+
'data': {
58+
'type': 'users',
59+
'id': encoding.force_text(self.john.pk),
60+
'attributes': {
61+
'first_name': self.john.first_name,
62+
'last_name': self.john.last_name,
63+
'email': self.john.email
64+
}
65+
}
5966
}
6067

6168
json_content = json.loads(response.content.decode('utf8'))
62-
meta = json_content.get("meta")
69+
links = json_content.get("links")
70+
meta = json_content.get("meta").get('pagination')
6371

6472
self.assertEquals(expected.get('user'), json_content.get('user'))
6573
self.assertEquals(meta.get('count', 0), 2)
66-
self.assertEquals(meta.get("next"), 2)
6774
self.assertEqual(
6875
sorted(
6976
'http://testserver/identities?ids%5B%5D=2&ids%5B%5D=1&page=2'\
7077
.split('?')[1].split('&')
7178
),
7279
sorted(
73-
meta.get("next_link").split('?')[1].split('&'))
80+
links.get("next").split('?')[1].split('&'))
7481
)
7582
self.assertEqual(meta.get("page"), 1)
76-
77-

rest_framework_json_api/exceptions.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
from django.utils import six, encoding
3+
from rest_framework.views import exception_handler as drf_exception_handler
4+
from rest_framework_json_api.utils import format_value
5+
6+
7+
def exception_handler(exc, context):
8+
response = drf_exception_handler(exc, context)
9+
10+
errors = []
11+
# handle generic errors. ValidationError('test') in a view for example
12+
if isinstance(response.data, list):
13+
for message in response.data:
14+
errors.append({
15+
'detail': message,
16+
'source': {
17+
'pointer': '/data',
18+
},
19+
'status': encoding.force_text(response.status_code),
20+
})
21+
# handle all errors thrown from serializers
22+
else:
23+
for field, error in response.data.items():
24+
field = format_value(field)
25+
pointer = '/data/attributes/{}'.format(field)
26+
# see if they passed a dictionary to ValidationError manually
27+
if isinstance(error, dict):
28+
errors.append(error)
29+
else:
30+
for message in error:
31+
errors.append({
32+
'detail': message,
33+
'source': {
34+
'pointer': pointer,
35+
},
36+
'status': encoding.force_text(response.status_code),
37+
})
38+
39+
context['view'].resource_name = 'errors'
40+
response.data = errors
41+
return response

0 commit comments

Comments
 (0)