diff options
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/negotiation.py | 16 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 115 | ||||
| -rw-r--r-- | rest_framework/response.py | 29 | ||||
| -rw-r--r-- | rest_framework/tests/decorators.py | 2 | ||||
| -rw-r--r-- | rest_framework/tests/htmlrenderer.py | 50 | ||||
| -rw-r--r-- | rest_framework/tests/negotiation.py | 37 | ||||
| -rw-r--r-- | rest_framework/views.py | 16 |
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 |
