Commit 648d2be2 by Tom Christie

Make sure JSON output in Browseable API is nicely indented

parent ccd2b011
...@@ -132,7 +132,7 @@ Renders data into HTML for the Browseable API. This renderer will determine whi ...@@ -132,7 +132,7 @@ Renders data into HTML for the Browseable API. This renderer will determine whi
## Custom renderers ## 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: For example:
...@@ -144,11 +144,26 @@ For example: ...@@ -144,11 +144,26 @@ For example:
media_type = 'text/plain' media_type = 'text/plain'
format = 'txt' format = 'txt'
def render(self, data, media_type): def render(self, data, media_type=None, renderer_context=None):
if isinstance(data, basestring): if isinstance(data, basestring):
return data return data
return smart_unicode(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 # Advanced renderer usage
......
...@@ -82,6 +82,11 @@ The media type that was selected by the content negotiation stage. ...@@ -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. 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/ [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/
[statuscodes]: status-codes.md [statuscodes]: status-codes.md
...@@ -24,17 +24,17 @@ class Response(SimpleTemplateResponse): ...@@ -24,17 +24,17 @@ class Response(SimpleTemplateResponse):
@property @property
def rendered_content(self): def rendered_content(self):
renderer = self.accepted_renderer renderer = getattr(self, 'accepted_renderer', None)
media_type = self.accepted_media_type media_type = getattr(self, 'accepted_media_type', None)
context = getattr(self, 'renderer_context', None)
assert renderer, "No accepted renderer set on Response" assert renderer, ".accepted_renderer not set on Response"
assert media_type, "No accepted media type 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 self['Content-Type'] = media_type
if self.data is None: return renderer.render(self.data, media_type, context)
return renderer.render()
return renderer.render(self.data, media_type)
@property @property
def status_text(self): def status_text(self):
...@@ -42,4 +42,6 @@ class Response(SimpleTemplateResponse): ...@@ -42,4 +42,6 @@ class Response(SimpleTemplateResponse):
Returns reason text corresponding to our HTTP response status code. Returns reason text corresponding to our HTTP response status code.
Provided for convenience. 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, '') return STATUS_CODE_TEXT.get(self.status_code, '')
...@@ -41,16 +41,16 @@ class RendererA(BaseRenderer): ...@@ -41,16 +41,16 @@ class RendererA(BaseRenderer):
media_type = 'mock/renderera' media_type = 'mock/renderera'
format = "formata" format = "formata"
def render(self, obj=None, media_type=None): def render(self, data, media_type=None, renderer_context=None):
return RENDERER_A_SERIALIZER(obj) return RENDERER_A_SERIALIZER(data)
class RendererB(BaseRenderer): class RendererB(BaseRenderer):
media_type = 'mock/rendererb' media_type = 'mock/rendererb'
format = "formatb" format = "formatb"
def render(self, obj=None, media_type=None): def render(self, data, media_type=None, renderer_context=None):
return RENDERER_B_SERIALIZER(obj) return RENDERER_B_SERIALIZER(data)
class MockView(APIView): class MockView(APIView):
...@@ -235,7 +235,7 @@ class JSONRendererTests(TestCase): ...@@ -235,7 +235,7 @@ class JSONRendererTests(TestCase):
Test basic JSON rendering. Test basic JSON rendering.
""" """
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None) renderer = JSONRenderer()
content = renderer.render(obj, 'application/json') content = renderer.render(obj, 'application/json')
# Fix failing test case which depends on version of JSON library. # Fix failing test case which depends on version of JSON library.
self.assertEquals(content, _flat_repr) self.assertEquals(content, _flat_repr)
...@@ -245,7 +245,7 @@ class JSONRendererTests(TestCase): ...@@ -245,7 +245,7 @@ class JSONRendererTests(TestCase):
Test JSON rendering with additional content type arguments supplied. Test JSON rendering with additional content type arguments supplied.
""" """
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer(None) renderer = JSONRenderer()
content = renderer.render(obj, 'application/json; indent=2') content = renderer.render(obj, 'application/json; indent=2')
self.assertEquals(strip_trailing_whitespace(content), _indented_repr) self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
...@@ -302,7 +302,7 @@ if yaml: ...@@ -302,7 +302,7 @@ if yaml:
Test basic YAML rendering. Test basic YAML rendering.
""" """
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer(None) renderer = YAMLRenderer()
content = renderer.render(obj, 'application/yaml') content = renderer.render(obj, 'application/yaml')
self.assertEquals(content, _yaml_repr) self.assertEquals(content, _yaml_repr)
...@@ -313,7 +313,7 @@ if yaml: ...@@ -313,7 +313,7 @@ if yaml:
""" """
obj = {'foo': ['bar', 'baz']} obj = {'foo': ['bar', 'baz']}
renderer = YAMLRenderer(None) renderer = YAMLRenderer()
parser = YAMLParser() parser = YAMLParser()
content = renderer.render(obj, 'application/yaml') content = renderer.render(obj, 'application/yaml')
...@@ -345,7 +345,7 @@ class XMLRendererTestCase(TestCase): ...@@ -345,7 +345,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render({'field': 'astring'}, 'application/xml') content = renderer.render({'field': 'astring'}, 'application/xml')
self.assertXMLContains(content, '<field>astring</field>') self.assertXMLContains(content, '<field>astring</field>')
...@@ -353,7 +353,7 @@ class XMLRendererTestCase(TestCase): ...@@ -353,7 +353,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render({'field': 111}, 'application/xml') content = renderer.render({'field': 111}, 'application/xml')
self.assertXMLContains(content, '<field>111</field>') self.assertXMLContains(content, '<field>111</field>')
...@@ -361,7 +361,7 @@ class XMLRendererTestCase(TestCase): ...@@ -361,7 +361,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render({ content = renderer.render({
'field': datetime.datetime(2011, 12, 25, 12, 45, 00) 'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
}, 'application/xml') }, 'application/xml')
...@@ -371,7 +371,7 @@ class XMLRendererTestCase(TestCase): ...@@ -371,7 +371,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render({'field': 123.4}, 'application/xml') content = renderer.render({'field': 123.4}, 'application/xml')
self.assertXMLContains(content, '<field>123.4</field>') self.assertXMLContains(content, '<field>123.4</field>')
...@@ -379,7 +379,7 @@ class XMLRendererTestCase(TestCase): ...@@ -379,7 +379,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render({'field': Decimal('111.2')}, 'application/xml') content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
self.assertXMLContains(content, '<field>111.2</field>') self.assertXMLContains(content, '<field>111.2</field>')
...@@ -387,7 +387,7 @@ class XMLRendererTestCase(TestCase): ...@@ -387,7 +387,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render({'field': None}, 'application/xml') content = renderer.render({'field': None}, 'application/xml')
self.assertXMLContains(content, '<field></field>') self.assertXMLContains(content, '<field></field>')
...@@ -395,7 +395,7 @@ class XMLRendererTestCase(TestCase): ...@@ -395,7 +395,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = renderer.render(self._complex_data, 'application/xml') content = renderer.render(self._complex_data, 'application/xml')
self.assertXMLContains(content, '<sub_name>first</sub_name>') self.assertXMLContains(content, '<sub_name>first</sub_name>')
self.assertXMLContains(content, '<sub_name>second</sub_name>') self.assertXMLContains(content, '<sub_name>second</sub_name>')
...@@ -404,7 +404,7 @@ class XMLRendererTestCase(TestCase): ...@@ -404,7 +404,7 @@ class XMLRendererTestCase(TestCase):
""" """
Test XML rendering. Test XML rendering.
""" """
renderer = XMLRenderer(None) renderer = XMLRenderer()
content = StringIO(renderer.render(self._complex_data, 'application/xml')) content = StringIO(renderer.render(self._complex_data, 'application/xml'))
parser = XMLParser() parser = XMLParser()
......
...@@ -33,16 +33,16 @@ class RendererA(BaseRenderer): ...@@ -33,16 +33,16 @@ class RendererA(BaseRenderer):
media_type = 'mock/renderera' media_type = 'mock/renderera'
format = "formata" format = "formata"
def render(self, obj=None, media_type=None): def render(self, data, media_type=None, renderer_context=None):
return RENDERER_A_SERIALIZER(obj) return RENDERER_A_SERIALIZER(data)
class RendererB(BaseRenderer): class RendererB(BaseRenderer):
media_type = 'mock/rendererb' media_type = 'mock/rendererb'
format = "formatb" format = "formatb"
def render(self, obj=None, media_type=None): def render(self, data, media_type=None, renderer_context=None):
return RENDERER_B_SERIALIZER(obj) return RENDERER_B_SERIALIZER(data)
class MockView(APIView): class MockView(APIView):
......
...@@ -86,6 +86,7 @@ class APIView(View): ...@@ -86,6 +86,7 @@ class APIView(View):
@property @property
def default_response_headers(self): def default_response_headers(self):
# TODO: deprecate?
# TODO: Only vary by accept if multiple renderers # TODO: Only vary by accept if multiple renderers
return { return {
'Allow': ', '.join(self.allowed_methods), 'Allow': ', '.join(self.allowed_methods),
...@@ -158,6 +159,20 @@ class APIView(View): ...@@ -158,6 +159,20 @@ class APIView(View):
""" """
raise exceptions.Throttled(wait) 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 # API policy instantiation methods
def get_format_suffix(self, **kwargs): def get_format_suffix(self, **kwargs):
...@@ -171,7 +186,7 @@ class APIView(View): ...@@ -171,7 +186,7 @@ class APIView(View):
""" """
Instantiates and returns the list of renderers that this view can use. 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): def get_parsers(self):
""" """
...@@ -269,6 +284,7 @@ class APIView(View): ...@@ -269,6 +284,7 @@ class APIView(View):
response.accepted_renderer = request.accepted_renderer response.accepted_renderer = request.accepted_renderer
response.accepted_media_type = request.accepted_media_type response.accepted_media_type = request.accepted_media_type
response.renderer_context = self.get_renderer_context()
for key, value in self.headers.items(): for key, value in self.headers.items():
response[key] = value response[key] = value
...@@ -306,7 +322,7 @@ class APIView(View): ...@@ -306,7 +322,7 @@ class APIView(View):
self.request = request self.request = request
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.headers = self.default_response_headers self.headers = self.default_response_headers # deprecate?
try: try:
self.initial(request, *args, **kwargs) self.initial(request, *args, **kwargs)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment