Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 79b7c79

Browse files
committed
Merge pull request #170 from django-json-api/feature/meta
Added support for adding meta objects from serializers
2 parents d5e1227 + 73130e3 commit 79b7c79

File tree

10 files changed

+231
-7
lines changed

10 files changed

+231
-7
lines changed

‎docs/api.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,34 @@
77
Add this mixin to a view to override `get_queryset` to automatically filter
88
records by `ids[]=1&ids[]=2` in URL query params.
99

10+
## rest_framework_json_api.renderers.JSONRenderer
11+
12+
The `JSONRenderer` exposes a number of methods that you may override if you need
13+
highly custom rendering control.
14+
15+
#### extract_attributes
16+
17+
`extract_attributes(fields, resource)`
18+
19+
Builds the `attributes` object of the JSON API resource object.
20+
21+
#### extract_relationships(fields, resource, resource_instance)
22+
23+
Builds the `relationships` top level object based on related serializers.
24+
25+
#### extract_included(fields, resource, resource_instance, included_resources)
26+
27+
Adds related data to the top level `included` key when the request includes `?include=example,example_field2`
28+
29+
#### extract_meta(serializer, resource)
30+
31+
Gathers the data from serializer fields specified in `meta_fields` and adds it to the `meta` object.
32+
33+
#### extract_root_meta(serializer, resource, meta)
34+
35+
Calls a `get_root_meta` function on a serializer, if it exists.
36+
37+
#### build_json_resource_obj(fields, resource, resource_instance, resource_name)
38+
39+
Builds the resource object (type, id, attributes) and extracts relationships.
40+

‎docs/usage.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,28 @@ When set to pluralize:
229229
Both `JSON_API_PLURALIZE_RELATION_TYPE` and `JSON_API_FORMAT_RELATION_KEYS` can be combined to
230230
achieve different results.
231231

232+
### Meta
233+
234+
You may add metadata to the rendered json in two different ways: `meta_fields` and `get_root_meta`.
235+
236+
On any `rest_framework_json_api.serializers.ModelSerializer` you may add a `meta_fields`
237+
property to the `Meta` class. This behaves in the same manner as the default
238+
`fields` property and will cause `SerializerMethodFields` or model values to be
239+
added to the `meta` object within the same `data` as the serializer.
240+
241+
To add metadata to the top level `meta` object add:
242+
243+
``` python
244+
def get_root_meta(self, obj):
245+
return {
246+
'size': len(obj)
247+
}
248+
```
249+
to the serializer. It must return a dict and will be merged with the existing top level `meta`.
250+
232251
<!--
233252
### Relationships
234253
### Links
235254
### Included
236255
### Errors
237-
### Meta
238256
-->

‎example/serializers.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1+
from datetime import datetime
12
from rest_framework_json_api import serializers, relations
23
from example.models import Blog, Entry, Author, AuthorBio, Comment
34

45

56
class BlogSerializer(serializers.ModelSerializer):
67

8+
copyright = serializers.SerializerMethodField()
9+
10+
def get_copyright(self, obj):
11+
return datetime.now().year
12+
13+
def get_root_meta(self, obj):
14+
return {
15+
'api_docs': '/docs/api/blogs'
16+
}
17+
718
class Meta:
819
model = Blog
920
fields = ('name', )
21+
meta_fields = ('copyright',)
1022

1123

1224
class EntrySerializer(serializers.ModelSerializer):
@@ -24,6 +36,7 @@ def __init__(self, *args, **kwargs):
2436
'suggested': 'example.serializers.EntrySerializer',
2537
}
2638

39+
body_format = serializers.SerializerMethodField()
2740
comments = relations.ResourceRelatedField(
2841
source='comment_set', many=True, read_only=True)
2942
suggested = relations.SerializerMethodResourceRelatedField(
@@ -32,10 +45,14 @@ def __init__(self, *args, **kwargs):
3245
def get_suggested(self, obj):
3346
return Entry.objects.exclude(pk=obj.pk).first()
3447

48+
def get_body_format(self, obj):
49+
return 'text'
50+
3551
class Meta:
3652
model = Entry
3753
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
3854
'authors', 'comments', 'suggested',)
55+
meta_fields = ('body_format',)
3956

4057

4158
class AuthorBioSerializer(serializers.ModelSerializer):

‎example/tests/integration/test_meta.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from datetime import datetime
2+
from django.core.urlresolvers import reverse
3+
4+
import pytest
5+
from example.tests.utils import dump_json, redump_json
6+
7+
pytestmark = pytest.mark.django_db
8+
9+
10+
def test_top_level_meta(blog, client):
11+
12+
expected = {
13+
"data": {
14+
"type": "blogs",
15+
"id": "1",
16+
"attributes": {
17+
"name": blog.name
18+
},
19+
"meta": {
20+
"copyright": datetime.now().year
21+
},
22+
},
23+
"meta": {
24+
"apiDocs": "/docs/api/blogs"
25+
},
26+
}
27+
28+
response = client.get(reverse("blog-detail", kwargs={'pk': blog.pk}))
29+
content_dump = redump_json(response.content)
30+
expected_dump = dump_json(expected)
31+
32+
assert content_dump == expected_dump

‎example/tests/integration/test_non_paginated_responses.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf):
2626
"pubDate": None,
2727
"modDate": None
2828
},
29+
"meta": {
30+
"bodyFormat": "text"
31+
},
2932
"relationships":
3033
{
3134
"blog": {
@@ -51,6 +54,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf):
5154
"pubDate": None,
5255
"modDate": None
5356
},
57+
"meta": {
58+
"bodyFormat": "text"
59+
},
5460
"relationships":
5561
{
5662
"blog": {

‎example/tests/integration/test_pagination.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ def test_pagination_with_single_entry(single_entry, client):
2020
"pubDate": None,
2121
"modDate": None
2222
},
23+
"meta": {
24+
"bodyFormat": "text"
25+
},
2326
"relationships":
2427
{
2528
"blog": {

‎example/tests/unit/test_renderer_class_methods.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
pytestmark = pytest.mark.django_db
88

99
class ResourceSerializer(serializers.ModelSerializer):
10+
version = serializers.SerializerMethodField()
11+
def get_version(self, obj):
12+
return '1.0.0'
1013
class Meta:
1114
fields = ('username',)
15+
meta_fields = ('version',)
1216
model = get_user_model()
1317

1418

@@ -48,3 +52,47 @@ def test_extract_attributes():
4852
assert sorted(JSONRenderer.extract_attributes(fields, resource)) == sorted(expected), 'Regular fields should be extracted'
4953
assert sorted(JSONRenderer.extract_attributes(fields, {})) == sorted(
5054
{'username': ''}), 'Should not extract read_only fields on empty serializer'
55+
56+
def test_extract_meta():
57+
serializer = ResourceSerializer(data={'username': 'jerel', 'version':'1.0.0'})
58+
serializer.is_valid()
59+
expected = {
60+
'version': '1.0.0',
61+
}
62+
assert JSONRenderer.extract_meta(serializer, serializer.data) == expected
63+
64+
def test_extract_root_meta():
65+
def get_root_meta(obj):
66+
return {
67+
'foo': 'meta-value'
68+
}
69+
70+
serializer = ResourceSerializer()
71+
serializer.get_root_meta = get_root_meta
72+
expected = {
73+
'foo': 'meta-value',
74+
}
75+
assert JSONRenderer.extract_root_meta(serializer, {}, {}) == expected
76+
77+
def test_extract_root_meta_many():
78+
def get_root_meta(obj):
79+
return {
80+
'foo': 'meta-value'
81+
}
82+
83+
serializer = ResourceSerializer(many=True)
84+
serializer.get_root_meta = get_root_meta
85+
expected = {
86+
'foo': 'meta-value'
87+
}
88+
assert JSONRenderer.extract_root_meta(serializer, {}, {}) == expected
89+
90+
def test_extract_root_meta_invalid_meta():
91+
def get_root_meta(obj):
92+
return 'not a dict'
93+
94+
serializer = ResourceSerializer()
95+
serializer.get_root_meta = get_root_meta
96+
with pytest.raises(AssertionError) as e_info:
97+
JSONRenderer.extract_root_meta(serializer, {}, {})
98+

‎rest_framework_json_api/renderers.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,29 @@ def extract_included(fields, resource, resource_instance, included_resources):
321321

322322
return utils.format_keys(included_data)
323323

324+
@staticmethod
325+
def extract_meta(serializer, resource):
326+
if hasattr(serializer, 'child'):
327+
meta = getattr(serializer.child, 'Meta', None)
328+
else:
329+
meta = getattr(serializer, 'Meta', None)
330+
meta_fields = getattr(meta, 'meta_fields', [])
331+
data = OrderedDict()
332+
for field_name in meta_fields:
333+
data.update({
334+
field_name: resource.get(field_name)
335+
})
336+
return data
337+
338+
@staticmethod
339+
def extract_root_meta(serializer, resource, meta):
340+
if getattr(serializer, 'get_root_meta', None):
341+
root_meta = serializer.get_root_meta(resource)
342+
if root_meta:
343+
assert isinstance(root_meta, dict), 'get_root_meta must return a dict'
344+
meta.update(root_meta)
345+
return meta
346+
324347
@staticmethod
325348
def build_json_resource_obj(fields, resource, resource_instance, resource_name):
326349
resource_data = [
@@ -388,6 +411,8 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
388411
included_resources = list()
389412

390413
json_api_included = list()
414+
# initialize json_api_meta with pagination meta or an empty dict
415+
json_api_meta = data.get('meta', {}) if isinstance(data, dict) else {}
391416

392417
if data and 'results' in data:
393418
serializer_data = data["results"]
@@ -411,8 +436,14 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
411436
for position in range(len(serializer_data)):
412437
resource = serializer_data[position] # Get current resource
413438
resource_instance = resource_serializer.instance[position] # Get current instance
414-
json_api_data.append(
415-
self.build_json_resource_obj(fields, resource, resource_instance, resource_name))
439+
440+
json_resource_obj = self.build_json_resource_obj(fields, resource, resource_instance, resource_name)
441+
meta = self.extract_meta(resource_serializer, resource)
442+
if meta:
443+
json_resource_obj.update({'meta': utils.format_keys(meta)})
444+
json_api_meta = self.extract_root_meta(resource_serializer, resource, json_api_meta)
445+
json_api_data.append(json_resource_obj)
446+
416447
included = self.extract_included(fields, resource, resource_instance, included_resources)
417448
if included:
418449
json_api_included.extend(included)
@@ -422,6 +453,12 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
422453
fields = utils.get_serializer_fields(data.serializer)
423454
resource_instance = data.serializer.instance
424455
json_api_data = self.build_json_resource_obj(fields, data, resource_instance, resource_name)
456+
457+
meta = self.extract_meta(data.serializer, data)
458+
if meta:
459+
json_api_data.update({'meta': utils.format_keys(meta)})
460+
json_api_meta = self.extract_root_meta(data.serializer, data, json_api_meta)
461+
425462
included = self.extract_included(fields, data, resource_instance, included_resources)
426463
if included:
427464
json_api_included.extend(included)
@@ -454,8 +491,8 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
454491
# Sort the items by type then by id
455492
render_data['included'] = sorted(unique_compound_documents, key=lambda item: (item['type'], item['id']))
456493

457-
if isinstance(data, dict) anddata.get('meta'):
458-
render_data['meta'] = data.get('meta')
494+
if json_api_meta:
495+
render_data['meta'] = utils.format_keys(json_api_meta)
459496

460497
return super(JSONRenderer, self).render(
461498
render_data, accepted_media_type, renderer_context

‎rest_framework_json_api/serializers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,24 @@ class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, Mo
136136
* A mixin class to enable validation of included resources is included
137137
"""
138138
serializer_related_field = ResourceRelatedField
139+
140+
def __init__(self, *args, **kwargs):
141+
meta_fields = getattr(self.Meta, 'meta_fields', [])
142+
# we add meta_fields to fields so they will be serialized like usual
143+
self.Meta.fields = tuple(tuple(self.Meta.fields) + tuple(meta_fields))
144+
super(ModelSerializer, self).__init__(*args, **kwargs)
145+
146+
def get_field_names(self, declared_fields, info):
147+
"""
148+
We override the parent to omit explicity defined meta fields (such
149+
as SerializerMethodFields) from the list of declared fields
150+
"""
151+
meta_fields = getattr(self.Meta, 'meta_fields', [])
152+
153+
declared = OrderedDict()
154+
for field_name in set(declared_fields.keys()):
155+
field = declared_fields[field_name]
156+
if field_name not in meta_fields:
157+
declared[field_name] = field
158+
return super(ModelSerializer, self).get_field_names(declared, info)
159+

‎rest_framework_json_api/utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,22 @@ def get_resource_name(context):
6565

6666

6767
def get_serializer_fields(serializer):
68+
fields = None
6869
if hasattr(serializer, 'child'):
69-
return getattr(serializer.child, 'fields')
70+
fields = getattr(serializer.child, 'fields')
71+
meta = getattr(serializer.child, 'Meta', None)
7072
if hasattr(serializer, 'fields'):
71-
return getattr(serializer, 'fields')
73+
fields = getattr(serializer, 'fields')
74+
meta = getattr(serializer, 'Meta', None)
7275

76+
if fields:
77+
meta_fields = getattr(meta, 'meta_fields', {})
78+
for field in meta_fields:
79+
try:
80+
fields.pop(field)
81+
except KeyError:
82+
pass
83+
return fields
7384

7485
def format_keys(obj, format_type=None):
7586
"""

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /