Skip to content

Commit 42e0554

Browse files
committed
Merge branch develop into feature/compound_documents
2 parents 73bc6d5 + ce945f2 commit 42e0554

File tree

12 files changed

+444
-349
lines changed

12 files changed

+444
-349
lines changed

.travis.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,25 @@ script: tox
66
env:
77
- TOXENV=py27-django17-drf31
88
- TOXENV=py27-django17-drf32
9-
- TOXENV=py32-django17-drf31
10-
- TOXENV=py32-django17-drf32
119
- TOXENV=py33-django17-drf31
1210
- TOXENV=py33-django17-drf32
1311
- TOXENV=py34-django17-drf31
1412
- TOXENV=py34-django17-drf32
1513
- TOXENV=py27-django18-drf31
1614
- TOXENV=py27-django18-drf32
17-
- TOXENV=py32-django18-drf31
18-
- TOXENV=py32-django18-drf32
15+
- TOXENV=py27-django18-drf33
1916
- TOXENV=py33-django18-drf31
2017
- TOXENV=py33-django18-drf32
18+
- TOXENV=py33-django18-drf33
2119
- TOXENV=py34-django18-drf31
2220
- TOXENV=py34-django18-drf32
21+
- TOXENV=py34-django18-drf33
2322
- TOXENV=py27-django19-drf31
2423
- TOXENV=py27-django19-drf32
24+
- TOXENV=py27-django19-drf33
2525
- TOXENV=py34-django19-drf31
2626
- TOXENV=py34-django19-drf32
27+
- TOXENV=py34-django19-drf33
2728
- TOXENV=py35-django19-drf31
2829
- TOXENV=py35-django19-drf32
30+
- TOXENV=py35-django19-drf33

example/serializers.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,31 @@ class Meta:
1111

1212
class EntrySerializer(serializers.ModelSerializer):
1313

14+
def __init__(self, *args, **kwargs):
15+
# to make testing more concise we'll only output the
16+
# `suggested` field when it's requested via `include`
17+
request = kwargs.get('context', {}).get('request')
18+
if request and 'suggested' not in request.query_params.get('include', []):
19+
self.fields.pop('suggested')
20+
super(EntrySerializer, self).__init__(*args, **kwargs)
21+
1422
included_serializers = {
1523
'comments': 'example.serializers.CommentSerializer',
24+
'suggested': 'example.serializers.EntrySerializer',
1625
}
1726

1827
comments = relations.ResourceRelatedField(
1928
source='comment_set', many=True, read_only=True)
29+
suggested = relations.SerializerMethodResourceRelatedField(
30+
source='get_suggested', model=Entry, read_only=True)
31+
32+
def get_suggested(self, obj):
33+
return Entry.objects.exclude(pk=obj.pk).first()
2034

2135
class Meta:
2236
model = Entry
2337
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
24-
'authors', 'comments',)
38+
'authors', 'comments', 'suggested',)
2539

2640

2741
class AuthorSerializer(serializers.ModelSerializer):

example/tests/integration/test_includes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,12 @@ def test_included_data_on_detail(single_entry, client):
2727
comment_count = len([resource for resource in included if resource["type"] == "comments"])
2828
expected_comment_count = single_entry.comment_set.count()
2929
assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
30+
31+
def test_dynamic_related_data_is_included(single_entry, entry_factory, client):
32+
entry_factory()
33+
response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + '?include=suggested')
34+
included = load_json(response.content).get('included')
35+
36+
assert [x.get('type') for x in included] == ['entries'], 'Dynamic included types are incorrect'
37+
assert len(included) == 1, 'The dynamically included blog entries are of an incorrect count'
38+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.core.urlresolvers import reverse
2+
3+
import pytest
4+
5+
6+
pytestmark = pytest.mark.django_db
7+
8+
9+
def test_sparse_fieldset_ordered_dict_error(multiple_entries, client):
10+
base_url = reverse('entry-list')
11+
querystring = '?fields[entries]=blog,headline'
12+
response = client.get(base_url + querystring) # RuntimeError: OrderedDict mutated during iteration
13+
assert response.status_code == 200 # succeed if we didn't fail due to the above RuntimeError
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
from django.contrib.auth import get_user_model
3+
4+
from rest_framework_json_api import serializers
5+
from rest_framework_json_api.renderers import JSONRenderer
6+
7+
pytestmark = pytest.mark.django_db
8+
9+
class ResourceSerializer(serializers.ModelSerializer):
10+
class Meta:
11+
fields = ('username',)
12+
model = get_user_model()
13+
14+
15+
def test_build_json_resource_obj():
16+
resource = {
17+
'pk': 1,
18+
'username': 'Alice',
19+
}
20+
21+
serializer = ResourceSerializer(data={'username': 'Alice'})
22+
serializer.is_valid()
23+
resource_instance = serializer.save()
24+
25+
output = {
26+
'type': 'user',
27+
'id': '1',
28+
'attributes': {
29+
'username': 'Alice'
30+
},
31+
}
32+
33+
assert JSONRenderer.build_json_resource_obj(
34+
serializer.fields, resource, resource_instance, 'user') == output
35+
36+
37+
def test_extract_attributes():
38+
fields = {
39+
'id': serializers.Field(),
40+
'username': serializers.Field(),
41+
'deleted': serializers.ReadOnlyField(),
42+
}
43+
resource = {'id': 1, 'deleted': None, 'username': 'jerel'}
44+
expected = {
45+
'username': 'jerel',
46+
'deleted': None
47+
}
48+
assert sorted(JSONRenderer.extract_attributes(fields, resource)) == sorted(expected), 'Regular fields should be extracted'
49+
assert sorted(JSONRenderer.extract_attributes(fields, {})) == sorted(
50+
{'username': ''}), 'Should not extract read_only fields on empty serializer'

example/tests/unit/test_utils.py

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,35 @@
11
import pytest
2-
from django.utils import six
32
from django.conf import settings
43
from django.contrib.auth import get_user_model
4+
from django.utils import six
55
from rest_framework import serializers
6+
from rest_framework.generics import GenericAPIView
67
from rest_framework.response import Response
78
from rest_framework.views import APIView
89

9-
from rest_framework_json_api import utils
1010
from example.serializers import (EntrySerializer, BlogSerializer,
1111
AuthorSerializer, CommentSerializer)
12+
from rest_framework_json_api import utils
1213
from rest_framework_json_api.utils import get_included_serializers
1314

1415
pytestmark = pytest.mark.django_db
1516

1617

17-
class ResourceView(APIView):
18-
pass
19-
20-
2118
class ResourceSerializer(serializers.ModelSerializer):
22-
class Meta():
19+
class Meta:
2320
fields = ('username',)
2421
model = get_user_model()
2522

2623

2724
def test_get_resource_name():
28-
view = ResourceView()
25+
view = APIView()
2926
context = {'view': view}
3027
setattr(settings, 'JSON_API_FORMAT_RELATION_KEYS', None)
31-
assert 'ResourceViews' == utils.get_resource_name(context), 'not formatted'
28+
assert 'APIViews' == utils.get_resource_name(context), 'not formatted'
3229

33-
view = ResourceView()
3430
context = {'view': view}
3531
setattr(settings, 'JSON_API_FORMAT_RELATION_KEYS', 'dasherize')
36-
assert 'resource-views' == utils.get_resource_name(context), 'derived from view'
32+
assert 'api-views' == utils.get_resource_name(context), 'derived from view'
3733

3834
view.model = get_user_model()
3935
assert 'users' == utils.get_resource_name(context), 'derived from view model'
@@ -47,9 +43,9 @@ def test_get_resource_name():
4743
view.response = Response(status=500)
4844
assert 'errors' == utils.get_resource_name(context), 'handles 500 error'
4945

50-
view = ResourceView()
51-
context = {'view': view}
46+
view = GenericAPIView()
5247
view.serializer_class = ResourceSerializer
48+
context = {'view': view}
5349
assert 'users' == utils.get_resource_name(context), 'derived from serializer'
5450

5551
view.serializer_class.Meta.resource_name = 'rcustom'
@@ -90,28 +86,6 @@ def test_format_relation_name():
9086
assert utils.format_relation_name('first_name', 'camelize') == 'firstNames'
9187

9288

93-
def test_build_json_resource_obj():
94-
resource = {
95-
'pk': 1,
96-
'username': 'Alice',
97-
}
98-
99-
serializer = ResourceSerializer(data={'username': 'Alice'})
100-
serializer.is_valid()
101-
resource_instance = serializer.save()
102-
103-
output = {
104-
'type': 'user',
105-
'id': '1',
106-
'attributes': {
107-
'username': 'Alice'
108-
},
109-
}
110-
111-
assert utils.build_json_resource_obj(
112-
serializer.fields, resource, resource_instance, 'user') == output
113-
114-
11589
class SerializerWithIncludedSerializers(EntrySerializer):
11690
included_serializers = {
11791
'blog': BlogSerializer,

rest_framework_json_api/pagination.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""
22
Pagination fields
33
"""
4+
from collections import OrderedDict
45
from rest_framework import serializers
56
from rest_framework.views import Response
6-
from rest_framework.compat import OrderedDict
77
from rest_framework.pagination import PageNumberPagination
88
from rest_framework.templatetags.rest_framework import replace_query_param
99

rest_framework_json_api/relations.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwar
3131
self.related_link_lookup_field = kwargs.pop('related_link_lookup_field', self.related_link_lookup_field)
3232
self.related_link_url_kwarg = kwargs.pop('related_link_url_kwarg', self.related_link_lookup_field)
3333

34+
# check for a model class that was passed in for the relation type
35+
model = kwargs.pop('model', None)
36+
if model:
37+
self.model = model
38+
3439
# We include this simply for dependency injection in tests.
3540
# We can't add it as a class attributes or it would expect an
3641
# implicit `self` argument to be passed.
@@ -104,7 +109,11 @@ def get_links(self, obj=None, lookup_field='pk'):
104109

105110
def to_internal_value(self, data):
106111
if isinstance(data, six.text_type):
107-
data = json.loads(data)
112+
try:
113+
data = json.loads(data)
114+
except ValueError:
115+
# show a useful error if they send a `pk` instead of resource object
116+
self.fail('incorrect_type', data_type=type(data).__name__)
108117
if not isinstance(data, dict):
109118
self.fail('incorrect_type', data_type=type(data).__name__)
110119
expected_relation_type = get_resource_type_from_queryset(self.queryset)
@@ -136,3 +145,12 @@ def choices(self):
136145
for item in queryset
137146
])
138147

148+
149+
class SerializerMethodResourceRelatedField(ResourceRelatedField):
150+
def get_attribute(self, instance):
151+
# check for a source fn defined on the serializer instead of the model
152+
if self.source and hasattr(self.parent, self.source):
153+
serializer_method = getattr(self.parent, self.source)
154+
if hasattr(serializer_method, '__call__'):
155+
return serializer_method(instance)
156+
return super(ResourceRelatedField, self).get_attribute(instance)

0 commit comments

Comments
 (0)