diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 668c4e5c4..4d205c0e8 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -58,17 +58,11 @@ class DefaultContentNegotiation(BaseContentNegotiation): _MediaType(media_type).precedence): # Eg client requests '*/*' # Accepted media type is 'application/json' - renderer_and_media_type = renderer, renderer.media_type + return renderer, renderer.media_type else: # Eg client requests 'application/json; indent=8' # Accepted media type is 'application/json; indent=8' - renderer_and_media_type = renderer, media_type - if renderer.charset: - charset = renderer.charset - else: - charset = self.__class__.settings.DEFAULT_CHARSET - retval = renderer_and_media_type + (charset,) - return retval + return renderer, media_type raise exceptions.NotAcceptable(available_renderers=renderers) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 65d8b6864..b91e3861f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -41,6 +41,7 @@ class BaseRenderer(object): def render(self, data, accepted_media_type=None, renderer_context=None): raise NotImplemented('Renderer class requires .render() to be implemented') + class JSONRenderer(BaseRenderer): """ Renderer which serializes to json. @@ -115,6 +116,7 @@ class XMLRenderer(BaseRenderer): media_type = 'application/xml' format = 'xml' + charset = 'utf-8' def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -164,6 +166,7 @@ class YAMLRenderer(BaseRenderer): media_type = 'application/yaml' format = 'yaml' encoder = encoders.SafeDumper + charset = 'utf-8' def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -204,6 +207,7 @@ class TemplateHTMLRenderer(BaseRenderer): '%(status_code)s.html', 'api_exception.html' ] + charset = 'utf-8' def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -275,6 +279,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer): """ media_type = 'text/html' format = 'html' + charset = 'utf-8' def render(self, data, accepted_media_type=None, renderer_context=None): renderer_context = renderer_context or {} @@ -296,6 +301,7 @@ class BrowsableAPIRenderer(BaseRenderer): media_type = 'text/html' format = 'api' template = 'rest_framework/api.html' + charset = 'utf-8' def get_default_renderer(self, view): """ diff --git a/rest_framework/response.py b/rest_framework/response.py index 40372f221..32e74a45e 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse): def __init__(self, data=None, status=200, template_name=None, headers=None, - exception=False): + exception=False, charset=None): """ Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. @@ -30,6 +30,7 @@ class Response(SimpleTemplateResponse): self.data = data self.template_name = template_name self.exception = exception + self.charset = charset if headers: for name, value in six.iteritems(headers): @@ -39,18 +40,21 @@ class Response(SimpleTemplateResponse): def rendered_content(self): renderer = getattr(self, 'accepted_renderer', None) media_type = getattr(self, 'accepted_media_type', None) - charset = getattr(self, 'charset', None) context = getattr(self, 'renderer_context', None) 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 - if charset is not None: - ct = "{0}; charset={1}".format(media_type, charset) + + if self.charset is None: + self.charset = renderer.charset + + if self.charset is not None: + content_type = "{0}; charset={1}".format(media_type, self.charset) else: - ct = media_type - self['Content-Type'] = ct + content_type = media_type + self['Content-Type'] = content_type return renderer.render(self.data, media_type, context) @property @@ -71,4 +75,4 @@ class Response(SimpleTemplateResponse): for key in ('accepted_renderer', 'renderer_context', 'data'): if key in state: del state[key] - return state \ No newline at end of file + return state diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 255a95e21..beb511aca 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -83,8 +83,6 @@ DEFAULTS = { 'FORMAT_SUFFIX_KWARG': 'format', # Input and output formats - 'DEFAULT_CHARSET': None, - 'DATE_INPUT_FORMATS': ( ISO_8601, ), diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 8f2e2b5a0..5d18a6e83 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -66,19 +66,19 @@ class TemplateHTMLRendererTests(TestCase): def test_simple_html_view(self): response = self.client.get('/') self.assertContains(response, "example: foobar") - self.assertEqual(response['Content-Type'], 'text/html') + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_not_found_html_view(self): response = self.client.get('/not_found') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.content, six.b("404 Not Found")) - self.assertEqual(response['Content-Type'], 'text/html') + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_permission_denied_html_view(self): response = self.client.get('/permission_denied') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.content, six.b("403 Forbidden")) - self.assertEqual(response['Content-Type'], 'text/html') + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') class TemplateHTMLRendererExceptionTests(TestCase): @@ -109,10 +109,10 @@ class TemplateHTMLRendererExceptionTests(TestCase): response = self.client.get('/not_found') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.content, six.b("404: Not found")) - self.assertEqual(response['Content-Type'], 'text/html') + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_permission_denied_html_view_with_template(self): response = self.client.get('/permission_denied') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.content, six.b("403: Permission denied")) - self.assertEqual(response['Content-Type'], 'text/html') + self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index d7ef6470a..7f84827f0 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -12,15 +12,14 @@ factory = RequestFactory() class MockJSONRenderer(BaseRenderer): media_type = 'application/json' + class MockHTMLRenderer(BaseRenderer): media_type = 'text/html' + class NoCharsetSpecifiedRenderer(BaseRenderer): media_type = 'my/media' -class CharsetSpecifiedRenderer(BaseRenderer): - media_type = 'my/media' - charset = 'mycharset' class TestAcceptedMediaType(TestCase): def setUp(self): @@ -32,32 +31,15 @@ class TestAcceptedMediaType(TestCase): def test_client_without_accept_use_renderer(self): request = Request(factory.get('/')) - accepted_renderer, accepted_media_type, charset = self.select_renderer(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEqual(accepted_media_type, 'application/json') def test_client_underspecifies_accept_use_renderer(self): request = Request(factory.get('/', HTTP_ACCEPT='*/*')) - accepted_renderer, accepted_media_type, charset = self.select_renderer(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEqual(accepted_media_type, 'application/json') def test_client_overspecifies_accept_use_client(self): request = Request(factory.get('/', HTTP_ACCEPT='application/json; indent=8')) - accepted_renderer, accepted_media_type, charset = self.select_renderer(request) + accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEqual(accepted_media_type, 'application/json; indent=8') - -class TestCharset(TestCase): - def setUp(self): - self.renderers = [NoCharsetSpecifiedRenderer()] - self.negotiator = DefaultContentNegotiation() - - def test_returns_none_if_no_charset_set(self): - request = Request(factory.get('/')) - renderers = [NoCharsetSpecifiedRenderer()] - _, _, charset = self.negotiator.select_renderer(request, renderers) - self.assertIsNone(charset) - - def test_returns_attribute_from_renderer_if_charset_is_set(self): - request = Request(factory.get('/')) - renderers = [CharsetSpecifiedRenderer()] - _, _, charset = self.negotiator.select_renderer(request, renderers) - self.assertEquals(CharsetSpecifiedRenderer.charset, charset) \ No newline at end of file diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index f2a1c635f..8f1163e87 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -12,6 +12,7 @@ from rest_framework.renderers import ( from rest_framework.settings import api_settings from rest_framework.compat import six + class MockPickleRenderer(BaseRenderer): media_type = 'application/pickle' @@ -19,6 +20,7 @@ class MockPickleRenderer(BaseRenderer): class MockJsonRenderer(BaseRenderer): media_type = 'application/json' + class MockTextMediaRenderer(BaseRenderer): media_type = 'text/html' @@ -44,6 +46,7 @@ class RendererB(BaseRenderer): def render(self, data, media_type=None, renderer_context=None): return RENDERER_B_SERIALIZER(data) + class RendererC(RendererB): media_type = 'mock/rendererc' format = 'formatc' @@ -56,6 +59,14 @@ class MockView(APIView): def get(self, request, **kwargs): return Response(DUMMYCONTENT, status=DUMMYSTATUS) + +class MockViewSettingCharset(APIView): + renderer_classes = (RendererA, RendererB, RendererC) + + def get(self, request, **kwargs): + return Response(DUMMYCONTENT, status=DUMMYSTATUS, charset='setbyview') + + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -70,6 +81,7 @@ class HTMLView1(APIView): return Response('text') urlpatterns = patterns('', + url(r'^setbyview$', MockViewSettingCharset.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), url(r'^html$', HTMLView.as_view()), @@ -178,43 +190,37 @@ class Issue122Tests(TestCase): """ self.client.get('/html1') + class Issue807Testts(TestCase): """ Covers #807 """ - + urls = 'rest_framework.tests.response' - + def test_does_not_append_charset_by_default(self): """ - For backwards compatibility `REST_FRAMEWORK['DEFAULT_CHARSET']` defaults - to None, so that all legacy code works as expected. + Renderers don't include a charset unless set explicitly. """ headers = {"HTTP_ACCEPT": RendererA.media_type} resp = self.client.get('/', **headers) - self.assertEquals(RendererA.media_type, resp['Content-Type']) - + self.assertEqual(RendererA.media_type, resp['Content-Type']) + def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self): """ If renderer class has charset attribute declared, it gets appended to Response's Content-Type """ - resp = self.client.get('/?format=%s' % RendererC.format) - expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset) - self.assertEquals(expected, resp['Content-Type']) - - def test_if_there_is_default_charset_specified_it_gets_appended(self): - """ - If user defines `REST_FRAMEWORK['DEFAULT_CHARSET']` it will get appended - to Content-Type of all responses. - """ - original_default_charset = api_settings.DEFAULT_CHARSET - api_settings.DEFAULT_CHARSET = "utf-8" - headers = {'HTTP_ACCEPT': RendererA.media_type} + headers = {"HTTP_ACCEPT": RendererC.media_type} resp = self.client.get('/', **headers) - expected = "{0}; charset={1}".format( - RendererA.media_type, - api_settings.DEFAULT_CHARSET - ) - self.assertEquals(expected, resp['Content-Type']) - api_settings.DEFAULT_CHARSET = original_default_charset \ No newline at end of file + expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset) + self.assertEqual(expected, resp['Content-Type']) + + def test_charset_set_explictly_on_response(self): + """ + The charset may be set explictly on the response. + """ + headers = {"HTTP_ACCEPT": RendererC.media_type} + resp = self.client.get('/setbyview', **headers) + expected = "{0}; charset={1}".format(RendererC.media_type, 'setbyview') + self.assertEqual(expected, resp['Content-Type']) diff --git a/rest_framework/views.py b/rest_framework/views.py index 035aa6466..555fa2f40 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -183,9 +183,7 @@ class APIView(View): return conneg.select_renderer(request, renderers, self.format_kwarg) except Exception: if force: - charset = renderers[0].charset - charset = charset if charset is not None else api_settings.DEFAULT_CHARSET - return (renderers[0], renderers[0].media_type, renderers[0].charset) + return (renderers[0], renderers[0].media_type) raise def perform_authentication(self, request): @@ -252,10 +250,7 @@ class APIView(View): # Perform content negotiation and store the accepted info on the request neg = self.perform_content_negotiation(request) - renderer, media_type, charset = neg - request.accepted_renderer = renderer - request.accepted_media_type = media_type - request.accepted_charset = charset + request.accepted_renderer, request.accepted_media_type = neg def finalize_response(self, request, response, *args, **kwargs): """ @@ -270,16 +265,11 @@ class APIView(View): if isinstance(response, Response): if not getattr(request, 'accepted_renderer', None): neg = self.perform_content_negotiation(request, force=True) - renderer, media_type, charset = neg - request.accepted_renderer = renderer - request.accepted_media_type = media_type + request.accepted_renderer, request.accepted_media_type = neg response.accepted_renderer = request.accepted_renderer response.accepted_media_type = request.accepted_media_type response.renderer_context = self.get_renderer_context() - charset = request.accepted_renderer.charset - charset = charset if charset else api_settings.DEFAULT_CHARSET - response.charset = charset for key, value in self.headers.items(): response[key] = value