mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-20 21:41:04 +03:00
Support for HTML error templates. Fixes #319.
This commit is contained in:
parent
455a8cedcf
commit
b19c58ae17
|
@ -257,6 +257,21 @@ In [the words of Roy Fielding][quote], "A REST API should spend almost all of it
|
||||||
|
|
||||||
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
|
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
|
||||||
|
|
||||||
|
## HTML error views
|
||||||
|
|
||||||
|
Typically a renderer will behave the same regardless of if it's dealing with a regular response, or with a response caused by an exception being raised, such as an `Http404` or `PermissionDenied` exception, or a subclass of `APIException`.
|
||||||
|
|
||||||
|
If you're using either the `TemplateHTMLRenderer` or the `StaticHTMLRenderer` and an exception is raised, the behavior is slightly different, and mirrors [Django's default handling of error views][django-error-views].
|
||||||
|
|
||||||
|
Exceptions raised and handled by an HTML renderer will attempt to render using one of the following methods, by order of precedence.
|
||||||
|
|
||||||
|
* Load and render a template named `{status_code}.html`.
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
|
||||||
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
|
||||||
[conneg]: content-negotiation.md
|
[conneg]: content-negotiation.md
|
||||||
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
|
||||||
|
@ -265,3 +280,4 @@ For good examples of custom media types, see GitHub's use of a custom [applicati
|
||||||
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
|
||||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||||
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
|
||||||
|
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
|
|
@ -10,7 +10,7 @@ import copy
|
||||||
import string
|
import string
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
from django.template import RequestContext, loader
|
from django.template import RequestContext, loader, Template
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
from rest_framework.compat import yaml
|
from rest_framework.compat import yaml
|
||||||
from rest_framework.exceptions import ConfigurationError
|
from rest_framework.exceptions import ConfigurationError
|
||||||
|
@ -162,6 +162,10 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
media_type = 'text/html'
|
media_type = 'text/html'
|
||||||
format = 'html'
|
format = 'html'
|
||||||
template_name = None
|
template_name = None
|
||||||
|
exception_template_names = [
|
||||||
|
'%(status_code)s.html',
|
||||||
|
'api_exception.html'
|
||||||
|
]
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
"""
|
"""
|
||||||
|
@ -178,15 +182,21 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
request = renderer_context['request']
|
request = renderer_context['request']
|
||||||
response = renderer_context['response']
|
response = renderer_context['response']
|
||||||
|
|
||||||
|
if response.exception:
|
||||||
|
template = self.get_exception_template(response)
|
||||||
|
else:
|
||||||
template_names = self.get_template_names(response, view)
|
template_names = self.get_template_names(response, view)
|
||||||
template = self.resolve_template(template_names)
|
template = self.resolve_template(template_names)
|
||||||
context = self.resolve_context(data, request)
|
|
||||||
|
context = self.resolve_context(data, request, response)
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
def resolve_template(self, template_names):
|
def resolve_template(self, template_names):
|
||||||
return loader.select_template(template_names)
|
return loader.select_template(template_names)
|
||||||
|
|
||||||
def resolve_context(self, data, request):
|
def resolve_context(self, data, request, response):
|
||||||
|
if response.exception:
|
||||||
|
data['status_code'] = response.status_code
|
||||||
return RequestContext(request, data)
|
return RequestContext(request, data)
|
||||||
|
|
||||||
def get_template_names(self, response, view):
|
def get_template_names(self, response, view):
|
||||||
|
@ -198,8 +208,21 @@ class TemplateHTMLRenderer(BaseRenderer):
|
||||||
return view.get_template_names()
|
return view.get_template_names()
|
||||||
raise ConfigurationError('Returned a template response with no template_name')
|
raise ConfigurationError('Returned a template response with no template_name')
|
||||||
|
|
||||||
|
def get_exception_template(self, response):
|
||||||
|
template_names = [name % {'status_code': response.status_code}
|
||||||
|
for name in self.exception_template_names]
|
||||||
|
|
||||||
class StaticHTMLRenderer(BaseRenderer):
|
try:
|
||||||
|
# Try to find an appropriate error template
|
||||||
|
return self.resolve_template(template_names)
|
||||||
|
except:
|
||||||
|
# Fall back to using eg '404 Not Found'
|
||||||
|
return Template('%d %s' % (response.status_code,
|
||||||
|
response.status_text.title()))
|
||||||
|
|
||||||
|
|
||||||
|
# Note, subclass TemplateHTMLRenderer simply for the exception behavior
|
||||||
|
class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||||
"""
|
"""
|
||||||
An HTML renderer class that simply returns pre-rendered HTML.
|
An HTML renderer class that simply returns pre-rendered HTML.
|
||||||
|
|
||||||
|
@ -216,6 +239,15 @@ class StaticHTMLRenderer(BaseRenderer):
|
||||||
format = 'html'
|
format = 'html'
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
response = renderer_context['response']
|
||||||
|
|
||||||
|
if response and response.exception:
|
||||||
|
request = renderer_context['request']
|
||||||
|
template = self.get_exception_template(response)
|
||||||
|
context = self.resolve_context(data, request, response)
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ class Response(SimpleTemplateResponse):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data=None, status=200,
|
def __init__(self, data=None, status=200,
|
||||||
template_name=None, headers=None):
|
template_name=None, headers=None,
|
||||||
|
exception=False):
|
||||||
"""
|
"""
|
||||||
Alters the init arguments slightly.
|
Alters the init arguments slightly.
|
||||||
For example, drop 'template_name', and instead use 'data'.
|
For example, drop 'template_name', and instead use 'data'.
|
||||||
|
@ -21,6 +22,7 @@ class Response(SimpleTemplateResponse):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.headers = headers and headers[:] or []
|
self.headers = headers and headers[:] or []
|
||||||
self.template_name = template_name
|
self.template_name = template_name
|
||||||
|
self.exception = exception
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rendered_content(self):
|
def rendered_content(self):
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
|
from django.http import Http404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.template import TemplateDoesNotExist, Template
|
from django.template import TemplateDoesNotExist, Template
|
||||||
import django.template.loader
|
import django.template.loader
|
||||||
|
@ -17,8 +19,22 @@ def example(request):
|
||||||
return Response(data, template_name='example.html')
|
return Response(data, template_name='example.html')
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((TemplateHTMLRenderer,))
|
||||||
|
def permission_denied(request):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(('GET',))
|
||||||
|
@renderer_classes((TemplateHTMLRenderer,))
|
||||||
|
def not_found(request):
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', example),
|
url(r'^$', example),
|
||||||
|
url(r'^permission_denied$', permission_denied),
|
||||||
|
url(r'^not_found$', not_found),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,3 +64,52 @@ class TemplateHTMLRendererTests(TestCase):
|
||||||
response = self.client.get('/')
|
response = self.client.get('/')
|
||||||
self.assertContains(response, "example: foobar")
|
self.assertContains(response, "example: foobar")
|
||||||
self.assertEquals(response['Content-Type'], 'text/html')
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_not_found_html_view(self):
|
||||||
|
response = self.client.get('/not_found')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
self.assertEquals(response.content, "404 Not Found")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_permission_denied_html_view(self):
|
||||||
|
response = self.client.get('/permission_denied')
|
||||||
|
self.assertEquals(response.status_code, 403)
|
||||||
|
self.assertEquals(response.content, "403 Forbidden")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateHTMLRendererExceptionTests(TestCase):
|
||||||
|
urls = 'rest_framework.tests.htmlrenderer'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Monkeypatch get_template
|
||||||
|
"""
|
||||||
|
self.get_template = django.template.loader.get_template
|
||||||
|
|
||||||
|
def get_template(template_name):
|
||||||
|
if template_name == '404.html':
|
||||||
|
return Template("404: {{ detail }}")
|
||||||
|
if template_name == '403.html':
|
||||||
|
return Template("403: {{ detail }}")
|
||||||
|
raise TemplateDoesNotExist(template_name)
|
||||||
|
|
||||||
|
django.template.loader.get_template = get_template
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""
|
||||||
|
Revert monkeypatching
|
||||||
|
"""
|
||||||
|
django.template.loader.get_template = self.get_template
|
||||||
|
|
||||||
|
def test_not_found_html_view_with_template(self):
|
||||||
|
response = self.client.get('/not_found')
|
||||||
|
self.assertEquals(response.status_code, 404)
|
||||||
|
self.assertEquals(response.content, "404: Not found")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
||||||
|
def test_permission_denied_html_view_with_template(self):
|
||||||
|
response = self.client.get('/permission_denied')
|
||||||
|
self.assertEquals(response.status_code, 403)
|
||||||
|
self.assertEquals(response.content, "403: Permission denied")
|
||||||
|
self.assertEquals(response['Content-Type'], 'text/html')
|
||||||
|
|
|
@ -320,13 +320,17 @@ class APIView(View):
|
||||||
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
|
||||||
|
|
||||||
if isinstance(exc, exceptions.APIException):
|
if isinstance(exc, exceptions.APIException):
|
||||||
return Response({'detail': exc.detail}, status=exc.status_code)
|
return Response({'detail': exc.detail},
|
||||||
|
status=exc.status_code,
|
||||||
|
exception=True)
|
||||||
elif isinstance(exc, Http404):
|
elif isinstance(exc, Http404):
|
||||||
return Response({'detail': 'Not found'},
|
return Response({'detail': 'Not found'},
|
||||||
status=status.HTTP_404_NOT_FOUND)
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
exception=True)
|
||||||
elif isinstance(exc, PermissionDenied):
|
elif isinstance(exc, PermissionDenied):
|
||||||
return Response({'detail': 'Permission denied'},
|
return Response({'detail': 'Permission denied'},
|
||||||
status=status.HTTP_403_FORBIDDEN)
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
exception=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user