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.)
Update 1
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:
- check if we are in
detailview - check GET parameters
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.
- localhost.me:8000/api/users/1/?table
- localhost.me:8000/api/users/1/
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;
}
Update 2
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.





