Skip to content

Commit 69a720a

Browse files
Add apply includes option in BrowsableAPI
custom BrowsableAPI to add choice of JSON:API includes
1 parent 56ef6f3 commit 69a720a

File tree

9 files changed

+150
-3
lines changed

9 files changed

+150
-3
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Beni Keller <[email protected]>
66
Boris Pleshakov <[email protected]>
77
Charlie Allatson <[email protected]>
88
Christian Zosel <https://zosel.ch>
9+
David Guillot, for Contexte <[email protected]>
910
David Vogt <[email protected]>
1011
Felix Viernickel <[email protected]>
1112
Greg Aker <[email protected]>

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
99
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
1010

11+
## [Unreleased] - TBD
12+
13+
### Added
14+
15+
* Ability for the user to select `included_serializers` to apply when using `BrowsableAPI`, based on available `included_serializers` defined for the current endpoint.
16+
17+
1118
## [4.0.0] - 2020-10-31
1219

1320
This release is not backwards compatible. For easy migration best upgrade first to version

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ override ``settings.REST_FRAMEWORK``
184184
),
185185
'DEFAULT_RENDERER_CLASSES': (
186186
'rest_framework_json_api.renderers.JSONRenderer',
187-
'rest_framework.renderers.BrowsableAPIRenderer',
187+
'rest_framework_json_api.renderers.BrowsableAPIRenderer',
188188
),
189189
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
190190
'DEFAULT_FILTER_BACKENDS': (

docs/usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ REST_FRAMEWORK = {
2828
# If performance testing, enable:
2929
# 'example.utils.BrowsableAPIRendererWithoutForms',
3030
# Otherwise, to play around with the browseable API, enable:
31-
'rest_framework.renderers.BrowsableAPIRenderer'
31+
'rest_framework_json_api.renderers.BrowsableAPIRenderer'
3232
),
3333
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
3434
'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',

example/settings/dev.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
# If performance testing, enable:
8787
# 'example.utils.BrowsableAPIRendererWithoutForms',
8888
# Otherwise, to play around with the browseable API, enable:
89-
'rest_framework.renderers.BrowsableAPIRenderer',
89+
'rest_framework_json_api.renderers.BrowsableAPIRenderer',
9090
),
9191
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
9292
'DEFAULT_SCHEMA_CLASS': 'rest_framework_json_api.schemas.openapi.AutoSchema',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import re
2+
3+
import pytest
4+
from django.urls import reverse
5+
6+
pytestmark = pytest.mark.django_db
7+
8+
9+
def test_browsable_api_with_included_serializers(single_entry, client):
10+
response = client.get(
11+
reverse(
12+
"entry-detail",
13+
kwargs={'pk': single_entry.pk, 'format': 'api'}
14+
)
15+
)
16+
content = str(response.content)
17+
assert response.status_code == 200
18+
assert re.search(r'JSON:API includes', content)
19+
assert re.search(
20+
r'<input type="checkbox" name="includes" [^>]* value="authors.bio"',
21+
content
22+
)
23+
24+
25+
def test_browsable_api_with_no_included_serializers(client):
26+
response = client.get(reverse("projecttype-list", kwargs={'format': 'api'}))
27+
content = str(response.content)
28+
assert response.status_code == 200
29+
assert not re.search(r'JSON:API includes', content)

rest_framework_json_api/renderers.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import inflection
99
from django.db.models import Manager
10+
from django.template import loader
1011
from django.utils import encoding
1112
from rest_framework import relations, renderers
1213
from rest_framework.fields import SkipField, get_attribute
@@ -606,3 +607,53 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
606607
return super(JSONRenderer, self).render(
607608
render_data, accepted_media_type, renderer_context
608609
)
610+
611+
612+
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
613+
template = 'rest_framework_json_api/api.html'
614+
includes_template = 'rest_framework_json_api/includes.html'
615+
616+
def get_context(self, data, accepted_media_type, renderer_context):
617+
context = super(BrowsableAPIRenderer, self).get_context(
618+
data, accepted_media_type, renderer_context
619+
)
620+
view = renderer_context['view']
621+
622+
context['includes_form'] = self.get_includes_form(view)
623+
624+
return context
625+
626+
@classmethod
627+
def _get_included_serializers(cls, serializer, prefix='', already_seen=None):
628+
if not already_seen:
629+
already_seen = set()
630+
631+
if serializer in already_seen:
632+
return []
633+
634+
included_serializers = []
635+
already_seen.add(serializer)
636+
637+
for include, included_serializer in utils.get_included_serializers(serializer).items():
638+
included_serializers.append(f'{prefix}{include}')
639+
included_serializers.extend(
640+
cls._get_included_serializers(
641+
included_serializer, f'{prefix}{include}.',
642+
already_seen=already_seen
643+
)
644+
)
645+
646+
return included_serializers
647+
648+
def get_includes_form(self, view):
649+
try:
650+
serializer_class = view.get_serializer_class()
651+
except AttributeError:
652+
return
653+
654+
if not hasattr(serializer_class, 'included_serializers'):
655+
return
656+
657+
template = loader.get_template(self.includes_template)
658+
context = {'elements': self._get_included_serializers(serializer_class)}
659+
return template.render(context)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% extends "rest_framework/base.html" %}
2+
{% load i18n %}
3+
4+
{% block request_forms %}
5+
{{ block.super }}
6+
{% if includes_form %}
7+
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#includesModal" class="btn btn-default">
8+
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
9+
{% trans "JSON:API includes" %}
10+
</button>
11+
{% endif %}
12+
{% endblock request_forms %}
13+
14+
{% block script %}
15+
{{ block.super }}
16+
{% if includes_form %}
17+
{{ includes_form }}
18+
{% endif %}
19+
{% endblock script %}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{% load i18n %}
2+
3+
<div class="modal fade" id="includesModal" tabindex="-1" role="dialog" aria-labelledby="includes" aria-hidden="true">
4+
<div class="modal-dialog">
5+
<div class="modal-content">
6+
<div class="modal-header">
7+
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
8+
<h4 class="modal-title">{% trans "JSON:API includes" %}</h4>
9+
</div>
10+
<div class="modal-body">
11+
{% for element in elements %}
12+
<div>
13+
<label for="includes-{{ element }}">{{ element }}</label>
14+
<input type="checkbox" name="includes" id="includes-{{ element }}" value="{{ element }}">
15+
</div>
16+
{% endfor %}
17+
<form method="get">
18+
<input type="hidden" name="include">
19+
<button type="submit">{% trans "Apply includes" %}</button>
20+
</form>
21+
</div>
22+
</div>
23+
</div>
24+
</div>
25+
<script>
26+
$(document).ready(function() {
27+
let param_include = new URLSearchParams(window.location.search).get('include')
28+
if (param_include) {
29+
let applied_includes = param_include.split(',')
30+
$('#includesModal input[name=includes]').each(function () {
31+
this.checked = applied_includes.includes(this.value)
32+
})
33+
}
34+
$('#includesModal form').submit(function () {
35+
$('#includesModal input[name=include]').get(0).value = $('#includesModal input[name=includes]:checked').map(
36+
function() {return this.value}
37+
).get().join(",")
38+
})
39+
});
40+
</script>

0 commit comments

Comments
 (0)