diff --git a/.gitignore b/.gitignore index e5428776..3177afc7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ pip-delete-this-directory.txt # Tox .tox/ + +# VirtualEnv +.venv/ diff --git a/docs/api.md b/docs/api.md index 14da1d8f..a7f8926f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -38,7 +38,7 @@ Gathers the data from serializer fields specified in `meta_fields` and adds it t #### extract_root_meta -`extract_root_meta(serializer, resource, meta)` +`extract_root_meta(serializer, resource)` Calls a `get_root_meta` function on a serializer, if it exists. @@ -47,4 +47,3 @@ Calls a `get_root_meta` function on a serializer, if it exists. `build_json_resource_obj(fields, resource, resource_instance, resource_name)` Builds the resource object (type, id, attributes) and extracts relationships. - diff --git a/docs/usage.md b/docs/usage.md index 7f951e1c..40105b26 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -260,10 +260,17 @@ added to the `meta` object within the same `data` as the serializer. To add metadata to the top level `meta` object add: ``` python -def get_root_meta(self, obj): - return { - 'size': len(obj) - } +def get_root_meta(self, resource, many): + if many: + # Dealing with a list request + return { + 'size': len(resource) + } + else: + # Dealing with a detail request + return { + 'foo': 'bar' + } ``` to the serializer. It must return a dict and will be merged with the existing top level `meta`. diff --git a/example/serializers.py b/example/serializers.py index 99cee740..61812337 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -7,10 +7,10 @@ class BlogSerializer(serializers.ModelSerializer): copyright = serializers.SerializerMethodField() - def get_copyright(self, obj): + def get_copyright(self, resource): return datetime.now().year - def get_root_meta(self, obj): + def get_root_meta(self, resource, many): return { 'api_docs': '/docs/api/blogs' } diff --git a/example/tests/integration/test_meta.py b/example/tests/integration/test_meta.py index af69a910..d854a34b 100644 --- a/example/tests/integration/test_meta.py +++ b/example/tests/integration/test_meta.py @@ -7,7 +7,39 @@ pytestmark = pytest.mark.django_db -def test_top_level_meta(blog, client): +def test_top_level_meta_for_list_view(blog, client): + + expected = { + "data": [{ + "type": "blogs", + "id": "1", + "attributes": { + "name": blog.name + }, + "meta": { + "copyright": datetime.now().year + }, + }], + 'links': { + 'first': 'http://testserver/blogs?page=1', + 'last': 'http://testserver/blogs?page=1', + 'next': None, + 'prev': None + }, + 'meta': { + 'pagination': {'count': 1, 'page': 1, 'pages': 1}, + 'apiDocs': '/docs/api/blogs' + } + } + + response = client.get(reverse("blog-list")) + content_dump = redump_json(response.content) + expected_dump = dump_json(expected) + + assert content_dump == expected_dump + + +def test_top_level_meta_for_detail_view(blog, client): expected = { "data": { diff --git a/example/tests/unit/test_renderer_class_methods.py b/example/tests/unit/test_renderer_class_methods.py index 9671b3d4..61208a32 100644 --- a/example/tests/unit/test_renderer_class_methods.py +++ b/example/tests/unit/test_renderer_class_methods.py @@ -61,38 +61,42 @@ def test_extract_meta(): } assert JSONRenderer.extract_meta(serializer, serializer.data) == expected -def test_extract_root_meta(): - def get_root_meta(obj): - return { - 'foo': 'meta-value' - } - serializer = ResourceSerializer() - serializer.get_root_meta = get_root_meta +class ExtractRootMetaResourceSerializer(ResourceSerializer): + def get_root_meta(self, resource, many): + if many: + return { + 'foo': 'meta-many-value' + } + else: + return { + 'foo': 'meta-value' + } + + +class InvalidExtractRootMetaResourceSerializer(ResourceSerializer): + def get_root_meta(self, resource, many): + return 'not a dict' + + +def test_extract_root_meta(): + serializer = ExtractRootMetaResourceSerializer() expected = { 'foo': 'meta-value', } - assert JSONRenderer.extract_root_meta(serializer, {}, {}) == expected + assert JSONRenderer.extract_root_meta(serializer, {}) == expected def test_extract_root_meta_many(): - def get_root_meta(obj): - return { - 'foo': 'meta-value' - } - - serializer = ResourceSerializer(many=True) - serializer.get_root_meta = get_root_meta + serializer = ExtractRootMetaResourceSerializer(many=True) expected = { - 'foo': 'meta-value' + 'foo': 'meta-many-value' } - assert JSONRenderer.extract_root_meta(serializer, {}, {}) == expected + assert JSONRenderer.extract_root_meta(serializer, {}) == expected def test_extract_root_meta_invalid_meta(): - def get_root_meta(obj): + def get_root_meta(resource, many): return 'not a dict' - serializer = ResourceSerializer() - serializer.get_root_meta = get_root_meta + serializer = InvalidExtractRootMetaResourceSerializer() with pytest.raises(AssertionError) as e_info: - JSONRenderer.extract_root_meta(serializer, {}, {}) - + JSONRenderer.extract_root_meta(serializer, {}) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 877ccf8e..ac0517b9 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -338,13 +338,18 @@ def extract_meta(serializer, resource): return data @staticmethod - def extract_root_meta(serializer, resource, meta): + def extract_root_meta(serializer, resource): + many = False + if hasattr(serializer, 'child'): + many = True + serializer = serializer.child + + data = {} if getattr(serializer, 'get_root_meta', None): - root_meta = serializer.get_root_meta(resource) - if root_meta: - assert isinstance(root_meta, dict), 'get_root_meta must return a dict' - meta.update(root_meta) - return meta + json_api_meta = serializer.get_root_meta(resource, many) + assert isinstance(json_api_meta, dict), 'get_root_meta must return a dict' + data.update(json_api_meta) + return data @staticmethod def build_json_resource_obj(fields, resource, resource_instance, resource_name): @@ -412,6 +417,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): else: included_resources = list() + json_api_data = data json_api_included = list() # initialize json_api_meta with pagination meta or an empty dict json_api_meta = data.get('meta', {}) if isinstance(data, dict) else {} @@ -421,51 +427,44 @@ def render(self, data, accepted_media_type=None, renderer_context=None): else: serializer_data = data - if hasattr(serializer_data, 'serializer') and getattr(serializer_data.serializer, 'many', False): - # The below is not true for non-paginated responses - # and isinstance(data, dict): - - # If detail view then json api spec expects dict, otherwise a list - # - http://jsonapi.org/format/#document-top-level - # The `results` key may be missing if unpaginated or an OPTIONS request + serializer = getattr(serializer_data, 'serializer', None) - resource_serializer = serializer_data.serializer + if serializer is not None: # Get the serializer fields - fields = utils.get_serializer_fields(resource_serializer) + fields = utils.get_serializer_fields(serializer) - json_api_data = list() - for position in range(len(serializer_data)): - resource = serializer_data[position] # Get current resource - resource_instance = resource_serializer.instance[position] # Get current instance + # Extract root meta for any type of serializer + json_api_meta.update(self.extract_root_meta(serializer, serializer_data)) - json_resource_obj = self.build_json_resource_obj(fields, resource, resource_instance, resource_name) - meta = self.extract_meta(resource_serializer, resource) - if meta: - json_resource_obj.update({'meta': utils.format_keys(meta)}) - json_api_meta = self.extract_root_meta(resource_serializer, resource, json_api_meta) - json_api_data.append(json_resource_obj) + if getattr(serializer, 'many', False): + json_api_data = list() - included = self.extract_included(fields, resource, resource_instance, included_resources) - if included: - json_api_included.extend(included) - else: - # Check if data contains a serializer - if hasattr(data, 'serializer'): - fields = utils.get_serializer_fields(data.serializer) - resource_instance = data.serializer.instance - json_api_data = self.build_json_resource_obj(fields, data, resource_instance, resource_name) + for position in range(len(serializer_data)): + resource = serializer_data[position] # Get current resource + resource_instance = serializer.instance[position] # Get current instance + + json_resource_obj = self.build_json_resource_obj(fields, resource, resource_instance, resource_name) + meta = self.extract_meta(serializer, resource) + if meta: + json_resource_obj.update({'meta': utils.format_keys(meta)}) + json_api_data.append(json_resource_obj) + + included = self.extract_included(fields, resource, resource_instance, included_resources) + if included: + json_api_included.extend(included) + else: + resource_instance = serializer.instance + json_api_data = self.build_json_resource_obj(fields, serializer_data, resource_instance, resource_name) - meta = self.extract_meta(data.serializer, data) + meta = self.extract_meta(serializer, serializer_data) if meta: json_api_data.update({'meta': utils.format_keys(meta)}) - json_api_meta = self.extract_root_meta(data.serializer, data, json_api_meta) - included = self.extract_included(fields, data, resource_instance, included_resources) + included = self.extract_included(fields, serializer_data, resource_instance, included_resources) if included: json_api_included.extend(included) - else: - json_api_data = data + # Make sure we render data in a specific order render_data = OrderedDict()