From 0081d744b9f530b2418d1e82d7ad94a2ebc31c9c Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Sat, 23 Mar 2013 14:18:11 +0100 Subject: [PATCH 01/20] Added tests for issue 747 in serializer.py --- rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 05217f35a..0386ca76e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1082,3 +1082,32 @@ class DeserializeListTestCase(TestCase): self.assertFalse(serializer.is_valid()) expected = [{}, {'email': ['This field is required.']}, {}] self.assertEqual(serializer.errors, expected) + + +# test for issue 747 + +class LazyStringModel(object): + def __init__(self, lazystring): + self.lazystring = lazystring + + +class LazyStringSerializer(serializers.Serializer): + lazystring = serializers.Field() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.lazystring = attrs.get('lazystring', instance.lazystring) + return instance + return Comment(**attrs) + + +class LazyStringsTestCase(TestCase): + + def setUp(self): + from django.utils.translation import ugettext_lazy as _ + + self.model = LazyStringModel(lazystring=_("lazystring")) + + def test_lazy_strings_are_translated(self): + serializer = LazyStringSerializer(self.model) + self.assertEqual(type(serializer.data['lazystring']), type("lazystring")) From b5640bb77843c50f42a649982b9b9592113c6f59 Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Sat, 23 Mar 2013 14:18:55 +0100 Subject: [PATCH 02/20] Forcing translations of lazy translatable strings in Field to_native method --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f3496b53e..09f076ab7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -18,7 +18,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time from rest_framework.compat import BytesIO from rest_framework.compat import six -from rest_framework.compat import smart_text +from rest_framework.compat import smart_text, force_text from rest_framework.settings import api_settings @@ -165,7 +165,7 @@ class Field(object): return [self.to_native(item) for item in value] elif isinstance(value, dict): return dict(map(self.to_native, (k, v)) for k, v in value.items()) - return smart_text(value) + return force_text(value) def attributes(self): """ From ef383d969c440a20fdf25748de590e07cb1abfda Mon Sep 17 00:00:00 2001 From: Ryan Kaskel Date: Sat, 18 May 2013 14:31:29 +0100 Subject: [PATCH 03/20] Clean up test case. --- rest_framework/tests/serializer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index d978dc872..e999b624b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH -from django.utils.datastructures import MultiValueDict from django.test import TestCase +from django.utils.datastructures import MultiValueDict +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, @@ -1246,6 +1247,7 @@ class DeserializeListTestCase(TestCase): # test for issue 747 + class LazyStringModel(object): def __init__(self, lazystring): self.lazystring = lazystring @@ -1258,16 +1260,14 @@ class LazyStringSerializer(serializers.Serializer): if instance is not None: instance.lazystring = attrs.get('lazystring', instance.lazystring) return instance - return Comment(**attrs) + return LazyStringModel(**attrs) class LazyStringsTestCase(TestCase): - def setUp(self): - from django.utils.translation import ugettext_lazy as _ - - self.model = LazyStringModel(lazystring=_("lazystring")) + self.model = LazyStringModel(lazystring=_('lazystring')) def test_lazy_strings_are_translated(self): serializer = LazyStringSerializer(self.model) - self.assertEqual(type(serializer.data['lazystring']), type("lazystring")) + self.assertEqual(type(serializer.data['lazystring']), + type('lazystring')) From ebe959b52a10a88975b15c69275b0ef5c50cb9fa Mon Sep 17 00:00:00 2001 From: Karol Majta Date: Sat, 18 May 2013 16:45:05 +0200 Subject: [PATCH 04/20] charset param gets now appended to response's Content-Type. Closes #807 --- rest_framework/negotiation.py | 10 ++++- rest_framework/renderers.py | 2 +- rest_framework/response.py | 10 +++-- rest_framework/settings.py | 2 + rest_framework/tests/negotiation.py | 35 +++++++++++++++--- rest_framework/tests/response.py | 57 ++++++++++++++++++++++++++--- rest_framework/views.py | 16 ++++++-- 7 files changed, 111 insertions(+), 21 deletions(-) diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 4d205c0e8..668c4e5c4 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -58,11 +58,17 @@ class DefaultContentNegotiation(BaseContentNegotiation): _MediaType(media_type).precedence): # Eg client requests '*/*' # Accepted media type is 'application/json' - return renderer, renderer.media_type + renderer_and_media_type = renderer, renderer.media_type else: # Eg client requests 'application/json; indent=8' # Accepted media type is 'application/json; indent=8' - return renderer, media_type + 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 raise exceptions.NotAcceptable(available_renderers=renderers) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8361cd409..65d8b6864 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -36,11 +36,11 @@ class BaseRenderer(object): media_type = None format = None + charset = None 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. diff --git a/rest_framework/response.py b/rest_framework/response.py index 26e4ab371..40372f221 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -39,14 +39,18 @@ 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 - - self['Content-Type'] = media_type + if charset is not None: + ct = "{0}; charset={1}".format(media_type, charset) + else: + ct = media_type + self['Content-Type'] = ct return renderer.render(self.data, media_type, context) @property @@ -67,4 +71,4 @@ class Response(SimpleTemplateResponse): for key in ('accepted_renderer', 'renderer_context', 'data'): if key in state: del state[key] - return state + return state \ No newline at end of file diff --git a/rest_framework/settings.py b/rest_framework/settings.py index beb511aca..255a95e21 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -83,6 +83,8 @@ DEFAULTS = { 'FORMAT_SUFFIX_KWARG': 'format', # Input and output formats + 'DEFAULT_CHARSET': None, + 'DATE_INPUT_FORMATS': ( ISO_8601, ), diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py index 43721b842..d7ef6470a 100644 --- a/rest_framework/tests/negotiation.py +++ b/rest_framework/tests/negotiation.py @@ -3,18 +3,24 @@ from django.test import TestCase from django.test.client import RequestFactory from rest_framework.negotiation import DefaultContentNegotiation from rest_framework.request import Request +from rest_framework.renderers import BaseRenderer factory = RequestFactory() -class MockJSONRenderer(object): +class MockJSONRenderer(BaseRenderer): media_type = 'application/json' - -class MockHTMLRenderer(object): +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): @@ -26,15 +32,32 @@ class TestAcceptedMediaType(TestCase): def test_client_without_accept_use_renderer(self): request = Request(factory.get('/')) - accepted_renderer, accepted_media_type = self.select_renderer(request) + accepted_renderer, accepted_media_type, charset = 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 = self.select_renderer(request) + accepted_renderer, accepted_media_type, charset = 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 = self.select_renderer(request) + accepted_renderer, accepted_media_type, charset = 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 aecf83f4e..f2a1c635f 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -12,7 +12,6 @@ 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' @@ -20,6 +19,8 @@ class MockPickleRenderer(BaseRenderer): class MockJsonRenderer(BaseRenderer): media_type = 'application/json' +class MockTextMediaRenderer(BaseRenderer): + media_type = 'text/html' DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' @@ -43,14 +44,18 @@ 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' + charset = "rendererc" + class MockView(APIView): - renderer_classes = (RendererA, RendererB) + renderer_classes = (RendererA, RendererB, RendererC) def get(self, request, **kwargs): return Response(DUMMYCONTENT, status=DUMMYSTATUS) - class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -64,10 +69,9 @@ class HTMLView1(APIView): def get(self, request, **kwargs): return Response('text') - urlpatterns = patterns('', - url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), - url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + 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()), url(r'^html1$', HTMLView1.as_view()), url(r'^restframework', include('rest_framework.urls', namespace='rest_framework')) @@ -173,3 +177,44 @@ class Issue122Tests(TestCase): Test if no infinite recursion occurs. """ 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. + """ + headers = {"HTTP_ACCEPT": RendererA.media_type} + resp = self.client.get('/', **headers) + self.assertEquals(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} + 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 diff --git a/rest_framework/views.py b/rest_framework/views.py index 555fa2f40..035aa6466 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -183,7 +183,9 @@ class APIView(View): return conneg.select_renderer(request, renderers, self.format_kwarg) except Exception: if force: - return (renderers[0], renderers[0].media_type) + 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) raise def perform_authentication(self, request): @@ -250,7 +252,10 @@ class APIView(View): # 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 + renderer, media_type, charset = neg + request.accepted_renderer = renderer + request.accepted_media_type = media_type + request.accepted_charset = charset def finalize_response(self, request, response, *args, **kwargs): """ @@ -265,11 +270,16 @@ class APIView(View): if isinstance(response, Response): if not getattr(request, 'accepted_renderer', None): neg = self.perform_content_negotiation(request, force=True) - request.accepted_renderer, request.accepted_media_type = neg + renderer, media_type, charset = neg + request.accepted_renderer = renderer + request.accepted_media_type = media_type 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 From 0c81d04170da19e5bfb332641d92d9c3346598f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Sat, 18 May 2013 17:04:20 +0200 Subject: [PATCH 05/20] Add min_value=0 to autogenerated Pos..IntFields --- rest_framework/serializers.py | 4 ++ rest_framework/tests/serializer.py | 73 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ff5eb8732..d0313513b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -739,6 +739,10 @@ class ModelSerializer(Serializer): if issubclass(model_field.__class__, models.TextField): kwargs['widget'] = widgets.Textarea + if issubclass(model_field.__class__, models.PositiveIntegerField) or\ + issubclass(model_field.__class__, models.PositiveSmallIntegerField): + kwargs['min_value'] = 0 + # TODO: TypedChoiceField? if model_field.flatchoices: # This ModelField contains choices kwargs['choices'] = model_field.flatchoices diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 4f188c3e4..220a581ab 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1402,3 +1402,76 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def test_url_field(self): self.field_test('url_field') + + +class DefaultValuesOnAutogeneratedFieldsTests(TestCase): + + def setUp(self): + class DVOAFModel(RESTFrameworkModel): + positive_integer_field = models.PositiveIntegerField(blank=True) + positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) + email_field = models.EmailField(blank=True) + file_field = models.FileField(blank=True) + image_field = models.ImageField(blank=True) + slug_field = models.SlugField(blank=True) + url_field = models.URLField(blank=True) + + class DVOAFSerializer(serializers.ModelSerializer): + class Meta: + model = DVOAFModel + + self.serializer_class = DVOAFSerializer + self.fields_attributes = { + 'positive_integer_field': [ + ('min_value', 0), + ], + 'positive_small_integer_field': [ + ('min_value', 0), + ], + 'email_field': [ + ('max_length', 75), + ], + 'file_field': [ + ('max_length', 100), + ], + 'image_field': [ + ('max_length', 100), + ], + 'slug_field': [ + ('max_length', 50), + ], + 'url_field': [ + ('max_length', 200), + ], + } + + def field_test(self, field): + serializer = self.serializer_class(data={}) + self.assertEqual(serializer.is_valid(), True) + + for attribute in self.fields_attributes[field]: + self.assertEqual( + getattr(serializer.fields[field], attribute[0]), + attribute[1] + ) + + def test_positive_integer_field(self): + self.field_test('positive_integer_field') + + def test_positive_small_integer_field(self): + self.field_test('positive_small_integer_field') + + def test_email_field(self): + self.field_test('email_field') + + def test_file_field(self): + self.field_test('file_field') + + def test_image_field(self): + self.field_test('image_field') + + def test_slug_field(self): + self.field_test('slug_field') + + def test_url_field(self): + self.field_test('url_field') From c69f960066dfdef2163a53205f27c2b9e457068f Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Sat, 18 May 2013 16:30:40 +0200 Subject: [PATCH 06/20] Adding a class attribute into JSONRenderer for ensuring ascii, and using it consistently --- rest_framework/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8361cd409..d55a3e0ea 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -49,6 +49,7 @@ class JSONRenderer(BaseRenderer): media_type = 'application/json' format = 'json' encoder_class = encoders.JSONEncoder + ensure_ascii = True def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -72,7 +73,7 @@ class JSONRenderer(BaseRenderer): except (ValueError, TypeError): indent = None - return json.dumps(data, cls=self.encoder_class, indent=indent) + return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii) class JSONPRenderer(JSONRenderer): From 97f034e3d65068f7bd1e982e6fd251222a4feea1 Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Sat, 18 May 2013 16:31:12 +0200 Subject: [PATCH 07/20] Adds UnicodeJSONRenderer which doesn't ensure ascii --- rest_framework/renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index d55a3e0ea..12e1107fa 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -76,6 +76,10 @@ class JSONRenderer(BaseRenderer): return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii) +class UnicodeJSONRenderer(JSONRenderer): + ensure_ascii = False + + class JSONPRenderer(JSONRenderer): """ Renderer which serializes to json, From 6dbbbc16da740fb27f9a5a390fb61400e9f6ff64 Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Sat, 18 May 2013 16:32:53 +0200 Subject: [PATCH 08/20] Better checking if the content can be printable in the BrowsableAPI --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 12e1107fa..4345a313b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -325,7 +325,7 @@ class BrowsableAPIRenderer(BaseRenderer): renderer_context['indent'] = 4 content = renderer.render(data, accepted_media_type, renderer_context) - if not all(char in string.printable for char in content): + if not isinstance(content, six.text_type): return '[%d bytes of binary content]' return content From 71e29644a2950d8a82cb26f6a3e39fb76faf9707 Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Sat, 18 May 2013 16:50:09 +0200 Subject: [PATCH 09/20] Adds new renderer into the documentation --- docs/api-guide/renderers.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b9a9fd7a3..1661ceecf 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -67,7 +67,7 @@ If your API includes views that can serve both regular webpages and API response ## JSONRenderer -Renders the request data into `JSON`. +Renders the request data into `JSON` enforcing ASCII encoding The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`. @@ -75,6 +75,10 @@ The client may additionally include an `'indent'` media type parameter, in which **.format**: `'.json'` +## UnicodeJSONRenderer + +Same as `JSONRenderer` but doesn't enforce ASCII encoding + ## JSONPRenderer Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback. From b9b22976125fffa4552da695183bf75fbaf0b927 Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Sat, 18 May 2013 17:06:25 +0200 Subject: [PATCH 10/20] Tests proper encoding in JSONRenderer and UnicodeJSONRenderer --- rest_framework/tests/renderers.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 40bac9cb3..739f91841 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from decimal import Decimal from django.core.cache import cache from django.test import TestCase @@ -8,7 +9,7 @@ from rest_framework.compat import yaml, etree, patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ - XMLRenderer, JSONPRenderer, BrowsableAPIRenderer + XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.compat import StringIO @@ -254,6 +255,23 @@ class JSONRendererTests(TestCase): content = renderer.render(obj, 'application/json; indent=2') self.assertEqual(strip_trailing_whitespace(content), _indented_repr) + def test_check_ascii(self): + obj = {'countries': ['United Kingdom', 'France', 'España']} + renderer = JSONRenderer() + content = renderer.render(obj, 'application/json') + self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}') + + +class UnicodeJSONRendererTests(TestCase): + """ + Tests specific for the Unicode JSON Renderer + """ + def test_proper_encoding(self): + obj = {'countries': ['United Kingdom', 'France', 'España']} + renderer = UnicodeJSONRenderer() + content = renderer.render(obj, 'application/json') + self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}') + class JSONPRendererTests(TestCase): """ From 025c40e7d5efbba0c3e65ab3737adef988995a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Sat, 18 May 2013 17:16:29 +0200 Subject: [PATCH 11/20] Fix order for applying min_value to fields --- rest_framework/serializers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d0313513b..943fba6ba 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -739,15 +739,16 @@ class ModelSerializer(Serializer): if issubclass(model_field.__class__, models.TextField): kwargs['widget'] = widgets.Textarea - if issubclass(model_field.__class__, models.PositiveIntegerField) or\ - issubclass(model_field.__class__, models.PositiveSmallIntegerField): - kwargs['min_value'] = 0 - # TODO: TypedChoiceField? if model_field.flatchoices: # This ModelField contains choices kwargs['choices'] = model_field.flatchoices return ChoiceField(**kwargs) + # put this below the ChoiceField because min_value isn't a valid initializer + if issubclass(model_field.__class__, models.PositiveIntegerField) or\ + issubclass(model_field.__class__, models.PositiveSmallIntegerField): + kwargs['min_value'] = 0 + attribute_dict = { models.CharField: ['max_length'], models.CommaSeparatedIntegerField: ['max_length'], From 10e451a85a034d1158148f54e98147e81454c2ed Mon Sep 17 00:00:00 2001 From: Ryan Kaskel Date: Sat, 18 May 2013 16:21:18 +0100 Subject: [PATCH 12/20] Handle Python 3 strings and lazy strings. --- rest_framework/fields.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2ab603cfb..c3ac62813 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -19,6 +19,7 @@ from django.db.models.fields import BLANK_CHOICE_DASH from django import forms from django.forms import widgets from django.utils.encoding import is_protected_type +from django.utils.functional import Promise from django.utils.translation import ugettext_lazy as _ from django.utils.datastructures import SortedDict @@ -45,6 +46,15 @@ def is_simple_callable(obj): len_defaults = len(defaults) if defaults else 0 return len_args <= len_defaults +if six.PY3: + def is_non_str_iterable(obj): + if (isinstance(obj, str) or + (isinstance(obj, Promise) and obj._delegate_text)): + return False + return hasattr(obj, '__iter__') +else: + def is_non_str_iterable(obj): + return hasattr(obj, '__iter__') def get_component(obj, attr_name): """ @@ -169,7 +179,8 @@ class Field(object): if is_protected_type(value): return value - elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)): + elif (is_non_str_iterable(value) and + not isinstance(value, (dict, six.string_types))): return [self.to_native(item) for item in value] elif isinstance(value, dict): # Make sure we preserve field ordering, if it exists From 579f77ceaa03a216a7a635c3d3a4d83b0e5868f8 Mon Sep 17 00:00:00 2001 From: Ryan Kaskel Date: Sat, 18 May 2013 17:04:17 +0100 Subject: [PATCH 13/20] Move function to compat. --- rest_framework/compat.py | 13 +++++++++++++ rest_framework/fields.py | 13 +------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index cd39f5445..76dc00526 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -495,3 +495,16 @@ except ImportError: oauth2_provider_forms = None oauth2_provider_scope = None oauth2_constants = None + +# Handle lazy strings +from django.utils.functional import Promise + +if six.PY3: + def is_non_str_iterable(obj): + if (isinstance(obj, str) or + (isinstance(obj, Promise) and obj._delegate_text)): + return False + return hasattr(obj, '__iter__') +else: + def is_non_str_iterable(obj): + return hasattr(obj, '__iter__') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d5cf30e43..b5f99823a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -19,7 +19,6 @@ from django.db.models.fields import BLANK_CHOICE_DASH from django import forms from django.forms import widgets from django.utils.encoding import is_protected_type -from django.utils.functional import Promise from django.utils.translation import ugettext_lazy as _ from django.utils.datastructures import SortedDict @@ -27,7 +26,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time from rest_framework.compat import BytesIO from rest_framework.compat import six -from rest_framework.compat import smart_text, force_text +from rest_framework.compat import smart_text, force_text, is_non_str_iterable from rest_framework.settings import api_settings @@ -46,16 +45,6 @@ def is_simple_callable(obj): len_defaults = len(defaults) if defaults else 0 return len_args <= len_defaults -if six.PY3: - def is_non_str_iterable(obj): - if (isinstance(obj, str) or - (isinstance(obj, Promise) and obj._delegate_text)): - return False - return hasattr(obj, '__iter__') -else: - def is_non_str_iterable(obj): - return hasattr(obj, '__iter__') - def get_component(obj, attr_name): """ Given an object, and an attribute name, From 6af61a19b78b0ebb9d8403e96c6e4a77c675b141 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 18:07:43 +0200 Subject: [PATCH 14/20] Added @matteosuppo for work on #865. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d805c0c1d..0e206de82 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -130,6 +130,7 @@ The following people have helped make REST framework great. * Òscar Vilaplana - [grimborg] * Ryan Kaskel - [ryankask] * Andy McKay - [andymckay] +* Matteo Suppo - [matteosuppo] Many thanks to everyone who's contributed to the project. @@ -296,3 +297,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [grimborg]: https://github.com/grimborg [ryankask]: https://github.com/ryankask [andymckay]: https://github.com/andymckay +[matteosuppo]: https://github.com/matteosuppo From 0d3d66cb0232e1067600ef22fcf88937ac6bee9d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 17:21:43 +0100 Subject: [PATCH 15/20] Added proper charset support --- rest_framework/negotiation.py | 10 ++---- rest_framework/renderers.py | 6 ++++ rest_framework/response.py | 18 ++++++---- rest_framework/settings.py | 2 -- rest_framework/tests/htmlrenderer.py | 10 +++--- rest_framework/tests/negotiation.py | 28 +++------------ rest_framework/tests/response.py | 54 +++++++++++++++------------- rest_framework/views.py | 16 ++------- 8 files changed, 62 insertions(+), 82 deletions(-) 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 From d4721fc1f70050fc275ad16958e4795e0f3aab1c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 17:25:38 +0100 Subject: [PATCH 16/20] Correct charset on UnicodeJSONRenderer --- rest_framework/renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a67c5eeae..9d08c5d48 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -79,6 +79,7 @@ class JSONRenderer(BaseRenderer): class UnicodeJSONRenderer(JSONRenderer): ensure_ascii = False + charset = 'utf-8' class JSONPRenderer(JSONRenderer): From 351814e110d642bf1351b95141924190e56d9694 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 18:32:02 +0200 Subject: [PATCH 17/20] Added @lolek09 for work on #862. Kick ass! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 0e206de82..acd6576ec 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -131,6 +131,7 @@ The following people have helped make REST framework great. * Ryan Kaskel - [ryankask] * Andy McKay - [andymckay] * Matteo Suppo - [matteosuppo] +* Karol Majta - [lolek09] Many thanks to everyone who's contributed to the project. @@ -298,3 +299,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ryankask]: https://github.com/ryankask [andymckay]: https://github.com/andymckay [matteosuppo]: https://github.com/matteosuppo +[lolek09]: https://github.com/lolek09 From 4631b91e144ae758b4417f20b0379ce5b9179ee6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 21:12:44 +0100 Subject: [PATCH 18/20] Fix broken 'binary content' in browseable API --- rest_framework/renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 9d08c5d48..6b508e6d0 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -332,8 +332,8 @@ class BrowsableAPIRenderer(BaseRenderer): renderer_context['indent'] = 4 content = renderer.render(data, accepted_media_type, renderer_context) - if not isinstance(content, six.text_type): - return '[%d bytes of binary content]' + if not all(char in string.printable for char in content): + return '[%d bytes of binary content]' % len(content) return content From 4802177766b92c644d7c6f446d0bdf2dbe1917e6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 21:19:53 +0100 Subject: [PATCH 19/20] Enforce object permissions check when rendering forms in browseable API --- rest_framework/renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6b508e6d0..08df7e91d 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -349,6 +349,7 @@ class BrowsableAPIRenderer(BaseRenderer): try: view.check_permissions(request) + view.check_object_permissions(request, obj) except exceptions.APIException: return False # Doesn't have permissions return True From 7c945b43f05f1b340f78c23f80c8043937c7fd2a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 18 May 2013 23:52:02 +0100 Subject: [PATCH 20/20] Only use object permissions in browsable api form generation if an object exists --- rest_framework/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 08df7e91d..c67c8ed6f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -349,7 +349,8 @@ class BrowsableAPIRenderer(BaseRenderer): try: view.check_permissions(request) - view.check_object_permissions(request, obj) + if obj is not None: + view.check_object_permissions(request, obj) except exceptions.APIException: return False # Doesn't have permissions return True