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 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 <beni@matraxi.ch>
66
Boris Pleshakov <koordinator.kun@gmail.com>
77
Charlie Allatson <charles.allatson@gmail.com>
88
Christian Zosel <https://zosel.ch>
9+
David Guillot, for Contexte <dguillot@contexte.com>
910
David Vogt <david.vogt@adfinis-sygroup.ch>
1011
Felix Viernickel <felix@gedankenspieler.org>
1112
Greg Aker <greg@gregaker.net>

‎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)

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