Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Beni Keller <beni@matraxi.ch>
Boris Pleshakov <koordinator.kun@gmail.com>
Charlie Allatson <charles.allatson@gmail.com>
Christian Zosel <https://zosel.ch>
David Guillot, for Contexte <dguillot@contexte.com>
David Vogt <david.vogt@adfinis-sygroup.ch>
Felix Viernickel <felix@gedankenspieler.org>
Greg Aker <greg@gregaker.net>
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.

## [Unreleased] - TBD

### Added

* Ability for the user to select `included_serializers` to apply when using `BrowsableAPI`, based on available `included_serializers` defined for the current endpoint.


## [4.0.0] - 2020-10-31

This release is not backwards compatible. For easy migration best upgrade first to version
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ override ``settings.REST_FRAMEWORK``
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework_json_api.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
'rest_framework_json_api.renderers.BrowsableAPIRenderer',
),
'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
'DEFAULT_FILTER_BACKENDS': (
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ REST_FRAMEWORK = {
# 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',
Expand Down
2 changes: 1 addition & 1 deletion example/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
29 changes: 29 additions & 0 deletions example/tests/integration/test_browsable_api.py
Original file line number Diff line number Diff line change
@@ -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)
51 changes: 51 additions & 0 deletions rest_framework_json_api/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
19 changes: 19 additions & 0 deletions rest_framework_json_api/templates/rest_framework_json_api/api.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "rest_framework/base.html" %}
{% load i18n %}

{% block request_forms %}
{{ block.super }}
{% if includes_form %}
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#includesModal" class="btn btn-default">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
{% trans "JSON:API includes" %}
</button>
{% endif %}
{% endblock request_forms %}

{% block script %}
{{ block.super }}
{% if includes_form %}
{{ includes_form }}
{% endif %}
{% endblock script %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{% load i18n %}

<div class="modal fade" id="includesModal" tabindex="-1" role="dialog" aria-labelledby="includes" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">{% trans "JSON:API includes" %}</h4>
</div>
<div class="modal-body">
{% for element in elements %}
<div>
<label for="includes-{{ element }}">{{ element }}</label>
<input type="checkbox" name="includes" id="includes-{{ element }}" value="{{ element }}">
</div>
{% endfor %}
<form method="get">
<input type="hidden" name="include">
<button type="submit">{% trans "Apply includes" %}</button>
</form>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
let param_include = new URLSearchParams(window.location.search).get('include')
if (param_include) {
let applied_includes = param_include.split(',')
$('#includesModal input[name=includes]').each(function () {
this.checked = applied_includes.includes(this.value)
})
}
$('#includesModal form').submit(function () {
$('#includesModal input[name=include]').get(0).value = $('#includesModal input[name=includes]:checked').map(
function() {return this.value}
).get().join(",")
})
});
</script>