Reputation: 372
I'm using Django Rest Framework 3.11.0 and I want to use the BrowsableAPIRenderer with a customized template for rendering the details of an instance. I only want to override the rendering of the dict/json, marked red in the image below, and I want to keep all the rest.
By overwriting restframework/api.html
I only managed to change the title, heading and some fields, but I did not find a way to render the details of an instance e.g. in a table. Is there a way to do this?
Clarification: I have models with large dictionaries that I want to display prettier than just as inline strings. I thin that when I find out how to customize the (already beautiful) Django RestFramework BrowsableAPI, I will also be able to solve my problem.
(Look at my Update 2 in case you want to solve a similar problem.)
This is where I got with Bedilbeks answer (up to the first update).
I don't want to change all views, so I'm not registering the renderer globally.
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
# 'users.renderers.CustomBrowsableAPIRenderer',
]
}
Instead I am setting the renderer_classes
for my UserViewSet
and use my CustomBrowsableAPIRenderer
here.
class UserViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin):
queryset = UserModel.objects.all()
serializer_class = UserSerializer
renderer_classes = [renderers.JSONRenderer, CustomBrowsableAPIRenderer]
I need to override the api.html
template, but I do not want this change to apply everywhere, so I am dynamically choosing the the template in the renderer. By default the BrowsableAPIRenderer
has a template = "rest_framework/api.html"
property, but I need logic so I'm using the @property
decorator to do the following:
detail
view If we are in detail view and a "table"
parameter is present, return my template, else return the default.
class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
@property
def template(self):
view = self.renderer_context.get("view", {})
table = "table" in view.request.query_params
if view and hasattr(view, "detail") and view.detail and table:
return "users/api.html" # custom template
else:
return "rest_framework/api.html" # default
def get_default_renderer(self, view):
table = "table" in view.request.query_params
if hasattr(view, "detail") and view.detail and table:
return TableHtmlRenderer()
return super().get_default_renderer(view)
The crucial section of api.html
looks like this (around line 123).
...
{% block style %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "css/api.css" %}"/>
{% endblock %}
<!-- HERE IS THE ACTUAL CONTENT -->
</span></pre><div class="prettyprint" style="overflow: auto;">{{ content|urlize_quoted_links }}</div>
</div>
...
I'm actually not doing this for the User
model and ViewSet, but I'm sticking to it for the sake of the example. In my model, I have larger JSON elements that I want to render, so I am doing some preprocessing in my TableHTMLRenderer
to return JSON in indented form.
class TableHtmlRenderer(TemplateHTMLRenderer):
media_type = "text/html"
format = "api"
template_name = "table_template.html"
def get_template_context(self, data, renderer_context):
for key in data.keys():
try:
data[key] = json.dumps(json.loads(data[key]), indent=4)
except (JSONDecodeError, TypeError):
pass
context = {
"data": data
}
response = renderer_context["response"]
if response.exception:
context["status_code"] = response.status_code
return context
So controlled by the URL, I can switch between the default renderer and the Custom/Table renderer.
So far so good, I now have my own Renderer classes and I can modify how the API view for my User instance looks. I'm still struggling with the table, because line-breaks on long lines don't work and it won't stay inside of the div's boundaries.
Here is the app.css
which is loaded in the api.html
template.
pre.inline {
padding: 0;
border: none;
word-break: break-all;
word-wrap: break-word;
display: contents;
}
table, th, td {
vertical-align: top;
padding: 2px;
text-align: left;}
table {
//table-layout: fixed;
width: 100% !important;
word-wrap:break-word;
}
th, td {
border-bottom: 1px solid #ddd;
overflow: auto;
width: 100%;
}
tr:hover {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f5f5f5;
}
Since I can now display some views with a customized BrowsableAPIRenderer and templates with quite a few hacks, I came back to the problem that lead me to this question. I wanted to get to know how DRF renders my models, to make changes in order to display large nested dictionaries.
I found out that the BrowsableAPIRenderer
inserts the model contents as a single string int he api.html
template like this {{ content|urlize_quoted_links }}
inside of <pre>
tags.
The insertion takes place in the BrowsableAPIRenderer.get_content
method.
# original code
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
I now see that all I had to do is subclass BrowsableAPIRenderer
and override the get_content
method. I'm doing it like this.
class LogBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_content(self, renderer, data, accepted_media_type, renderer_context):
"""
Extends BrowsableAPIRenderer.get_content.
"""
if not renderer:
return '[No renderers were found]'
renderer_context['indent'] = 4
# content = renderer.render(data, accepted_media_type, renderer_context)
# try to convert all string-values into dictionaries
data_dict = dict(data.items())
for k in data_dict.keys():
try:
data_dict[k] = json.loads(data_dict[k], strict=False)
except JSONDecodeError:
# ignore errors and move on for now
pass
# dump into indented string again
content = json.dumps(data_dict, indent=4, sort_keys=True).encode(encoding="utf-8")
render_style = getattr(renderer, 'render_style', 'text')
assert render_style in ['text', 'binary'], 'Expected .render_style "text" or "binary", but got "%s"' % render_style
if render_style == 'binary':
return '[%d bytes of binary content]' % len(content)
return content
I also realize that I could have worded my question differently to maybe come to this closure more quickly.
Upvotes: 6
Views: 4335
Reputation: 899
This is not the most appropriate and best answer, but I think it is more or less what you want and it is a little hack.
Let's say we want to expose /users/ endpoint and we have the following views.py
:
from django.contrib.auth import get_user_model
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
UserModel = get_user_model()
class UserSerializer(ModelSerializer):
class Meta:
model = UserModel
fields = ('first_name', 'last_name')
class UserViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin):
queryset = UserModel.objects.all()
serializer_class = UserSerializer
router = DefaultRouter()
router.register('users', UserViewSet)
urlpatterns = router.urls
So, first, let's create renderers.py
inside one of our apps (let's say we have users
app):
from rest_framework.renderers import BrowsableAPIRenderer, TemplateHTMLRenderer
class TableHtmlRenderer(TemplateHTMLRenderer):
media_type = 'text/html'
format = 'api'
template_name = 'users/table_template.html'
def get_template_context(self, data, renderer_context):
context = {'data': data}
response = renderer_context['response']
if response.exception:
context['status_code'] = response.status_code
return context
class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_default_renderer(self, view):
if view.detail:
return TableHtmlRenderer()
return super().get_default_renderer(view)
In this way we are overriding BrowsableAPIRenderer
with our CustomBrowsableAPIRenderer
just to change the default renderer for our content
(In our case it is dict
obj that is json serializable). We check whether our view is detail=True
by accessing viewset.detail attribute.
Then, we need to override existing TemplateHTMLRenderer
with our own TableHTMLRenderer
to give our dict object as a data
to a context of users/table_template.html
So, now we have to create our users/table_template.html
:
<table>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{% for key, value in data.items %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</table>
Now, we are ready to check just after enabling our renderers in the settings.py
:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'users.renderers.CustomBrowsableAPIRenderer',
]
}
If you see, now we replaced rest_framework.renderers.BrowsableAPIRenderer
with our own users.renderers.CustomBrowsableAPIRenderer
custom renderer.
We see that now we have table instead of json structure.
I hope, this should work on your side too.
UPDATE
If we want to get rid of whitespace or we want more of a custom modification, we have to override and change the api.html
. So, we create our own users/api.html
:
{% extends "rest_framework/base.html" %}
{% load i18n %}
{% load rest_framework %}
{% block content %}
...
</span></pre>
<div class="prettyprint">{{ content|urlize_quoted_links }}
</div>
</div>
...
{% endblock content %}
where ...
3 dots are put, we have to copy and paste the rest of the base.html
starting from content
block because content
block is too big and we want to change something that inside this block. We can not do it without copy-pasting the whole content
block. If we compare our template with old base.html
, the place where </span>
element finishes, we remove {{ content|urlize_quoted_links }}
context and put it inside a new div after the end of </pre>
element as we see in the code example, so that now we do not use <pre>
element and no extra whitespace is put.
Then, if we look at the result:
But, what is bad, now we have changed and overrode the whole template, so we broke other views too:
As a result, you see that our result became clumsy and we have made too much of hack to come to result that we want. That's why I would not really recommend overriding api.html
just to change that content
part. Instead, I would use my first answer which is less hacky, by overriding renderer classes.
Upvotes: 1