aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/negotiation.py16
-rw-r--r--rest_framework/renderers.py115
-rw-r--r--rest_framework/response.py29
-rw-r--r--rest_framework/tests/decorators.py2
-rw-r--r--rest_framework/tests/htmlrenderer.py50
-rw-r--r--rest_framework/tests/negotiation.py37
-rw-r--r--rest_framework/views.py16
7 files changed, 177 insertions, 88 deletions
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 0d3b368c..73ae7899 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -1,6 +1,6 @@
from rest_framework import exceptions
from rest_framework.settings import api_settings
-from rest_framework.utils.mediatypes import order_by_precedence
+from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
class BaseContentNegotiation(object):
@@ -46,8 +46,16 @@ class DefaultContentNegotiation(object):
for media_type_set in order_by_precedence(accepts):
for renderer in renderers:
for media_type in media_type_set:
- if renderer.can_handle_media_type(media_type):
- return renderer, media_type
+ if media_type_matches(renderer.media_type, media_type):
+ # Return the most specific media type as accepted.
+ if len(renderer.media_type) > len(media_type):
+ # Eg client requests '*/*'
+ # Accepted media type is 'application/json'
+ return renderer, renderer.media_type
+ else:
+ # Eg client requests 'application/json; indent=8'
+ # Accepted media type is 'application/json; indent=8'
+ return renderer, media_type
raise exceptions.NotAcceptable(available_renderers=renderers)
@@ -57,7 +65,7 @@ class DefaultContentNegotiation(object):
so that we only negotiation against those that accept that format.
"""
renderers = [renderer for renderer in renderers
- if renderer.can_handle_format(format)]
+ if renderer.format == format]
if not renderers:
raise exceptions.InvalidFormat(format)
return renderers
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index e33fa30e..4157468f 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -10,12 +10,13 @@ from django import forms
from django.template import RequestContext, loader
from django.utils import simplejson as json
from rest_framework.compat import yaml
+from rest_framework.exceptions import ConfigurationError
from rest_framework.settings import api_settings
from rest_framework.request import clone_request
from rest_framework.utils import dict2xml
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
+from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param
from rest_framework import VERSION
from rest_framework import serializers
@@ -32,39 +33,8 @@ class BaseRenderer(object):
def __init__(self, view=None):
self.view = view
- def can_handle_format(self, format):
- return format == self.format
-
- def can_handle_media_type(self, media_type):
- """
- Returns `True` if this renderer is able to deal with the given
- media type.
-
- The default implementation for this function is to check the media type
- argument against the media_type attribute set on the class to see if
- they match.
-
- This may be overridden to provide for other behavior, but typically
- you'll instead want to just set the `media_type` attribute on the class.
- """
- return media_type_matches(self.media_type, media_type)
-
- def render(self, obj=None, media_type=None):
- """
- Given an object render it into a string.
-
- The requested media type is also passed to this method,
- as it may contain parameters relevant to how the parser
- should render the output.
- EG: ``application/json; indent=4``
-
- By default render simply returns the output as-is.
- Override this method to provide for other behavior.
- """
- if obj is None:
- return ''
-
- return str(obj)
+ def render(self, data=None, accepted_media_type=None):
+ raise NotImplemented('Renderer class requires .render() to be implemented')
class JSONRenderer(BaseRenderer):
@@ -76,16 +46,16 @@ class JSONRenderer(BaseRenderer):
format = 'json'
encoder_class = encoders.JSONEncoder
- def render(self, obj=None, media_type=None):
+ def render(self, data=None, accepted_media_type=None):
"""
Render `obj` into json.
"""
- if obj is None:
+ if data is None:
return ''
# If the media type looks like 'application/json; indent=4', then
# pretty print the result.
- indent = get_media_type_params(media_type).get('indent', None)
+ indent = get_media_type_params(accepted_media_type).get('indent', None)
sort_keys = False
try:
indent = max(min(int(indent), 8), 0)
@@ -93,7 +63,7 @@ class JSONRenderer(BaseRenderer):
except (ValueError, TypeError):
indent = None
- return json.dumps(obj, cls=self.encoder_class,
+ return json.dumps(data, cls=self.encoder_class,
indent=indent, sort_keys=sort_keys)
@@ -115,7 +85,7 @@ class JSONPRenderer(JSONRenderer):
params = self.view.request.GET
return params.get(self.callback_parameter, self.default_callback)
- def render(self, obj=None, media_type=None):
+ def render(self, data=None, accepted_media_type=None):
"""
Renders into jsonp, wrapping the json output in a callback function.
@@ -123,7 +93,7 @@ class JSONPRenderer(JSONRenderer):
on the URL, for example: ?callback=exampleCallbackName
"""
callback = self.get_callback()
- json = super(JSONPRenderer, self).render(obj, media_type)
+ json = super(JSONPRenderer, self).render(data, accepted_media_type)
return "%s(%s);" % (callback, json)
@@ -135,13 +105,13 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml'
format = 'xml'
- def render(self, obj=None, media_type=None):
+ def render(self, data=None, accepted_media_type=None):
"""
Renders *obj* into serialized XML.
"""
- if obj is None:
+ if data is None:
return ''
- return dict2xml(obj)
+ return dict2xml(data)
class YAMLRenderer(BaseRenderer):
@@ -152,17 +122,17 @@ class YAMLRenderer(BaseRenderer):
media_type = 'application/yaml'
format = 'yaml'
- def render(self, obj=None, media_type=None):
+ def render(self, data=None, accepted_media_type=None):
"""
Renders *obj* into serialized YAML.
"""
- if obj is None:
+ if data is None:
return ''
- return yaml.safe_dump(obj)
+ return yaml.safe_dump(data)
-class TemplateRenderer(BaseRenderer):
+class HTMLTemplateRenderer(BaseRenderer):
"""
A Base class provided for convenience.
@@ -171,30 +141,53 @@ class TemplateRenderer(BaseRenderer):
the :attr:`media_type` and :attr:`template` attributes.
"""
- media_type = None
- template = None
+ media_type = 'text/html'
+ format = 'html'
+ template_name = None
- def render(self, obj=None, media_type=None):
+ def render(self, data=None, accepted_media_type=None):
"""
- Renders *obj* using the :attr:`template` specified on the class.
+ Renders data to HTML, using Django's standard template rendering.
+
+ The template name is determined by (in order of preference):
+
+ 1. An explicit .template_name set on the response.
+ 2. An explicit .template_name set on this class.
+ 3. The return result of calling view.get_template_names().
"""
- if obj is None:
- return ''
+ view = self.view
+ request, response = view.request, view.response
- template = loader.get_template(self.template)
- context = RequestContext(self.view.request, {'object': obj})
+ template_names = self.get_template_names(response, view)
+ template = self.resolve_template(template_names)
+ context = self.resolve_context(data, request)
return template.render(context)
+ def resolve_template(self, template_names):
+ return loader.select_template(template_names)
+
+ def resolve_context(self, data, request):
+ return RequestContext(request, data)
+
+ def get_template_names(self, response, view):
+ if response.template_name:
+ return [response.template_name]
+ elif self.template_name:
+ return [self.template_name]
+ elif hasattr(view, 'get_template_names'):
+ return view.get_template_names()
+ raise ConfigurationError('Returned a template response with no template_name')
+
class DocumentingHTMLRenderer(BaseRenderer):
"""
HTML renderer used to self-document the API.
"""
media_type = 'text/html'
- format = 'html'
+ format = 'api'
template = 'rest_framework/api.html'
- def get_content(self, view, request, obj, media_type):
+ def get_content(self, view, request, data, accepted_media_type):
"""
Get the content as if it had been rendered by a non-documenting renderer.
@@ -208,8 +201,8 @@ class DocumentingHTMLRenderer(BaseRenderer):
if not renderers:
return '[No renderers were found]'
- media_type = add_media_type_param(media_type, 'indent', '4')
- content = renderers[0](view).render(obj, media_type)
+ accepted_media_type = add_media_type_param(accepted_media_type, 'indent', '4')
+ content = renderers[0](view).render(data, accepted_media_type)
if not all(char in string.printable for char in content):
return '[%d bytes of binary content]'
@@ -333,7 +326,7 @@ class DocumentingHTMLRenderer(BaseRenderer):
except AttributeError:
return self.view.__doc__
- def render(self, obj=None, media_type=None):
+ def render(self, data=None, accepted_media_type=None):
"""
Renders *obj* using the :attr:`template` set on the class.
@@ -344,7 +337,7 @@ class DocumentingHTMLRenderer(BaseRenderer):
request = view.request
response = view.response
- content = self.get_content(view, request, obj, media_type)
+ content = self.get_content(view, request, data, accepted_media_type)
put_form = self.get_form(view, 'PUT', request)
post_form = self.get_form(view, 'POST', request)
diff --git a/rest_framework/response.py b/rest_framework/response.py
index db6bf3e2..9a633a8a 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -8,8 +8,8 @@ class Response(SimpleTemplateResponse):
arbitrary media types.
"""
- def __init__(self, data=None, status=None, headers=None,
- renderer=None, accepted_media_type=None):
+ def __init__(self, data=None, status=200,
+ template_name=None, headers=None):
"""
Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'.
@@ -20,26 +20,21 @@ class Response(SimpleTemplateResponse):
super(Response, self).__init__(None, status=status)
self.data = data
self.headers = headers and headers[:] or []
- self.renderer = renderer
-
- # Accepted media type is the portion of the request Accept header
- # that the renderer satisfied. It could be '*/*', or somthing like
- # application/json; indent=4
- #
- # This is NOT the value that will be returned in the 'Content-Type'
- # header, but we do need to know the value in case there are
- # any specific parameters which affect the rendering process.
- self.accepted_media_type = accepted_media_type
+ self.template_name = template_name
@property
def rendered_content(self):
- assert self.renderer, "No renderer set on Response"
+ renderer = self.accepted_renderer
+ media_type = self.accepted_media_type
+
+ assert renderer, "No accepted renderer set on Response"
+ assert media_type, "No accepted media type set on Response"
- self['Content-Type'] = self.renderer.media_type
+ self['Content-Type'] = media_type
if self.data is None:
- return self.renderer.render()
- render_media_type = self.accepted_media_type or self.renderer.media_type
- return self.renderer.render(self.data, render_media_type)
+ return renderer.render()
+
+ return renderer.render(self.data, media_type)
@property
def status_text(self):
diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py
index 4be53786..e943d8fe 100644
--- a/rest_framework/tests/decorators.py
+++ b/rest_framework/tests/decorators.py
@@ -58,7 +58,7 @@ class DecoratorTestCase(TestCase):
request = self.factory.get('/')
response = view(request)
- self.assertTrue(isinstance(response.renderer, JSONRenderer))
+ self.assertTrue(isinstance(response.accepted_renderer, JSONRenderer))
def test_parser_classes(self):
diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py
new file mode 100644
index 00000000..7a7f2743
--- /dev/null
+++ b/rest_framework/tests/htmlrenderer.py
@@ -0,0 +1,50 @@
+from django.conf.urls.defaults import patterns, url
+from django.test import TestCase
+from django.template import TemplateDoesNotExist, Template
+import django.template.loader
+from rest_framework.decorators import api_view, renderer_classes
+from rest_framework.renderers import HTMLTemplateRenderer
+from rest_framework.response import Response
+
+
+@api_view(('GET',))
+@renderer_classes((HTMLTemplateRenderer,))
+def example(request):
+ """
+ A view that can returns an HTML representation.
+ """
+ data = {'object': 'foobar'}
+ return Response(data, template_name='example.html')
+
+
+urlpatterns = patterns('',
+ url(r'^$', example),
+)
+
+
+class HTMLRendererTests(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 == 'example.html':
+ return Template("example: {{ object }}")
+ 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_simple_html_view(self):
+ response = self.client.get('/')
+ self.assertContains(response, "example: foobar")
+ self.assertEquals(response['Content-Type'], 'text/html')
diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py
new file mode 100644
index 00000000..d8265b43
--- /dev/null
+++ b/rest_framework/tests/negotiation.py
@@ -0,0 +1,37 @@
+from django.test import TestCase
+from django.test.client import RequestFactory
+from rest_framework.negotiation import DefaultContentNegotiation
+
+factory = RequestFactory()
+
+
+class MockJSONRenderer(object):
+ media_type = 'application/json'
+
+
+class MockHTMLRenderer(object):
+ media_type = 'text/html'
+
+
+class TestAcceptedMediaType(TestCase):
+ def setUp(self):
+ self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]
+ self.negotiator = DefaultContentNegotiation()
+
+ def negotiate(self, request):
+ return self.negotiator.negotiate(request, self.renderers)
+
+ def test_client_without_accept_use_renderer(self):
+ request = factory.get('/')
+ accepted_renderer, accepted_media_type = self.negotiate(request)
+ self.assertEquals(accepted_media_type, 'application/json')
+
+ def test_client_underspecifies_accept_use_renderer(self):
+ request = factory.get('/', HTTP_ACCEPT='*/*')
+ accepted_renderer, accepted_media_type = self.negotiate(request)
+ self.assertEquals(accepted_media_type, 'application/json')
+
+ def test_client_overspecifies_accept_use_client(self):
+ request = factory.get('/', HTTP_ACCEPT='application/json; indent=8')
+ accepted_renderer, accepted_media_type = self.negotiate(request)
+ self.assertEquals(accepted_media_type, 'application/json; indent=8')
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 2bbdbe17..0359c225 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -199,20 +199,26 @@ class APIView(View):
Runs anything that needs to occur prior to calling the method handlers.
"""
self.format = self.get_format_suffix(**kwargs)
+
if not self.has_permission(request):
self.permission_denied(request)
self.check_throttles(request)
- self.renderer, self.accepted_media_type = self.perform_content_negotiation(request)
+
+ # Perform content negotiation and store the accepted info on the request
+ neg = self.perform_content_negotiation(request)
+ request.accepted_renderer, request.accepted_media_type = neg
def finalize_response(self, request, response, *args, **kwargs):
"""
Returns the final response object.
"""
if isinstance(response, Response):
- if not getattr(self, 'renderer', None):
- self.renderer, self.accepted_media_type = self.perform_content_negotiation(request, force=True)
- response.renderer = self.renderer
- response.accepted_media_type = self.accepted_media_type
+ if not getattr(request, 'accepted_renderer', None):
+ neg = self.perform_content_negotiation(request, force=True)
+ request.accepted_renderer, request.accepted_media_type = neg
+
+ response.accepted_renderer = request.accepted_renderer
+ response.accepted_media_type = request.accepted_media_type
for key, value in self.headers.items():
response[key] = value