mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +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.
|
||||
|
||||
## 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
|
||||
[conneg]: content-negotiation.md
|
||||
[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
|
||||
[application/vnd.github+json]: http://developer.github.com/v3/media/
|
||||
[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
|
||||
from django import forms
|
||||
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 rest_framework.compat import yaml
|
||||
from rest_framework.exceptions import ConfigurationError
|
||||
|
@ -162,6 +162,10 @@ class TemplateHTMLRenderer(BaseRenderer):
|
|||
media_type = 'text/html'
|
||||
format = 'html'
|
||||
template_name = None
|
||||
exception_template_names = [
|
||||
'%(status_code)s.html',
|
||||
'api_exception.html'
|
||||
]
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -178,15 +182,21 @@ class TemplateHTMLRenderer(BaseRenderer):
|
|||
request = renderer_context['request']
|
||||
response = renderer_context['response']
|
||||
|
||||
if response.exception:
|
||||
template = self.get_exception_template(response)
|
||||
else:
|
||||
template_names = self.get_template_names(response, view)
|
||||
template = self.resolve_template(template_names)
|
||||
context = self.resolve_context(data, request)
|
||||
|
||||
context = self.resolve_context(data, request, response)
|
||||
return template.render(context)
|
||||
|
||||
def resolve_template(self, 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)
|
||||
|
||||
def get_template_names(self, response, view):
|
||||
|
@ -198,8 +208,21 @@ class TemplateHTMLRenderer(BaseRenderer):
|
|||
return view.get_template_names()
|
||||
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.
|
||||
|
||||
|
@ -216,6 +239,15 @@ class StaticHTMLRenderer(BaseRenderer):
|
|||
format = 'html'
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ class Response(SimpleTemplateResponse):
|
|||
"""
|
||||
|
||||
def __init__(self, data=None, status=200,
|
||||
template_name=None, headers=None):
|
||||
template_name=None, headers=None,
|
||||
exception=False):
|
||||
"""
|
||||
Alters the init arguments slightly.
|
||||
For example, drop 'template_name', and instead use 'data'.
|
||||
|
@ -21,6 +22,7 @@ class Response(SimpleTemplateResponse):
|
|||
self.data = data
|
||||
self.headers = headers and headers[:] or []
|
||||
self.template_name = template_name
|
||||
self.exception = exception
|
||||
|
||||
@property
|
||||
def rendered_content(self):
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.conf.urls.defaults import patterns, url
|
||||
from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.template import TemplateDoesNotExist, Template
|
||||
import django.template.loader
|
||||
|
@ -17,8 +19,22 @@ def example(request):
|
|||
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('',
|
||||
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('/')
|
||||
self.assertContains(response, "example: foobar")
|
||||
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
|
||||
|
||||
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):
|
||||
return Response({'detail': 'Not found'},
|
||||
status=status.HTTP_404_NOT_FOUND)
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
exception=True)
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
return Response({'detail': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
exception=True)
|
||||
raise
|
||||
|
||||
# Note: session based authentication is explicitly CSRF validated,
|
||||
|
|
Loading…
Reference in New Issue
Block a user