diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 1b266f7ef..b2ebd0c7e 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -132,7 +132,7 @@ Renders data into HTML for the Browseable API. This renderer will determine whi
## Custom renderers
-To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type)` method.
+To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method.
For example:
@@ -144,11 +144,26 @@ For example:
media_type = 'text/plain'
format = 'txt'
- def render(self, data, media_type):
+ def render(self, data, media_type=None, renderer_context=None):
if isinstance(data, basestring):
return data
return smart_unicode(data)
+The arguments passed to the `.render()` method are:
+
+#### `data`
+
+The request data, as set by the `Response()` instantiation.
+
+#### `media_type=None`
+
+Optional. If provided, this is the accepted media type, as determined by the content negotiation stage. Depending on the client's `Accept:` header, this may be more specific than the renderer's `media_type` attribute, and may include media type parameters. For example `"application/json; nested=true"`.
+
+#### `renderer_context=None`
+
+Optional. If provided, this is a dictionary of contextual information provided by the view.
+By default this will include the following keys: `view`, `request`, `response`, `args`, `kwargs`.
+
---
# Advanced renderer usage
diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md
index e01983240..b0de6824e 100644
--- a/docs/api-guide/responses.md
+++ b/docs/api-guide/responses.md
@@ -82,6 +82,11 @@ The media type that was selected by the content negotiation stage.
Set automatically by the `APIView` or `@api_view` immediately before the response is returned from the view.
+## .renderer_context
+
+A dictionary of additional context information that will be passed to the renderer's `.render()` method.
+
+Set automatically by the `APIView` or `@api_view` immediately before the response is returned from the view.
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/
[statuscodes]: status-codes.md
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 25e6ed628..27a85ab12 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -16,7 +16,7 @@ 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
+from rest_framework.utils.mediatypes import get_media_type_params
from rest_framework import VERSION
from rest_framework import serializers
@@ -30,10 +30,7 @@ class BaseRenderer(object):
media_type = None
format = None
- def __init__(self, view=None):
- self.view = view
-
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
raise NotImplemented('Renderer class requires .render() to be implemented')
@@ -46,22 +43,29 @@ class JSONRenderer(BaseRenderer):
format = 'json'
encoder_class = encoders.JSONEncoder
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Render `obj` into json.
"""
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(accepted_media_type).get('indent', None)
- sort_keys = False
- try:
- indent = max(min(int(indent), 8), 0)
- sort_keys = True
- except (ValueError, TypeError):
- indent = None
+ # If 'indent' is provided in the context, then pretty print the result.
+ # E.g. If we're being called by the BrowseableAPIRenderer.
+ renderer_context = renderer_context or {}
+ indent = renderer_context.get('indent', None)
+ sort_keys = indent and True or False
+
+ if accepted_media_type:
+ # If the media type looks like 'application/json; indent=4',
+ # then pretty print the result.
+ params = get_media_type_params(accepted_media_type)
+ indent = params.get('indent', indent)
+ try:
+ indent = max(min(int(indent), 8), 0)
+ sort_keys = True
+ except (ValueError, TypeError):
+ indent = None
return json.dumps(data, cls=self.encoder_class,
indent=indent, sort_keys=sort_keys)
@@ -78,22 +82,25 @@ class JSONPRenderer(JSONRenderer):
callback_parameter = 'callback'
default_callback = 'callback'
- def get_callback(self):
+ def get_callback(self, renderer_context):
"""
Determine the name of the callback to wrap around the json output.
"""
- params = self.view.request.GET
+ request = renderer_context.get('request', None)
+ params = request and request.GET or {}
return params.get(self.callback_parameter, self.default_callback)
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Renders into jsonp, wrapping the json output in a callback function.
Clients may set the callback function name using a query parameter
on the URL, for example: ?callback=exampleCallbackName
"""
- callback = self.get_callback()
- json = super(JSONPRenderer, self).render(data, accepted_media_type)
+ renderer_context = renderer_context or {}
+ callback = self.get_callback(renderer_context)
+ json = super(JSONPRenderer, self).render(data, accepted_media_type,
+ renderer_context)
return "%s(%s);" % (callback, json)
@@ -105,7 +112,7 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml'
format = 'xml'
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Renders *obj* into serialized XML.
"""
@@ -122,7 +129,7 @@ class YAMLRenderer(BaseRenderer):
media_type = 'application/yaml'
format = 'yaml'
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Renders *obj* into serialized YAML.
"""
@@ -145,7 +152,7 @@ class HTMLRenderer(BaseRenderer):
format = 'html'
template_name = None
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Renders data to HTML, using Django's standard template rendering.
@@ -155,8 +162,10 @@ class HTMLRenderer(BaseRenderer):
2. An explicit .template_name set on this class.
3. The return result of calling view.get_template_names().
"""
- view = self.view
- request, response = view.request, view.response
+ renderer_context = renderer_context or {}
+ view = renderer_context['view']
+ request = renderer_context['request']
+ response = renderer_context['response']
template_names = self.get_template_names(response, view)
template = self.resolve_template(template_names)
@@ -187,22 +196,29 @@ class BrowsableAPIRenderer(BaseRenderer):
format = 'api'
template = 'rest_framework/api.html'
- def get_content(self, view, request, data, accepted_media_type):
+ def get_default_renderer(self, view):
"""
- Get the content as if it had been rendered by a non-documenting renderer.
-
- (Typically this will be the content as it would have been if the Resource had been
- requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)
+ Return an instance of the first valid renderer.
+ (Don't use another documenting renderer.)
"""
-
- # Find the first valid renderer and render the content. (Don't use another documenting renderer.)
renderers = [renderer for renderer in view.renderer_classes
if not issubclass(renderer, BrowsableAPIRenderer)]
if not renderers:
+ return None
+ return renderers[0]()
+
+ def get_content(self, renderer, data,
+ accepted_media_type, renderer_context):
+ """
+ Get the content as if it had been rendered by the default
+ non-documenting renderer.
+ """
+ if not renderer:
return '[No renderers were found]'
- accepted_media_type = add_media_type_param(accepted_media_type, 'indent', '4')
- content = renderers[0](view).render(data, accepted_media_type)
+ renderer_context['indent'] = 4
+ content = renderer.render(data, accepted_media_type, renderer_context)
+
if not all(char in string.printable for char in content):
return '[%d bytes of binary content]'
@@ -228,7 +244,8 @@ class BrowsableAPIRenderer(BaseRenderer):
return True # Don't actually need to return a form
if not getattr(view, 'get_serializer', None):
- return self.get_generic_content_form(view)
+ media_types = [parser.media_type for parser in view.parser_classes]
+ return self.get_generic_content_form(media_types)
#####
# TODO: This is a little bit of a hack. Actually we'd like to remove
@@ -273,9 +290,10 @@ class BrowsableAPIRenderer(BaseRenderer):
form_instance = OnTheFlyForm(data)
return form_instance
- def get_generic_content_form(self, view):
+ def get_generic_content_form(self, media_types):
"""
- Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
+ Returns a form that allows for arbitrary content types to be tunneled
+ via standard HTML forms.
(Which are typically application/x-www-form-urlencoded)
"""
@@ -285,74 +303,68 @@ class BrowsableAPIRenderer(BaseRenderer):
and api_settings.FORM_CONTENTTYPE_OVERRIDE):
return None
+ content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
+ content_field = api_settings.FORM_CONTENT_OVERRIDE
+ choices = [(media_type, media_type) for media_type in media_types]
+ initial = media_types[0]
+
# NB. http://jacobian.org/writing/dynamic-form-generation/
class GenericContentForm(forms.Form):
- def __init__(self, view, request):
- """We don't know the names of the fields we want to set until the point the form is instantiated,
- as they are determined by the Resource the form is being created against.
- Add the fields dynamically."""
+ def __init__(self):
super(GenericContentForm, self).__init__()
- parsed_media_types = [parser.media_type for parser in view.parser_classes]
- contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types]
- initial_contenttype = parsed_media_types[0]
-
- self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField(
+ self.fields[content_type_field] = forms.ChoiceField(
label='Content Type',
- choices=contenttype_choices,
- initial=initial_contenttype
+ choices=choices,
+ initial=initial
)
- self.fields[api_settings.FORM_CONTENT_OVERRIDE] = forms.CharField(
+ self.fields[content_field] = forms.CharField(
label='Content',
widget=forms.Textarea
)
- # If either of these reserved parameters are turned off then content tunneling is not possible
- if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None:
- return None
+ return GenericContentForm()
- # Okey doke, let's do it
- return GenericContentForm(view, view.request)
-
- def get_name(self):
+ def get_name(self, view):
try:
- return self.view.get_name()
+ return view.get_name()
except AttributeError:
- return self.view.__doc__
+ return view.__doc__
- def get_description(self, html=None):
- if html is None:
- html = bool('html' in self.format)
+ def get_description(self, view):
try:
- return self.view.get_description(html)
+ return view.get_description(html=True)
except AttributeError:
- return self.view.__doc__
+ return view.__doc__
- def render(self, data=None, accepted_media_type=None):
+ def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Renders *obj* using the :attr:`template` set on the class.
The context used in the template contains all the information
needed to self-document the response to this request.
"""
- view = self.view
- request = view.request
- response = view.response
+ accepted_media_type = accepted_media_type or ''
+ renderer_context = renderer_context or {}
- content = self.get_content(view, request, data, accepted_media_type)
+ view = renderer_context['view']
+ request = renderer_context['request']
+ response = renderer_context['response']
+
+ renderer = self.get_default_renderer(view)
+ content = self.get_content(renderer, data, accepted_media_type, renderer_context)
put_form = self.get_form(view, 'PUT', request)
post_form = self.get_form(view, 'POST', request)
delete_form = self.get_form(view, 'DELETE', request)
options_form = self.get_form(view, 'OPTIONS', request)
- name = self.get_name()
- description = self.get_description()
-
- breadcrumb_list = get_breadcrumbs(self.view.request.path)
+ name = self.get_name(view)
+ description = self.get_description(view)
+ breadcrumb_list = get_breadcrumbs(request.path)
template = loader.get_template(self.template)
- context = RequestContext(self.view.request, {
+ context = RequestContext(request, {
'content': content,
'view': view,
'request': request,
@@ -375,7 +387,7 @@ class BrowsableAPIRenderer(BaseRenderer):
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include
# the normal deletion response code in the output)
- if self.view.response.status_code == 204:
- self.view.response.status_code = 200
+ if response.status_code == 204:
+ response.status_code = 200
return ret
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 9a633a8a7..7a459c8f5 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -24,17 +24,17 @@ class Response(SimpleTemplateResponse):
@property
def rendered_content(self):
- renderer = self.accepted_renderer
- media_type = self.accepted_media_type
+ renderer = getattr(self, 'accepted_renderer', None)
+ media_type = getattr(self, 'accepted_media_type', None)
+ context = getattr(self, 'renderer_context', None)
- assert renderer, "No accepted renderer set on Response"
- assert media_type, "No accepted media type set on Response"
+ assert renderer, ".accepted_renderer not set on Response"
+ assert media_type, ".accepted_media_type not set on Response"
+ assert context, ".renderer_context not set on Response"
+ context['response'] = self
self['Content-Type'] = media_type
- if self.data is None:
- return renderer.render()
-
- return renderer.render(self.data, media_type)
+ return renderer.render(self.data, media_type, context)
@property
def status_text(self):
@@ -42,4 +42,6 @@ class Response(SimpleTemplateResponse):
Returns reason text corresponding to our HTTP response status code.
Provided for convenience.
"""
+ # TODO: Deprecate and use a template tag instead
+ # TODO: Status code text for RFC 6585 status codes
return STATUS_CODE_TEXT.get(self.status_code, '')
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index b8c30fcc5..48d8d9bd7 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -41,16 +41,16 @@ class RendererA(BaseRenderer):
media_type = 'mock/renderera'
format = "formata"
- def render(self, obj=None, media_type=None):
- return RENDERER_A_SERIALIZER(obj)
+ def render(self, data, media_type=None, renderer_context=None):
+ return RENDERER_A_SERIALIZER(data)
class RendererB(BaseRenderer):
media_type = 'mock/rendererb'
format = "formatb"
- def render(self, obj=None, media_type=None):
- return RENDERER_B_SERIALIZER(obj)
+ def render(self, data, media_type=None, renderer_context=None):
+ return RENDERER_B_SERIALIZER(data)
class MockView(APIView):
@@ -235,7 +235,7 @@ class JSONRendererTests(TestCase):
Test basic JSON rendering.
"""
obj = {'foo': ['bar', 'baz']}
- renderer = JSONRenderer(None)
+ renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
# Fix failing test case which depends on version of JSON library.
self.assertEquals(content, _flat_repr)
@@ -245,7 +245,7 @@ class JSONRendererTests(TestCase):
Test JSON rendering with additional content type arguments supplied.
"""
obj = {'foo': ['bar', 'baz']}
- renderer = JSONRenderer(None)
+ renderer = JSONRenderer()
content = renderer.render(obj, 'application/json; indent=2')
self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
@@ -302,7 +302,7 @@ if yaml:
Test basic YAML rendering.
"""
obj = {'foo': ['bar', 'baz']}
- renderer = YAMLRenderer(None)
+ renderer = YAMLRenderer()
content = renderer.render(obj, 'application/yaml')
self.assertEquals(content, _yaml_repr)
@@ -313,7 +313,7 @@ if yaml:
"""
obj = {'foo': ['bar', 'baz']}
- renderer = YAMLRenderer(None)
+ renderer = YAMLRenderer()
parser = YAMLParser()
content = renderer.render(obj, 'application/yaml')
@@ -345,7 +345,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render({'field': 'astring'}, 'application/xml')
self.assertXMLContains(content, 'astring')
@@ -353,7 +353,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render({'field': 111}, 'application/xml')
self.assertXMLContains(content, '111')
@@ -361,7 +361,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render({
'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
}, 'application/xml')
@@ -371,7 +371,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render({'field': 123.4}, 'application/xml')
self.assertXMLContains(content, '123.4')
@@ -379,7 +379,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
self.assertXMLContains(content, '111.2')
@@ -387,7 +387,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render({'field': None}, 'application/xml')
self.assertXMLContains(content, '')
@@ -395,7 +395,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = renderer.render(self._complex_data, 'application/xml')
self.assertXMLContains(content, 'first')
self.assertXMLContains(content, 'second')
@@ -404,7 +404,7 @@ class XMLRendererTestCase(TestCase):
"""
Test XML rendering.
"""
- renderer = XMLRenderer(None)
+ renderer = XMLRenderer()
content = StringIO(renderer.render(self._complex_data, 'application/xml'))
parser = XMLParser()
diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py
index d1625b67d..18b6af394 100644
--- a/rest_framework/tests/response.py
+++ b/rest_framework/tests/response.py
@@ -33,16 +33,16 @@ class RendererA(BaseRenderer):
media_type = 'mock/renderera'
format = "formata"
- def render(self, obj=None, media_type=None):
- return RENDERER_A_SERIALIZER(obj)
+ def render(self, data, media_type=None, renderer_context=None):
+ return RENDERER_A_SERIALIZER(data)
class RendererB(BaseRenderer):
media_type = 'mock/rendererb'
format = "formatb"
- def render(self, obj=None, media_type=None):
- return RENDERER_B_SERIALIZER(obj)
+ def render(self, data, media_type=None, renderer_context=None):
+ return RENDERER_B_SERIALIZER(data)
class MockView(APIView):
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 058a6cd38..b3f360851 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -86,6 +86,7 @@ class APIView(View):
@property
def default_response_headers(self):
+ # TODO: deprecate?
# TODO: Only vary by accept if multiple renderers
return {
'Allow': ', '.join(self.allowed_methods),
@@ -158,6 +159,20 @@ class APIView(View):
"""
raise exceptions.Throttled(wait)
+ def get_renderer_context(self):
+ """
+ Returns a dict that is passed through to the Renderer.render(),
+ as the `renderer_context` keyword argument.
+ """
+ # Note: Additionally 'response' will also be set on the context,
+ # by the Response object.
+ return {
+ 'view': self,
+ 'request': self.request,
+ 'args': self.args,
+ 'kwargs': self.kwargs
+ }
+
# API policy instantiation methods
def get_format_suffix(self, **kwargs):
@@ -171,7 +186,7 @@ class APIView(View):
"""
Instantiates and returns the list of renderers that this view can use.
"""
- return [renderer(self) for renderer in self.renderer_classes]
+ return [renderer() for renderer in self.renderer_classes]
def get_parsers(self):
"""
@@ -269,6 +284,7 @@ class APIView(View):
response.accepted_renderer = request.accepted_renderer
response.accepted_media_type = request.accepted_media_type
+ response.renderer_context = self.get_renderer_context()
for key, value in self.headers.items():
response[key] = value
@@ -306,7 +322,7 @@ class APIView(View):
self.request = request
self.args = args
self.kwargs = kwargs
- self.headers = self.default_response_headers
+ self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)