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

Issue:
[Feature proposal] Allow choosing included resources from browsable API
Hi there,

Title says it all: our frontend team asked me if it would be possible, in the browsable API view, to choose related resource to include, among the list of possible includes defined by serializers. I think it's a good idea, and actually the code to achieve that is generic enough to be proposed as an addition to this awesome lib.

What do you think?


Discussion:

Resolving patch:
diff --git a/example/settings/dev.py b/example/settings/dev.py
--- a/example/settings/dev.py
+++ b/example/settings/dev.py
@@ -86,7 +86,7 @@
         # If performance testing, enable:
         # 'example.utils.BrowsableAPIRendererWithoutForms',
         # Otherwise, to play around with the browseable API, enable:
-        'rest_framework.renderers.BrowsableAPIRenderer',
+        'rest_framework_json_api.renderers.BrowsableAPIRenderer',
     ),
     'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
     'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',
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
@@ -7,6 +7,7 @@
 
 import inflection
 from django.db.models import Manager
+from django.template import loader
 from django.utils import encoding
 from rest_framework import relations, renderers
 from rest_framework.fields import SkipField, get_attribute
@@ -606,3 +607,53 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
         return super(JSONRenderer, self).render(
             render_data, accepted_media_type, renderer_context
         )
+
+
+class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
+    template = 'rest_framework_json_api/api.html'
+    includes_template = 'rest_framework_json_api/includes.html'
+
+    def get_context(self, data, accepted_media_type, renderer_context):
+        context = super(BrowsableAPIRenderer, self).get_context(
+            data, accepted_media_type, renderer_context
+        )
+        view = renderer_context['view']
+
+        context['includes_form'] = self.get_includes_form(view)
+
+        return context
+
+    @classmethod
+    def _get_included_serializers(cls, serializer, prefix='', already_seen=None):
+        if not already_seen:
+            already_seen = set()
+
+        if serializer in already_seen:
+            return []
+
+        included_serializers = []
+        already_seen.add(serializer)
+
+        for include, included_serializer in utils.get_included_serializers(serializer).items():
+            included_serializers.append(f'{prefix}{include}')
+            included_serializers.extend(
+                cls._get_included_serializers(
+                    included_serializer, f'{prefix}{include}.',
+                    already_seen=already_seen
+                )
+            )
+
+        return included_serializers
+
+    def get_includes_form(self, view):
+        try:
+            serializer_class = view.get_serializer_class()
+        except AttributeError:
+            return
+
+        if not hasattr(serializer_class, 'included_serializers'):
+            return
+
+        template = loader.get_template(self.includes_template)
+        context = {'elements': self._get_included_serializers(serializer_class)}
+        return template.render(context)


Test patch:
diff --git a/example/tests/integration/test_browsable_api.py b/example/tests/integration/test_browsable_api.py
new file mode 100644
--- /dev/null
+++ b/example/tests/integration/test_browsable_api.py
@@ -0,0 +1,29 @@
+import re
+
+import pytest
+from django.urls import reverse
+
+pytestmark = pytest.mark.django_db
+
+
+def test_browsable_api_with_included_serializers(single_entry, client):
+    response = client.get(
+        reverse(
+            "entry-detail",
+            kwargs={'pk': single_entry.pk, 'format': 'api'}
+        )
+    )
+    content = str(response.content)
+    assert response.status_code == 200
+    assert re.search(r'JSON:API includes', content)
+    assert re.search(
+        r'<input type="checkbox" name="includes" [^>]* value="authors.bio"',
+        content
+    )
+
+
+def test_browsable_api_with_no_included_serializers(client):
+    response = client.get(reverse("projecttype-list", kwargs={'format': 'api'}))
+    content = str(response.content)
+    assert response.status_code == 200
+    assert not re.search(r'JSON:API includes', content)