Skip to content

Commit f5ea1de

Browse files
author
Eric Honkanen
committed
Refactored renderer, updated parser, added error handling to return
proper error response. Renamed package to json api.
1 parent 24f37a3 commit f5ea1de

File tree

11 files changed

+190
-35
lines changed

11 files changed

+190
-35
lines changed

example/__init__.py

Whitespace-only changes.

example/api/__init__.py

Whitespace-only changes.

example/api/resources/__init__.py

Whitespace-only changes.

example/api/resources/identity.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from django.contrib.auth import models as auth_models
2+
from rest_framework import viewsets, generics, renderers, parsers
3+
from rest_framework.decorators import list_route, detail_route
4+
from rest_framework.response import Response
5+
from rest_framework_ember import mixins, utils
6+
from ..serializers.identity import IdentitySerializer
7+
from ..serializers.post import PostSerializer
8+
9+
10+
class Identity(mixins.MultipleIDMixin, viewsets.ModelViewSet):
11+
queryset = auth_models.User.objects.all()
12+
serializer_class = IdentitySerializer
13+
14+
@list_route()
15+
def empty_list(self, request):
16+
"""
17+
This is a hack/workaround to return an empty result on a list
18+
endpoint because the delete operation in the test_empty_pluralization
19+
test doesn't prevent the /identities endpoint from still returning
20+
records when called in the same test. Suggestions welcome.
21+
"""
22+
self.queryset = self.queryset.filter(pk=None)
23+
return super(Identity, self).list(request)
24+
25+
# demonstrate sideloading data for use at app boot time
26+
@list_route()
27+
def posts(self, request):
28+
self.resource_name = False
29+
30+
identities = self.queryset
31+
posts = [{'id': 1, 'title': 'Test Blog Post'}]
32+
33+
data = {
34+
u'identities': IdentitySerializer(identities, many=True).data,
35+
u'posts': PostSerializer(posts, many=True).data,
36+
}
37+
return Response(utils.format_keys(data, format_type='camelize'))
38+
39+
@detail_route()
40+
def manual_resource_name(self, request, *args, **kwargs):
41+
self.resource_name = 'data'
42+
return super(Identity, self).retrieve(request, args, kwargs)
43+
44+
45+
class GenericIdentity(generics.GenericAPIView):
46+
"""
47+
An endpoint that uses DRF's default format so we can test that.
48+
49+
GET /identities/generic
50+
"""
51+
serializer_class = IdentitySerializer
52+
allowed_methods = ['GET']
53+
renderer_classes = (renderers.JSONRenderer, )
54+
parser_classes = (parsers.JSONParser, )
55+
56+
57+
def get_queryset(self):
58+
return auth_models.User.objects.all()
59+
60+
def get(self, request, pk=None):
61+
"""
62+
GET request
63+
"""
64+
obj = self.get_object()
65+
return Response(IdentitySerializer(obj).data)
66+

example/api/serializers/__init__.py

Whitespace-only changes.

example/api/serializers/identity.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.contrib.auth import models as auth_models
2+
from rest_framework import serializers
3+
4+
5+
class IdentitySerializer(serializers.ModelSerializer):
6+
"""
7+
Identity Serializer
8+
"""
9+
class Meta:
10+
model = auth_models.User
11+
fields = (
12+
'id', 'first_name', 'last_name', 'email', )
13+

example/api/serializers/post.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from rest_framework import serializers
2+
3+
4+
class PostSerializer(serializers.Serializer):
5+
"""
6+
Blog post serializer
7+
"""
8+
title = serializers.CharField(max_length=50)
9+

example/tests/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
from django.contrib.auth import get_user_model
3+
from rest_framework.test import APITestCase
4+
5+
6+
class TestBase(APITestCase):
7+
"""
8+
Test base class to setup a couple users.
9+
"""
10+
def setUp(self):
11+
"""
12+
Create those users
13+
"""
14+
super(TestBase, self).setUp()
15+
self.create_users()
16+
17+
def create_user(self, username, email, password="pw",
18+
first_name='', last_name=''):
19+
"""
20+
Helper method to create a user
21+
"""
22+
User = get_user_model()
23+
user = User.objects.create_user(
24+
username, email, password=password
25+
)
26+
if first_name or last_name:
27+
user.first_name = first_name
28+
user.last_name = last_name
29+
user.save()
30+
return user
31+
32+
def create_users(self):
33+
"""
34+
Create a couple users
35+
"""
36+
self.john = self.create_user(
37+
'trane', '[email protected]',
38+
first_name='John', last_name="Coltrane")
39+
self.miles = self.create_user(
40+
'miles', '[email protected]',
41+
first_name="Miles", last_name="Davis")
42+

rest_framework_json_api/parsers.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
Parsers
33
"""
44
from rest_framework import parsers
5-
from rest_framework_ember.utils import get_resource_name
65

7-
from .utils import format_keys
6+
from . import utils
87

98

109
class JSONParser(parsers.JSONParser):
@@ -28,13 +27,5 @@ def parse(self, stream, media_type=None, parser_context=None):
2827
"""
2928
result = super(JSONParser, self).parse(stream, media_type=media_type,
3029
parser_context=parser_context)
31-
resource = result.get(get_resource_name(parser_context.get('view', None)))
32-
return format_keys(resource, 'underscore')
33-
34-
35-
class EmberJSONParser(JSONParser):
36-
"""
37-
Backward compatability for our first uniquely named parser
38-
"""
39-
pass
40-
30+
resource = result.get(utils.get_resource_name(parser_context))
31+
return utils.format_keys(resource, 'underscore')

rest_framework_json_api/renderers.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
Renderers
33
"""
44
from rest_framework import renderers
5-
from rest_framework_ember.utils import get_resource_name
65

7-
from .utils import format_keys, format_resource_name
6+
from . import utils
87

98

109
class JSONRenderer(renderers.JSONRenderer):
@@ -20,27 +19,43 @@ class JSONRenderer(renderers.JSONRenderer):
2019
}
2120
"""
2221
def render(self, data, accepted_media_type=None, renderer_context=None):
23-
view = renderer_context.get('view')
24-
resource_name = get_resource_name(view)
22+
# Get the resource name.
23+
resource_name = utils.get_resource_name(view)
2524

26-
if resource_name == False:
25+
# If no `resource_name` is found, render the default response.
26+
if not resource_name:
2727
return super(JSONRenderer, self).render(
28-
data, accepted_media_type, renderer_context)
28+
data, accepted_media_type, renderer_context
29+
)
2930

30-
data = format_keys(data, 'camelize')
31-
32-
try:
33-
content = data.pop('results')
34-
resource_name = format_resource_name(content, resource_name)
35-
data = {resource_name : content, "meta" : data}
36-
except (TypeError, KeyError, AttributeError) as e:
37-
38-
# Default behavior
39-
if not resource_name == 'data':
40-
format_keys(data, 'camelize')
41-
resource_name = format_resource_name(data, resource_name)
42-
43-
data = {resource_name : data}
31+
# If this is an error response, skip the rest.
32+
if 'errors' in resource_name or resource_name == 'data':
33+
return super(JSONRenderer, self).render(
34+
{resource_name: data}, accepted_media_type, renderer_context
35+
)
36+
37+
# Camelize the keynames.
38+
formatted_data = utils.format_keys(data, 'camelize')
39+
40+
# Check if it's paginated data and contains a `results` key.
41+
results = (formatted_data.get('results')
42+
if isinstance(formatted_data, dict) else None)
43+
44+
# Pluralize the resource_name.
45+
resource_name = utils.format_resource_name(
46+
results or formatted_data, resource_name
47+
)
48+
49+
if results:
50+
rendered_data = {
51+
resource_name: results,
52+
'meta': formatted_data
53+
}
54+
else:
55+
rendered_data = {
56+
resource_name: formatted_data
57+
}
4458

4559
return super(JSONRenderer, self).render(
46-
data, accepted_media_type, renderer_context)
60+
rendered_data, accepted_media_type, renderer_context
61+
)

rest_framework_json_api/utils.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,38 @@
33
"""
44
import inflection
55

6+
from rest_framework.exceptions import APIException
7+
68
from django.conf import settings
79
from django.utils import six
10+
from django.utils.translation import ugettext_lazy as _
811

912
try:
1013
from rest_framework.compat import OrderedDict
1114
except ImportError:
1215
OrderedDict = dict
1316

1417

15-
def get_resource_name(view):
18+
def get_resource_name(context):
1619
"""
1720
Return the name of a resource.
1821
"""
22+
view = context.get('view')
23+
24+
# Sanity check to make sure we have a view.
25+
if not view:
26+
raise APIException(_('Could not find view.'))
27+
28+
# Check to see if there is a status code and return early
29+
# with the resource_name value of `errors`.
30+
try:
31+
code = str(view.response.status_code)
32+
except (AttributeError, ValueError):
33+
pass
34+
else:
35+
if code.startswith('4') or code.startswith('5'):
36+
return 'errors'
37+
1938
try:
2039
# Check the view
2140
resource_name = getattr(view, 'resource_name')
@@ -75,7 +94,7 @@ def format_resource_name(obj, name):
7594
"""
7695
Pluralize the resource name if more than one object in results.
7796
"""
78-
if (getattr(settings, 'REST_EMBER_PLURALIZE_KEYS', False)
97+
if (getattr(settings, 'REST_EMBER_PLURALIZE_KEYS')
7998
and isinstance(obj, list)):
8099

81100
return inflection.pluralize(name)

0 commit comments

Comments
 (0)