Skip to content

Commit 79e7738

Browse files
slivercn2ygk
authored andcommitted
Preserve values from being formatted (#420)
Introduce `JSON_API_FORMAT_FIELD_NAMES` which preserves keys of values. `JSON_API_FORMAT_KEYS` still exists but is deprecated.
1 parent 028f191 commit 79e7738

15 files changed

+144
-35
lines changed

CHANGELOG.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
v2.5.0 - [unreleased]
22
* Add new pagination classes based on JSON:API query parameter *recommendations*:
3-
* JsonApiPageNumberPagination and JsonApiLimitOffsetPagination. See [usage docs](docs/usage.md#pagination).
4-
* Deprecates PageNumberPagination and LimitOffsetPagination.
5-
* Add ReadOnlyModelViewSet extension with prefetch mixins.
3+
* `JsonApiPageNumberPagination` and `JsonApiLimitOffsetPagination`. See [usage docs](docs/usage.md#pagination).
4+
* Deprecates `PageNumberPagination` and `LimitOffsetPagination`.
5+
* Add `ReadOnlyModelViewSet` extension with prefetch mixins.
66
* Add support for Django REST Framework 3.8.x
7+
* Introduce `JSON_API_FORMAT_FIELD_NAMES` option replacing `JSON_API_FORMAT_KEYS` but in comparision preserving
8+
values from being formatted as attributes can contain any [json value](http://jsonapi.org/format/#document-resource-object-attributes).
9+
* `JSON_API_FORMAT_KEYS` still works as before (formating all json value keys also nested) but is marked as deprecated.
710

811
v2.4.0 - Released January 25, 2018
912

docs/usage.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,13 @@ multiple endpoints. Setting the `resource_name` on views may result in a differe
149149

150150
### Inflecting object and relation keys
151151

152-
This package includes the ability (off by default) to automatically convert json
153-
requests and responses from the python/rest_framework's preferred underscore to
152+
This package includes the ability (off by default) to automatically convert [json
153+
api field names](http://jsonapi.org/format/#document-resource-object-fields) of requests and responses from the python/rest_framework's preferred underscore to
154154
a format of your choice. To hook this up include the following setting in your
155155
project settings:
156156

157157
``` python
158-
JSON_API_FORMAT_KEYS = 'dasherize'
158+
JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
159159
```
160160

161161
Possible values:

example/api/resources/identity.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def posts(self, request):
3737
encoding.force_text('identities'): IdentitySerializer(identities, many=True).data,
3838
encoding.force_text('posts'): PostSerializer(posts, many=True).data,
3939
}
40-
return Response(utils.format_keys(data, format_type='camelize'))
40+
return Response(utils.format_field_names(data, format_type='camelize'))
4141

4242
@detail_route()
4343
def manual_resource_name(self, request, *args, **kwargs):

example/settings/dev.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565

6666
INTERNAL_IPS = ('127.0.0.1', )
6767

68-
JSON_API_FORMAT_KEYS = 'camelize'
68+
JSON_API_FORMAT_FIELD_NAMES = 'camelize'
6969
JSON_API_FORMAT_TYPES = 'camelize'
7070
REST_FRAMEWORK = {
7171
'PAGE_SIZE': 5,

example/settings/test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
ROOT_URLCONF = 'example.urls_test'
1111

12-
JSON_API_FORMAT_KEYS = 'camelize'
12+
JSON_API_FIELD_NAMES = 'camelize'
1313
JSON_API_FORMAT_TYPES = 'camelize'
1414
JSON_API_PLURALIZE_TYPES = True
1515
REST_FRAMEWORK.update({

example/tests/test_generic_viewset.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from example.tests import TestBase
55

66

7-
@override_settings(JSON_API_FORMAT_KEYS='dasherize')
7+
@override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize')
88
class GenericViewSet(TestBase):
99
"""
1010
Test expected responses coming from a Generic ViewSet

example/tests/test_model_viewsets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from example.tests import TestBase
88

99

10-
@override_settings(JSON_API_FORMAT_KEYS='dasherize')
10+
@override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize')
1111
class ModelViewSetTests(TestBase):
1212
"""
1313
Test usage with ModelViewSets, also tests pluralization, camelization,

example/tests/test_parsers.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
from io import BytesIO
33

4-
from django.test import TestCase
4+
from django.test import TestCase, override_settings
55
from rest_framework.exceptions import ParseError
66

77
from rest_framework_json_api.parsers import JSONParser
@@ -22,7 +22,10 @@ def __init__(self):
2222
data = {
2323
'data': {
2424
'id': 123,
25-
'type': 'Blog'
25+
'type': 'Blog',
26+
'attributes': {
27+
'json-value': {'JsonKey': 'JsonValue'}
28+
},
2629
},
2730
'meta': {
2831
'random_key': 'random_value'
@@ -31,13 +34,25 @@ def __init__(self):
3134

3235
self.string = json.dumps(data)
3336

34-
def test_parse_include_metadata(self):
37+
@override_settings(JSON_API_FORMAT_KEYS='camelize')
38+
def test_parse_include_metadata_format_keys(self):
3539
parser = JSONParser()
3640

3741
stream = BytesIO(self.string.encode('utf-8'))
3842
data = parser.parse(stream, None, self.parser_context)
3943

4044
self.assertEqual(data['_meta'], {'random_key': 'random_value'})
45+
self.assertEqual(data['json_value'], {'json_key': 'JsonValue'})
46+
47+
@override_settings(JSON_API_FORMAT_FIELD_NAMES='dasherize')
48+
def test_parse_include_metadata_format_field_names(self):
49+
parser = JSONParser()
50+
51+
stream = BytesIO(self.string.encode('utf-8'))
52+
data = parser.parse(stream, None, self.parser_context)
53+
54+
self.assertEqual(data['_meta'], {'random_key': 'random_value'})
55+
self.assertEqual(data['json_value'], {'JsonKey': 'JsonValue'})
4156

4257
def test_parse_invalid_data(self):
4358
parser = JSONParser()

example/tests/unit/test_renderers.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
from rest_framework_json_api import serializers, views
24
from rest_framework_json_api.renderers import JSONRenderer
35

@@ -19,9 +21,14 @@ class DummyTestSerializer(serializers.ModelSerializer):
1921
related_models = RelatedModelSerializer(
2022
source='comments', many=True, read_only=True)
2123

24+
json_field = serializers.SerializerMethodField()
25+
26+
def get_json_field(self, entry):
27+
return {'JsonKey': 'JsonValue'}
28+
2229
class Meta:
2330
model = Entry
24-
fields = ('related_models',)
31+
fields = ('related_models', 'json_field')
2532

2633
class JSONAPIMeta:
2734
included_resources = ('related_models',)
@@ -61,3 +68,22 @@ def test_simple_reverse_relation_included_read_only_viewset():
6168
ReadOnlyDummyTestViewSet)
6269

6370
assert rendered
71+
72+
73+
def test_render_format_field_names(settings):
74+
"""Test that json field is kept untouched."""
75+
settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
76+
rendered = render_dummy_test_serialized_view(DummyTestViewSet)
77+
78+
result = json.loads(rendered.decode())
79+
assert result['data']['attributes']['json-field'] == {'JsonKey': 'JsonValue'}
80+
81+
82+
def test_render_format_keys(settings):
83+
"""Test that json field value keys are formated."""
84+
delattr(settings, 'JSON_API_FORMAT_FILED_NAMES')
85+
settings.JSON_API_FORMAT_KEYS = 'dasherize'
86+
rendered = render_dummy_test_serialized_view(DummyTestViewSet)
87+
88+
result = json.loads(rendered.decode())
89+
assert result['data']['attributes']['json-field'] == {'json-key': 'JsonValue'}

example/tests/unit/test_settings.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ def test_settings_default():
1313

1414

1515
def test_settings_override(settings):
16-
settings.JSON_API_FORMAT_KEYS = 'dasherize'
17-
assert json_api_settings.FORMAT_KEYS == 'dasherize'
16+
settings.JSON_API_FORMAT_FIELD_NAMES = 'dasherize'
17+
assert json_api_settings.FORMAT_FIELD_NAMES == 'dasherize'

example/tests/unit/test_utils.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ def test_format_keys():
6969
}
7070

7171
output = {'firstName': 'a', 'lastName': 'b'}
72-
assert utils.format_keys(underscored, 'camelize') == output
72+
result = pytest.deprecated_call(utils.format_keys, underscored, 'camelize')
73+
assert result == output
7374

7475
output = {'FirstName': 'a', 'LastName': 'b'}
7576
assert utils.format_keys(underscored, 'capitalize') == output
@@ -84,6 +85,19 @@ def test_format_keys():
8485
assert utils.format_keys([underscored], 'dasherize') == output
8586

8687

88+
@pytest.mark.parametrize("format_type,output", [
89+
('camelize', {'fullName': {'last-name': 'a', 'first-name': 'b'}}),
90+
('capitalize', {'FullName': {'last-name': 'a', 'first-name': 'b'}}),
91+
('dasherize', {'full-name': {'last-name': 'a', 'first-name': 'b'}}),
92+
('underscore', {'full_name': {'last-name': 'a', 'first-name': 'b'}}),
93+
])
94+
def test_format_field_names(settings, format_type, output):
95+
settings.JSON_API_FORMAT_FIELD_NAMES = format_type
96+
97+
value = {'full_name': {'last-name': 'a', 'first-name': 'b'}}
98+
assert utils.format_field_names(value, format_type) == output
99+
100+
87101
def test_format_value():
88102
assert utils.format_value('first_name', 'camelize') == 'firstName'
89103
assert utils.format_value('first_name', 'capitalize') == 'FirstName'

rest_framework_json_api/parsers.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,26 @@ class JSONParser(parsers.JSONParser):
3232
@staticmethod
3333
def parse_attributes(data):
3434
attributes = data.get('attributes')
35-
uses_format_translation = json_api_settings.FORMAT_KEYS
35+
uses_format_translation = json_api_settings.format_type
3636

3737
if not attributes:
3838
return dict()
3939
elif uses_format_translation:
4040
# convert back to python/rest_framework's preferred underscore format
41-
return utils.format_keys(attributes, 'underscore')
41+
return utils._format_object(attributes, 'underscore')
4242
else:
4343
return attributes
4444

4545
@staticmethod
4646
def parse_relationships(data):
47-
uses_format_translation = json_api_settings.FORMAT_KEYS
47+
uses_format_translation = json_api_settings.format_type
4848
relationships = data.get('relationships')
4949

5050
if not relationships:
5151
relationships = dict()
5252
elif uses_format_translation:
5353
# convert back to python/rest_framework's preferred underscore format
54-
relationships = utils.format_keys(relationships, 'underscore')
54+
relationships = utils._format_object(relationships, 'underscore')
5555

5656
# Parse the relationships
5757
parsed_relationships = dict()

rest_framework_json_api/renderers.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def extract_attributes(cls, fields, resource):
6868
field_name: resource.get(field_name)
6969
})
7070

71-
return utils.format_keys(data)
71+
return utils._format_object(data)
7272

7373
@classmethod
7474
def extract_relationships(cls, fields, resource, resource_instance):
@@ -281,7 +281,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
281281
})
282282
continue
283283

284-
return utils.format_keys(data)
284+
return utils._format_object(data)
285285

286286
@classmethod
287287
def extract_relation_instance(cls, field_name, field, resource_instance, serializer):
@@ -405,7 +405,7 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
405405
getattr(serializer, '_poly_force_type_resolution', False)
406406
)
407407
included_cache[new_item['type']][new_item['id']] = \
408-
utils.format_keys(new_item)
408+
utils._format_object(new_item)
409409
cls.extract_included(
410410
serializer_fields,
411411
serializer_resource,
@@ -427,7 +427,9 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
427427
relation_type,
428428
getattr(field, '_poly_force_type_resolution', False)
429429
)
430-
included_cache[new_item['type']][new_item['id']] = utils.format_keys(new_item)
430+
included_cache[new_item['type']][new_item['id']] = utils._format_object(
431+
new_item
432+
)
431433
cls.extract_included(
432434
serializer_fields,
433435
serializer_data,
@@ -577,7 +579,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
577579
)
578580
meta = self.extract_meta(serializer, resource)
579581
if meta:
580-
json_resource_obj.update({'meta': utils.format_keys(meta)})
582+
json_resource_obj.update({'meta': utils._format_object(meta)})
581583
json_api_data.append(json_resource_obj)
582584

583585
self.extract_included(
@@ -594,7 +596,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
594596

595597
meta = self.extract_meta(serializer, serializer_data)
596598
if meta:
597-
json_api_data.update({'meta': utils.format_keys(meta)})
599+
json_api_data.update({'meta': utils._format_object(meta)})
598600

599601
self.extract_included(
600602
fields, serializer_data, resource_instance, included_resources, included_cache
@@ -620,7 +622,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
620622
render_data['included'].append(included_cache[included_type][included_id])
621623

622624
if json_api_meta:
623-
render_data['meta'] = utils.format_keys(json_api_meta)
625+
render_data['meta'] = utils._format_object(json_api_meta)
624626

625627
return super(JSONRenderer, self).render(
626628
render_data, accepted_media_type, renderer_context

rest_framework_json_api/settings.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
JSON_API_SETTINGS_PREFIX = 'JSON_API_'
1111

1212
DEFAULTS = {
13-
'FORMAT_KEYS': False,
14-
'FORMAT_RELATION_KEYS': None,
13+
'FORMAT_FIELD_NAMES': False,
1514
'FORMAT_TYPES': False,
16-
'PLURALIZE_RELATION_TYPE': None,
1715
'PLURALIZE_TYPES': False,
1816
'UNIFORM_EXCEPTIONS': False,
17+
18+
# deprecated settings to be removed in the future
19+
'FORMAT_KEYS': None,
20+
'FORMAT_RELATION_KEYS': None,
21+
'PLURALIZE_RELATION_TYPE': None,
1922
}
2023

2124

@@ -39,6 +42,13 @@ def __getattr__(self, attr):
3942
setattr(self, attr, value)
4043
return value
4144

45+
@property
46+
def format_type(self):
47+
if self.FORMAT_KEYS is not None:
48+
return self.FORMAT_KEYS
49+
50+
return self.FORMAT_FIELD_NAMES
51+
4252

4353
json_api_settings = JSONAPISettings()
4454

0 commit comments

Comments
 (0)