From d07dc77e91c1f99b47915b3cef30b565f2618e82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 10:23:47 +0100 Subject: Accepted media type uses most specific of client/renderer media types. --- rest_framework/negotiation.py | 16 +++++++--- rest_framework/renderers.py | 19 +----------- rest_framework/response.py | 23 ++++++--------- rest_framework/tests/decorators.py | 2 +- rest_framework/tests/negotiation.py | 58 +++++++++++++++++++++++++++++++++++++ rest_framework/views.py | 2 +- 6 files changed, 82 insertions(+), 38 deletions(-) create mode 100644 rest_framework/tests/negotiation.py (limited to 'rest_framework') 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..6a95815a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -15,7 +15,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, 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,23 +32,6 @@ 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. diff --git a/rest_framework/response.py b/rest_framework/response.py index db6bf3e2..fca631c3 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -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_renderer = renderer self.accepted_media_type = accepted_media_type @property def rendered_content(self): - assert self.renderer, "No renderer set on Response" + renderer = self.accepted_renderer + + assert renderer, "No renderer set on Response" - self['Content-Type'] = self.renderer.media_type + self['content-type'] = self.accepted_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, self.accepted_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/negotiation.py b/rest_framework/tests/negotiation.py new file mode 100644 index 00000000..dd9f6a76 --- /dev/null +++ b/rest_framework/tests/negotiation.py @@ -0,0 +1,58 @@ +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework.decorators import api_view, renderer_classes +from rest_framework.negotiation import DefaultContentNegotiation +from rest_framework.response import Response + +factory = RequestFactory() + + +class MockJSONRenderer(object): + media_type = 'application/json' + + def __init__(self, view): + pass + + +class MockHTMLRenderer(object): + media_type = 'text/html' + + def __init__(self, view): + pass + + +@api_view(('GET',)) +@renderer_classes((MockJSONRenderer, MockHTMLRenderer)) +def example(request): + return Response() + + +class TestAcceptedMediaType(TestCase): + def setUp(self): + self.renderers = [MockJSONRenderer(None), MockHTMLRenderer(None)] + 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') + + +class IntegrationTests(TestCase): + def test_accepted_negotiation_set_on_request(self): + request = factory.get('/', HTTP_ACCEPT='*/*') + response = example(request) + self.assertEquals(response.accepted_media_type, 'application/json') diff --git a/rest_framework/views.py b/rest_framework/views.py index 2bbdbe17..4dd0d208 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -211,7 +211,7 @@ class APIView(View): 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_renderer = self.renderer response.accepted_media_type = self.accepted_media_type for key, value in self.headers.items(): -- cgit v1.2.3 From ed281be3fb3f49ffee69f08aeb95f116528dc833 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 10:33:44 +0100 Subject: User .accepted_renderer, .accepted_media_type --- rest_framework/tests/negotiation.py | 23 +---------------------- rest_framework/views.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 27 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index dd9f6a76..d8265b43 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -1,8 +1,6 @@ from django.test import TestCase from django.test.client import RequestFactory -from rest_framework.decorators import api_view, renderer_classes from rest_framework.negotiation import DefaultContentNegotiation -from rest_framework.response import Response factory = RequestFactory() @@ -10,26 +8,14 @@ factory = RequestFactory() class MockJSONRenderer(object): media_type = 'application/json' - def __init__(self, view): - pass - class MockHTMLRenderer(object): media_type = 'text/html' - def __init__(self, view): - pass - - -@api_view(('GET',)) -@renderer_classes((MockJSONRenderer, MockHTMLRenderer)) -def example(request): - return Response() - class TestAcceptedMediaType(TestCase): def setUp(self): - self.renderers = [MockJSONRenderer(None), MockHTMLRenderer(None)] + self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] self.negotiator = DefaultContentNegotiation() def negotiate(self, request): @@ -49,10 +35,3 @@ class TestAcceptedMediaType(TestCase): 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') - - -class IntegrationTests(TestCase): - def test_accepted_negotiation_set_on_request(self): - request = factory.get('/', HTTP_ACCEPT='*/*') - response = example(request) - self.assertEquals(response.accepted_media_type, 'application/json') diff --git a/rest_framework/views.py b/rest_framework/views.py index 4dd0d208..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.accepted_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 -- cgit v1.2.3 From 4af7fb96f7d726d56077835d7a7a6d5ad0ff0e99 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 11:12:52 +0100 Subject: Tidy up renderers slightly --- rest_framework/renderers.py | 65 ++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 39 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6a95815a..3227a03a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -32,22 +32,8 @@ class BaseRenderer(object): def __init__(self, view=None): self.view = view - 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): @@ -59,16 +45,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) @@ -76,7 +62,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) @@ -98,7 +84,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. @@ -106,7 +92,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) @@ -118,13 +104,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): @@ -135,17 +121,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. @@ -154,18 +140,19 @@ class TemplateRenderer(BaseRenderer): the :attr:`media_type` and :attr:`template` attributes. """ - media_type = None + media_type = 'text/html' + format = 'html' template = 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. """ - if obj is None: + if data is None: return '' template = loader.get_template(self.template) - context = RequestContext(self.view.request, {'object': obj}) + context = RequestContext(self.view.request, {'object': data}) return template.render(context) @@ -174,10 +161,10 @@ 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. @@ -191,8 +178,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]' @@ -316,7 +303,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. @@ -327,7 +314,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) -- cgit v1.2.3 From 26c7d6df6c0a12a2e19e07951b93de80bbfdf91c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 12:13:44 +0100 Subject: HTMLTemplateRenderer working --- rest_framework/renderers.py | 35 ++++++++++++++++++++----- rest_framework/response.py | 16 ++++++------ rest_framework/tests/htmlrenderer.py | 50 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 rest_framework/tests/htmlrenderer.py (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3227a03a..4157468f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -10,6 +10,7 @@ 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 @@ -142,19 +143,41 @@ class HTMLTemplateRenderer(BaseRenderer): media_type = 'text/html' format = 'html' - template = None + template_name = 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 data is None: - return '' + view = self.view + request, response = view.request, view.response - template = loader.get_template(self.template) - context = RequestContext(self.view.request, {'object': data}) + 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): """ diff --git a/rest_framework/response.py b/rest_framework/response.py index fca631c3..796750fc 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=None, + template_name=None, headers=None): """ Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. @@ -20,21 +20,21 @@ class Response(SimpleTemplateResponse): super(Response, self).__init__(None, status=status) self.data = data self.headers = headers and headers[:] or [] - - self.accepted_renderer = renderer - self.accepted_media_type = accepted_media_type + self.template_name = template_name @property def rendered_content(self): renderer = self.accepted_renderer + media_type = self.accepted_media_type - assert renderer, "No renderer set on Response" + assert renderer, "No accepted renderer set on Response" + assert media_type, "No accepted media type set on Response" - self['content-type'] = self.accepted_media_type + self['content-type'] = media_type if self.data is None: return renderer.render() - return renderer.render(self.data, self.accepted_media_type) + return renderer.render(self.data, media_type) @property def status_text(self): diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py new file mode 100644 index 00000000..2c672dd0 --- /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') -- cgit v1.2.3 From 2575ea92aad3608142cfdd3ede5ee1b53e2064ba Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 13:04:34 +0100 Subject: Docs for template responses --- rest_framework/response.py | 4 ++-- rest_framework/tests/htmlrenderer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/response.py b/rest_framework/response.py index 796750fc..9a633a8a 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -8,7 +8,7 @@ class Response(SimpleTemplateResponse): arbitrary media types. """ - def __init__(self, data=None, status=None, + def __init__(self, data=None, status=200, template_name=None, headers=None): """ Alters the init arguments slightly. @@ -30,7 +30,7 @@ class Response(SimpleTemplateResponse): assert renderer, "No accepted renderer set on Response" assert media_type, "No accepted media type set on Response" - self['content-type'] = media_type + self['Content-Type'] = media_type if self.data is None: return renderer.render() diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 2c672dd0..7a7f2743 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -47,4 +47,4 @@ class HTMLRendererTests(TestCase): def test_simple_html_view(self): response = self.client.get('/') self.assertContains(response, "example: foobar") - self.assertEquals(response['content-type'], 'text/html') + self.assertEquals(response['Content-Type'], 'text/html') -- cgit v1.2.3