Skip to content

Commit 8c4db64

Browse files
committed
Merge branch 'develop' of github.com:django-json-api/django-rest-framework-json-api
2 parents fa266f1 + 6bf944b commit 8c4db64

14 files changed

+250
-46
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
v2.0.0-beta.2
3+
4+
* Added JSONAPIMeta class option to models for overriding `resource_name`. #197
5+

README.rst

+8-12
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ JSON API and Django Rest Framework
44

55
.. image:: https://travis-ci.org/django-json-api/django-rest-framework-json-api.svg?branch=develop
66
:target: https://travis-ci.org/django-json-api/django-rest-framework-json-api
7-
7+
88
.. image:: https://readthedocs.org/projects/django-rest-framework-json-api/badge/?version=latest
99
:alt: Read the docs
1010
:target: http://django-rest-framework-json-api.readthedocs.org/
11-
11+
1212
.. image:: https://codeclimate.com/github/django-json-api/django-rest-framework-json-api/badges/gpa.svg
1313
:target: https://codeclimate.com/github/django-json-api/django-rest-framework-json-api
1414
:alt: Code Climate
15-
15+
1616
.. image:: https://badges.gitter.im/Join%20Chat.svg
1717
:alt: Join the chat at https://gitter.im/django-json-api/django-rest-framework-json-api
1818
:target: https://gitter.im/django-json-api/django-rest-framework-json-api
1919

20-
20+
2121
Documentation: http://django-rest-framework-json-api.readthedocs.org/
2222

2323
Live demo (resets every hour): http://json-api.jerel.co/
@@ -126,19 +126,15 @@ Settings
126126

127127
One can either add ``rest_framework_json_api.parsers.JSONParser`` and
128128
``rest_framework_json_api.renderers.JSONRenderer`` to each ``ViewSet`` class, or
129-
override ``settings.REST_FRAMEWORK``::
129+
override ``settings.REST_FRAMEWORK``
130130

131+
::
131132

132133
REST_FRAMEWORK = {
133-
'PAGINATE_BY': 10,
134-
'PAGINATE_BY_PARAM': 'page_size',
135-
'MAX_PAGINATE_BY': 100,
136-
# DRF v3.1+
134+
'PAGE_SIZE': 10,
135+
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
137136
'DEFAULT_PAGINATION_CLASS':
138137
'rest_framework_json_api.pagination.PageNumberPagination',
139-
# older than DRF v3.1
140-
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
141-
'rest_framework_json_api.pagination.PaginationSerializer',
142138
'DEFAULT_PARSER_CLASSES': (
143139
'rest_framework_json_api.parsers.JSONParser',
144140
'rest_framework.parsers.FormParser',

docs/usage.md

+25-3
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@ per request via the `PAGINATE_BY_PARAM` query parameter (`page_size` by default)
3636

3737
### Setting the resource_name
3838

39-
You may manually set the `resource_name` property on views or serializers to
40-
specify the `type` key in the json output. It is automatically set for you as the
41-
plural of the view or model name except on resources that do not subclass
39+
You may manually set the `resource_name` property on views, serializers, or
40+
models to specify the `type` key in the json output. In the case of setting the
41+
`resource_name` property for models you must include the property inside a
42+
`JSONAPIMeta` class on the model. It is automatically set for you as the plural
43+
of the view or model name except on resources that do not subclass
4244
`rest_framework.viewsets.ModelViewSet`:
45+
46+
47+
Example - `resource_name` on View:
4348
``` python
4449
class Me(generics.GenericAPIView):
4550
"""
@@ -56,6 +61,23 @@ If you set the `resource_name` property on the object to `False` the data
5661
will be returned without modification.
5762

5863

64+
Example - `resource_name` on Model:
65+
``` python
66+
class Me(models.Model):
67+
"""
68+
A simple model
69+
"""
70+
name = models.CharField(max_length=100)
71+
72+
class JSONAPIMeta:
73+
resource_name = "users"
74+
```
75+
If you set the `resource_name` on a combination of model, serializer, or view
76+
in the same hierarchy, the name will be resolved as following: view >
77+
serializer > model. (Ex: A view `resource_name` will always override a
78+
`resource_name` specified on a serializer or model)
79+
80+
5981
### Inflecting object and relation keys
6082

6183
This package includes the ability (off by default) to automatically convert json

example/serializers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def get_body_format(self, obj):
5151
class Meta:
5252
model = Entry
5353
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
54-
'authors', 'comments', 'suggested',)
54+
'authors', 'comments', 'suggested',)
5555
meta_fields = ('body_format',)
5656

5757

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import pytest
2+
from django.core.urlresolvers import reverse
3+
4+
from example.tests.utils import load_json
5+
6+
from example import models, serializers, views
7+
pytestmark = pytest.mark.django_db
8+
9+
10+
class _PatchedModel:
11+
class JSONAPIMeta:
12+
resource_name = "resource_name_from_JSONAPIMeta"
13+
14+
15+
def _check_resource_and_relationship_comment_type_match(django_client):
16+
entry_response = django_client.get(reverse("entry-list"))
17+
comment_response = django_client.get(reverse("comment-list"))
18+
19+
comment_resource_type = load_json(comment_response.content).get('data')[0].get('type')
20+
comment_relationship_type = load_json(entry_response.content).get(
21+
'data')[0].get('relationships').get('comments').get('data')[0].get('type')
22+
23+
assert comment_resource_type == comment_relationship_type, "The resource type seen in the relationships and head resource do not match"
24+
25+
26+
def _check_relationship_and_included_comment_type_are_the_same(django_client, url):
27+
response = django_client.get(url + "?include=comments")
28+
data = load_json(response.content).get('data')[0]
29+
comment = load_json(response.content).get('included')[0]
30+
31+
comment_relationship_type = data.get('relationships').get('comments').get('data')[0].get('type')
32+
comment_included_type = comment.get('type')
33+
34+
assert comment_relationship_type == comment_included_type, "The resource type seen in the relationships and included do not match"
35+
36+
37+
@pytest.mark.usefixtures("single_entry")
38+
class TestModelResourceName:
39+
40+
def test_model_resource_name_on_list(self, client):
41+
models.Comment.__bases__ += (_PatchedModel,)
42+
response = client.get(reverse("comment-list"))
43+
data = load_json(response.content)['data'][0]
44+
# name should be super-author instead of model name RenamedAuthor
45+
assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), (
46+
'resource_name from model incorrect on list')
47+
48+
# Precedence tests
49+
def test_resource_name_precendence(self, client):
50+
# default
51+
response = client.get(reverse("comment-list"))
52+
data = load_json(response.content)['data'][0]
53+
assert (data.get('type') == 'comments'), (
54+
'resource_name from model incorrect on list')
55+
56+
# model > default
57+
models.Comment.__bases__ += (_PatchedModel,)
58+
response = client.get(reverse("comment-list"))
59+
data = load_json(response.content)['data'][0]
60+
assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), (
61+
'resource_name from model incorrect on list')
62+
63+
# serializer > model
64+
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
65+
response = client.get(reverse("comment-list"))
66+
data = load_json(response.content)['data'][0]
67+
assert (data.get('type') == 'resource_name_from_serializer'), (
68+
'resource_name from serializer incorrect on list')
69+
70+
# view > serializer > model
71+
views.CommentViewSet.resource_name = 'resource_name_from_view'
72+
response = client.get(reverse("comment-list"))
73+
data = load_json(response.content)['data'][0]
74+
assert (data.get('type') == 'resource_name_from_view'), (
75+
'resource_name from view incorrect on list')
76+
77+
def teardown_method(self, method):
78+
models.Comment.__bases__ = (models.Comment.__bases__[0],)
79+
try:
80+
delattr(serializers.CommentSerializer.Meta, "resource_name")
81+
except AttributeError:
82+
pass
83+
try:
84+
delattr(views.CommentViewSet, "resource_name")
85+
except AttributeError:
86+
pass
87+
88+
89+
@pytest.mark.usefixtures("single_entry")
90+
class TestResourceNameConsistency:
91+
92+
# Included rename tests
93+
def test_type_match_on_included_and_inline_base(self, client):
94+
_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
95+
96+
def test_type_match_on_included_and_inline_with_JSONAPIMeta(self, client):
97+
models.Comment.__bases__ += (_PatchedModel,)
98+
99+
_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
100+
101+
def test_type_match_on_included_and_inline_with_serializer_resource_name(self, client):
102+
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
103+
104+
_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
105+
106+
def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(self, client):
107+
models.Comment.__bases__ += (_PatchedModel,)
108+
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
109+
110+
_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))
111+
112+
# Relation rename tests
113+
def test_resource_and_relationship_type_match(self, client):
114+
_check_resource_and_relationship_comment_type_match(client)
115+
116+
def test_resource_and_relationship_type_match_with_serializer_resource_name(self, client):
117+
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
118+
119+
_check_resource_and_relationship_comment_type_match(client)
120+
121+
def test_resource_and_relationship_type_match_with_JSONAPIMeta(self, client):
122+
models.Comment.__bases__ += (_PatchedModel,)
123+
124+
_check_resource_and_relationship_comment_type_match(client)
125+
126+
def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(self, client):
127+
models.Comment.__bases__ += (_PatchedModel,)
128+
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
129+
130+
_check_resource_and_relationship_comment_type_match(client)
131+
132+
def teardown_method(self, method):
133+
models.Comment.__bases__ = (models.Comment.__bases__[0],)
134+
try:
135+
delattr(serializers.CommentSerializer.Meta, "resource_name")
136+
except AttributeError:
137+
pass

example/tests/test_relations.py

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from rest_framework_json_api.exceptions import Conflict
88
from rest_framework_json_api.utils import format_relation_name
99
from example.models import Blog, Entry, Comment, Author
10+
from example.serializers import CommentSerializer
1011
from rest_framework_json_api.relations import ResourceRelatedField
1112

1213

@@ -115,6 +116,15 @@ def test_read_only(self):
115116
serializer.is_valid(raise_exception=True)
116117
self.assertNotIn('comment_set', serializer.validated_data)
117118

119+
def test_invalid_resource_id_object(self):
120+
comment = {'body': 'testing 123', 'entry': {'type': 'entry'}, 'author': {'id': '5'}}
121+
serializer = CommentSerializer(data=comment)
122+
assert not serializer.is_valid()
123+
assert serializer.errors == {
124+
'author': ["Invalid resource identifier object: missing 'type' attribute"],
125+
'entry': ["Invalid resource identifier object: missing 'id' attribute"]
126+
}
127+
118128

119129
class BlogFKSerializer(serializers.Serializer):
120130
blog = ResourceRelatedField(queryset=Blog.objects)

example/views.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework_json_api.views import RelationshipView
33
from example.models import Blog, Entry, Author, Comment
44
from example.serializers import (
5-
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
5+
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
66

77

88
class BlogViewSet(viewsets.ModelViewSet):
@@ -41,4 +41,3 @@ class CommentRelationshipView(RelationshipView):
4141
class AuthorRelationshipView(RelationshipView):
4242
queryset = Author.objects.all()
4343
self_link_view_name = 'author-relationships'
44-

rest_framework_json_api/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
__title__ = 'djangorestframework-jsonapi'
4-
__version__ = '2.0.0-beta.1'
4+
__version__ = '2.0.0-beta.2'
55
__author__ = ''
66
__license__ = 'MIT'
77
__copyright__ = ''

rest_framework_json_api/exceptions.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
from django.utils import six, encoding
33
from django.utils.translation import ugettext_lazy as _
44
from rest_framework import status, exceptions
5-
from rest_framework.views import exception_handler as drf_exception_handler
65

76
from rest_framework_json_api.utils import format_value
87

98

109
def exception_handler(exc, context):
10+
# Import this here to avoid potential edge-case circular imports, which
11+
# crashes with:
12+
# "ImportError: Could not import 'rest_framework_json_api.parsers.JSONParser' for API setting
13+
# 'DEFAULT_PARSER_CLASSES'. ImportError: cannot import name 'exceptions'.'"
14+
#
15+
# Also see: https://github.com/django-json-api/django-rest-framework-json-api/issues/158
16+
from rest_framework.views import exception_handler as drf_exception_handler
1117
response = drf_exception_handler(exc, context)
1218

1319
if not response:

rest_framework_json_api/relations.py

+25-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from django.utils.translation import ugettext_lazy as _
66

77
from rest_framework_json_api.exceptions import Conflict
8-
from rest_framework_json_api.utils import format_relation_name, Hyperlink, \
9-
get_resource_type_from_queryset, get_resource_type_from_instance
8+
from rest_framework_json_api.utils import Hyperlink, \
9+
get_resource_type_from_queryset, get_resource_type_from_instance, \
10+
get_included_serializers, get_resource_type_from_serializer
1011

1112

1213
class ResourceRelatedField(PrimaryKeyRelatedField):
@@ -19,6 +20,8 @@ class ResourceRelatedField(PrimaryKeyRelatedField):
1920
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
2021
'incorrect_type': _('Incorrect type. Expected resource identifier object, received {data_type}.'),
2122
'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'),
23+
'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'),
24+
'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'),
2225
'no_match': _('Invalid hyperlink - No URL match.'),
2326
}
2427

@@ -117,8 +120,16 @@ def to_internal_value(self, data):
117120
if not isinstance(data, dict):
118121
self.fail('incorrect_type', data_type=type(data).__name__)
119122
expected_relation_type = get_resource_type_from_queryset(self.queryset)
123+
124+
if 'type' not in data:
125+
self.fail('missing_type')
126+
127+
if 'id' not in data:
128+
self.fail('missing_id')
129+
120130
if data['type'] != expected_relation_type:
121131
self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type'])
132+
122133
return super(ResourceRelatedField, self).to_internal_value(data['id'])
123134

124135
def to_representation(self, value):
@@ -127,7 +138,18 @@ def to_representation(self, value):
127138
else:
128139
pk = value.pk
129140

130-
return OrderedDict([('type', format_relation_name(get_resource_type_from_instance(value))), ('id', str(pk))])
141+
# check to see if this resource has a different resource_name when
142+
# included and use that name
143+
resource_type = None
144+
root = getattr(self.parent, 'parent', self.parent)
145+
field_name = self.field_name if self.field_name else self.parent.field_name
146+
if getattr(root, 'included_serializers', None) is not None:
147+
includes = get_included_serializers(root)
148+
if field_name in includes.keys():
149+
resource_type = get_resource_type_from_serializer(includes[field_name])
150+
151+
resource_type = resource_type if resource_type else get_resource_type_from_instance(value)
152+
return OrderedDict([('type', resource_type), ('id', str(pk))])
131153

132154
@property
133155
def choices(self):

0 commit comments

Comments
 (0)