Skip to content
Open
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
9 changes: 6 additions & 3 deletions docs/api-guide/renderers.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,12 @@ Unlike other renderers, the data passed to the `Response` does not need to be se
The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context.

!!! note
When used with a view that makes use of a serializer the `Response` sent for rendering may not be a dictionary and will need to be wrapped in a dict before returning to allow the `TemplateHTMLRenderer` to render it. For example:
When used with a view that makes use of a serializer, the `Response` sent for rendering may be a list rather than a dictionary, for example with a list view. Lists are automatically wrapped in a dict and made available to the template under the `results` key, alongside a `status_code` key:

response.data = {'results': response.data}
{% for item in results %}{{ item }}{% endfor %}

!!! warning
Up to version 3.17, lists were wrapped under a `details` key instead. `details` still works but is deprecated, and will be removed in 3.20. Update your templates to use `results`.

The template name is determined by (in order of preference):

Expand Down Expand Up @@ -376,7 +379,7 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
* Load and render a template named `api_exception.html`.
* Render the HTTP status code and text, for example "404 Not Found".

Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
Templates will render with a `RequestContext` which includes the `status_code` key alongside the error data, typically under a `detail` key. When the error data is a list, as is the case for a `ValidationError`, it is available under the `results` key instead.

!!! note
If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
Expand Down
10 changes: 10 additions & 0 deletions rest_framework/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class RemovedInDRF319Warning(DeprecationWarning):
pass


class RemovedInDRF320Warning(PendingDeprecationWarning):
pass
Comment on lines +5 to +6


RemovedInNextDRFVersionWarning = RemovedInDRF319Warning
RemovedAfterNextDRFVersionWarning = RemovedInDRF320Warning
54 changes: 51 additions & 3 deletions rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import contextlib
import datetime
import sys
import warnings

from django import forms
from django.conf import settings
Expand All @@ -24,6 +25,7 @@
from rest_framework.compat import (
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, pygments_css, yaml
)
from rest_framework.deprecation import RemovedInDRF320Warning
from rest_framework.exceptions import ParseError
from rest_framework.request import is_form_media_type, override_method
from rest_framework.settings import api_settings
Expand Down Expand Up @@ -107,6 +109,46 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
return ret.encode()


class _DeprecatedResultsList(list):
"""
The list exposed under the legacy `details` template variable, warning on
first use to point users at `results` instead.

The deprecation lives on the value rather than on the context dict, because
Django copies the context dict into a plain `dict` when building the
`RequestContext`, which would drop any `dict` subclass behavior.

See `TemplateHTMLRenderer.get_template_context()`.
"""
_warned = False

def _warn(self):
if not self._warned:
self._warned = True
warnings.warn(
'The "details" template variable is deprecated, '
'update your templates to use "results" instead.',
RemovedInDRF320Warning,
stacklevel=3,
)

def __iter__(self):
self._warn()
return super().__iter__()

def __len__(self):
self._warn()
return super().__len__()

def __getitem__(self, item):
self._warn()
return super().__getitem__(item)

def __repr__(self):
self._warn()
return super().__repr__()


class TemplateHTMLRenderer(BaseRenderer):
"""
An HTML renderer for use with templates.
Expand Down Expand Up @@ -168,10 +210,16 @@ def resolve_template(self, template_names):

def get_template_context(self, data, renderer_context):
response = renderer_context['response']
# in case a ValidationError is caught the data parameter may be a list
# see rest_framework.views.exception_handler
# data may be a list when a list view is used or when a ValidationError
# is raised; wrap it in a dict so Django's template engine can accept it.
# Use 'results' to stay consistent with paginated response conventions.
if isinstance(data, list):
return {'details': data, 'status_code': response.status_code}
return {
'results': data,
# Deprecated alias for 'results', kept until DRF 3.20.
'details': _DeprecatedResultsList(data),
'status_code': response.status_code,
}
if response.exception:
data['status_code'] = response.status_code
return data
Expand Down
59 changes: 59 additions & 0 deletions tests/test_htmlrenderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from rest_framework import status
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.deprecation import RemovedInDRF320Warning
from rest_framework.exceptions import ValidationError
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
Expand Down Expand Up @@ -41,11 +42,27 @@ def validation_error(request):
raise ValidationError('error')


@api_view(('GET',))
@renderer_classes((TemplateHTMLRenderer,))
def list_view(request):
data = [{'name': 'foo'}, {'name': 'bar'}]
return Response(data, template_name='list.html')


@api_view(('GET',))
@renderer_classes((TemplateHTMLRenderer,))
def deprecated_list_view(request):
data = [{'name': 'foo'}, {'name': 'bar'}]
return Response(data, template_name='deprecated_list.html')


urlpatterns = [
path('', example),
path('permission_denied', permission_denied),
path('not_found', not_found),
path('validation_error', validation_error),
path('list', list_view),
path('deprecated_list', deprecated_list_view),
]


Expand All @@ -62,6 +79,7 @@ def _monkey_patch_get_template(self):
Monkeypatch get_template
"""
self.get_template = django.template.loader.get_template
self.select_template = django.template.loader.select_template

def get_template(template_name, dirs=None):
if template_name == 'example.html':
Expand All @@ -71,6 +89,14 @@ def get_template(template_name, dirs=None):
def select_template(template_name_list, dirs=None, using=None):
if template_name_list == ['example.html']:
return engines['django'].from_string("example: {{ object }}")
if template_name_list == ['list.html']:
return engines['django'].from_string(
"{% for item in results %}{{ item.name }}{% endfor %}"
)
if template_name_list == ['deprecated_list.html']:
return engines['django'].from_string(
"{% for item in details %}{{ item.name }}{% endfor %}"
)
raise TemplateDoesNotExist(template_name_list[0])

django.template.loader.get_template = get_template
Comment thread
browniebroke marked this conversation as resolved.
Expand All @@ -81,6 +107,7 @@ def tearDown(self):
Revert monkeypatching
"""
django.template.loader.get_template = self.get_template
django.template.loader.select_template = self.select_template

def test_simple_html_view(self):
response = self.client.get('/')
Expand All @@ -105,6 +132,38 @@ def test_validation_error_html_view(self):
self.assertEqual(response.content, b"400 Bad Request")
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')

def test_list_view_with_template_html_renderer(self):
response = self.client.get('/list')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertContains(response, 'foo')
self.assertContains(response, 'bar')
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')

def test_list_view_with_deprecated_details_variable(self):
with pytest.warns(RemovedInDRF320Warning, match='use "results" instead'):
response = self.client.get('/deprecated_list')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertContains(response, 'foo')
self.assertContains(response, 'bar')

def test_get_template_context_wraps_list_under_results_key(self):
renderer = TemplateHTMLRenderer()

class MockResponse:
template_name = None
exception = False
status_code = 200

context = renderer.get_template_context(
[{'name': 'foo'}], {'response': MockResponse()}
)
assert context['results'] == [{'name': 'foo'}]
assert context['status_code'] == 200

# Deprecated alias for 'results', removed in DRF 3.20
with pytest.warns(RemovedInDRF320Warning, match='use "results" instead'):
assert list(context['details']) == [{'name': 'foo'}]

# 2 tests below are based on order of if statements in corresponding method
# of TemplateHTMLRenderer
def test_get_template_names_returns_own_template_name(self):
Expand Down