Repository: django-json-api/django-rest-framework-json-api

Issue:
get_root_meta should add metadata on list endpoints
Currently when viewing a list endpoint metadata can be added to each item in the `data` array but not to the root of the object.

Closes #154 and has a test case demonstrating the bug in PR #179.



Discussion:

Resolving patch:
diff --git a/example/serializers.py b/example/serializers.py
--- 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/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py
--- 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()


Test patch:
diff --git a/example/tests/integration/test_meta.py b/example/tests/integration/test_meta.py
--- 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
--- 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, {})