Skip to content

Commit 9454e23

Browse files
committed
Merge branch 'master' of git://github.com/tomchristie/django-rest-framework into issue-192-expose-fields-for-options
2 parents 843ae60 + 7c945b4 commit 9454e23

File tree

12 files changed

+246
-22
lines changed

12 files changed

+246
-22
lines changed

docs/api-guide/renderers.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,18 @@ If your API includes views that can serve both regular webpages and API response
6767

6868
## JSONRenderer
6969

70-
Renders the request data into `JSON`.
70+
Renders the request data into `JSON` enforcing ASCII encoding
7171

7272
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`.
7373

7474
**.media_type**: `application/json`
7575

7676
**.format**: `'.json'`
7777

78+
## UnicodeJSONRenderer
79+
80+
Same as `JSONRenderer` but doesn't enforce ASCII encoding
81+
7882
## JSONPRenderer
7983

8084
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.

docs/topics/credits.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ The following people have helped make REST framework great.
130130
* Òscar Vilaplana - [grimborg]
131131
* Ryan Kaskel - [ryankask]
132132
* Andy McKay - [andymckay]
133+
* Matteo Suppo - [matteosuppo]
134+
* Karol Majta - [lolek09]
133135

134136
Many thanks to everyone who's contributed to the project.
135137

@@ -296,3 +298,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
296298
[grimborg]: https://github.com/grimborg
297299
[ryankask]: https://github.com/ryankask
298300
[andymckay]: https://github.com/andymckay
301+
[matteosuppo]: https://github.com/matteosuppo
302+
[lolek09]: https://github.com/lolek09

rest_framework/compat.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,16 @@ def apply_markdown(text):
495495
oauth2_provider_forms = None
496496
oauth2_provider_scope = None
497497
oauth2_constants = None
498+
499+
# Handle lazy strings
500+
from django.utils.functional import Promise
501+
502+
if six.PY3:
503+
def is_non_str_iterable(obj):
504+
if (isinstance(obj, str) or
505+
(isinstance(obj, Promise) and obj._delegate_text)):
506+
return False
507+
return hasattr(obj, '__iter__')
508+
else:
509+
def is_non_str_iterable(obj):
510+
return hasattr(obj, '__iter__')

rest_framework/fields.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
parse_time)
2828
from rest_framework.compat import BytesIO
2929
from rest_framework.compat import six
30-
from rest_framework.compat import smart_text
30+
from rest_framework.compat import smart_text, force_text, is_non_str_iterable
3131
from rest_framework.settings import api_settings
3232

3333

@@ -76,7 +76,6 @@ def is_simple_callable(obj):
7676
len_defaults = len(defaults) if defaults else 0
7777
return len_args <= len_defaults
7878

79-
8079
def get_component(obj, attr_name):
8180
"""
8281
Given an object, and an attribute name,
@@ -256,15 +255,16 @@ def to_native(self, value):
256255

257256
if is_protected_type(value):
258257
return value
259-
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
258+
elif (is_non_str_iterable(value) and
259+
not isinstance(value, (dict, six.string_types))):
260260
return [self.to_native(item) for item in value]
261261
elif isinstance(value, dict):
262262
# Make sure we preserve field ordering, if it exists
263263
ret = SortedDict()
264264
for key, val in value.items():
265265
ret[key] = self.to_native(val)
266266
return ret
267-
return smart_text(value)
267+
return force_text(value)
268268

269269
def attributes(self):
270270
"""

rest_framework/renderers.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class BaseRenderer(object):
3636

3737
media_type = None
3838
format = None
39+
charset = None
3940

4041
def render(self, data, accepted_media_type=None, renderer_context=None):
4142
raise NotImplemented('Renderer class requires .render() to be implemented')
@@ -49,6 +50,7 @@ class JSONRenderer(BaseRenderer):
4950
media_type = 'application/json'
5051
format = 'json'
5152
encoder_class = encoders.JSONEncoder
53+
ensure_ascii = True
5254

5355
def render(self, data, accepted_media_type=None, renderer_context=None):
5456
"""
@@ -72,7 +74,12 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
7274
except (ValueError, TypeError):
7375
indent = None
7476

75-
return json.dumps(data, cls=self.encoder_class, indent=indent)
77+
return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii)
78+
79+
80+
class UnicodeJSONRenderer(JSONRenderer):
81+
ensure_ascii = False
82+
charset = 'utf-8'
7683

7784

7885
class JSONPRenderer(JSONRenderer):
@@ -115,6 +122,7 @@ class XMLRenderer(BaseRenderer):
115122

116123
media_type = 'application/xml'
117124
format = 'xml'
125+
charset = 'utf-8'
118126

119127
def render(self, data, accepted_media_type=None, renderer_context=None):
120128
"""
@@ -164,6 +172,7 @@ class YAMLRenderer(BaseRenderer):
164172
media_type = 'application/yaml'
165173
format = 'yaml'
166174
encoder = encoders.SafeDumper
175+
charset = 'utf-8'
167176

168177
def render(self, data, accepted_media_type=None, renderer_context=None):
169178
"""
@@ -204,6 +213,7 @@ class TemplateHTMLRenderer(BaseRenderer):
204213
'%(status_code)s.html',
205214
'api_exception.html'
206215
]
216+
charset = 'utf-8'
207217

208218
def render(self, data, accepted_media_type=None, renderer_context=None):
209219
"""
@@ -275,6 +285,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
275285
"""
276286
media_type = 'text/html'
277287
format = 'html'
288+
charset = 'utf-8'
278289

279290
def render(self, data, accepted_media_type=None, renderer_context=None):
280291
renderer_context = renderer_context or {}
@@ -296,6 +307,7 @@ class BrowsableAPIRenderer(BaseRenderer):
296307
media_type = 'text/html'
297308
format = 'api'
298309
template = 'rest_framework/api.html'
310+
charset = 'utf-8'
299311

300312
def get_default_renderer(self, view):
301313
"""
@@ -321,7 +333,7 @@ def get_content(self, renderer, data,
321333
content = renderer.render(data, accepted_media_type, renderer_context)
322334

323335
if not all(char in string.printable for char in content):
324-
return '[%d bytes of binary content]'
336+
return '[%d bytes of binary content]' % len(content)
325337

326338
return content
327339

@@ -337,6 +349,8 @@ def show_form_for_method(self, view, method, request, obj):
337349

338350
try:
339351
view.check_permissions(request)
352+
if obj is not None:
353+
view.check_object_permissions(request, obj)
340354
except exceptions.APIException:
341355
return False # Doesn't have permissions
342356
return True

rest_framework/response.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse):
1818

1919
def __init__(self, data=None, status=200,
2020
template_name=None, headers=None,
21-
exception=False):
21+
exception=False, charset=None):
2222
"""
2323
Alters the init arguments slightly.
2424
For example, drop 'template_name', and instead use 'data'.
@@ -30,6 +30,7 @@ def __init__(self, data=None, status=200,
3030
self.data = data
3131
self.template_name = template_name
3232
self.exception = exception
33+
self.charset = charset
3334

3435
if headers:
3536
for name, value in six.iteritems(headers):
@@ -46,7 +47,14 @@ def rendered_content(self):
4647
assert context, ".renderer_context not set on Response"
4748
context['response'] = self
4849

49-
self['Content-Type'] = media_type
50+
if self.charset is None:
51+
self.charset = renderer.charset
52+
53+
if self.charset is not None:
54+
content_type = "{0}; charset={1}".format(media_type, self.charset)
55+
else:
56+
content_type = media_type
57+
self['Content-Type'] = content_type
5058
return renderer.render(self.data, media_type, context)
5159

5260
@property

rest_framework/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,11 @@ def get_field(self, model_field):
744744
kwargs['choices'] = model_field.flatchoices
745745
return ChoiceField(**kwargs)
746746

747+
# put this below the ChoiceField because min_value isn't a valid initializer
748+
if issubclass(model_field.__class__, models.PositiveIntegerField) or\
749+
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
750+
kwargs['min_value'] = 0
751+
747752
attribute_dict = {
748753
models.CharField: ['max_length'],
749754
models.CommaSeparatedIntegerField: ['max_length'],

rest_framework/tests/htmlrenderer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,19 @@ def tearDown(self):
6666
def test_simple_html_view(self):
6767
response = self.client.get('/')
6868
self.assertContains(response, "example: foobar")
69-
self.assertEqual(response['Content-Type'], 'text/html')
69+
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
7070

7171
def test_not_found_html_view(self):
7272
response = self.client.get('/not_found')
7373
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
7474
self.assertEqual(response.content, six.b("404 Not Found"))
75-
self.assertEqual(response['Content-Type'], 'text/html')
75+
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
7676

7777
def test_permission_denied_html_view(self):
7878
response = self.client.get('/permission_denied')
7979
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
8080
self.assertEqual(response.content, six.b("403 Forbidden"))
81-
self.assertEqual(response['Content-Type'], 'text/html')
81+
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
8282

8383

8484
class TemplateHTMLRendererExceptionTests(TestCase):
@@ -109,10 +109,10 @@ def test_not_found_html_view_with_template(self):
109109
response = self.client.get('/not_found')
110110
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
111111
self.assertEqual(response.content, six.b("404: Not found"))
112-
self.assertEqual(response['Content-Type'], 'text/html')
112+
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
113113

114114
def test_permission_denied_html_view_with_template(self):
115115
response = self.client.get('/permission_denied')
116116
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
117117
self.assertEqual(response.content, six.b("403: Permission denied"))
118-
self.assertEqual(response['Content-Type'], 'text/html')
118+
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')

rest_framework/tests/negotiation.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@
33
from django.test.client import RequestFactory
44
from rest_framework.negotiation import DefaultContentNegotiation
55
from rest_framework.request import Request
6+
from rest_framework.renderers import BaseRenderer
67

78

89
factory = RequestFactory()
910

1011

11-
class MockJSONRenderer(object):
12+
class MockJSONRenderer(BaseRenderer):
1213
media_type = 'application/json'
1314

1415

15-
class MockHTMLRenderer(object):
16+
class MockHTMLRenderer(BaseRenderer):
1617
media_type = 'text/html'
1718

1819

20+
class NoCharsetSpecifiedRenderer(BaseRenderer):
21+
media_type = 'my/media'
22+
23+
1924
class TestAcceptedMediaType(TestCase):
2025
def setUp(self):
2126
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]

rest_framework/tests/renderers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
from decimal import Decimal
23
from django.core.cache import cache
34
from django.test import TestCase
@@ -8,7 +9,7 @@
89
from rest_framework.response import Response
910
from rest_framework.views import APIView
1011
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
11-
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
12+
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
1213
from rest_framework.parsers import YAMLParser, XMLParser
1314
from rest_framework.settings import api_settings
1415
from rest_framework.compat import StringIO
@@ -254,6 +255,23 @@ def test_with_content_type_args(self):
254255
content = renderer.render(obj, 'application/json; indent=2')
255256
self.assertEqual(strip_trailing_whitespace(content), _indented_repr)
256257

258+
def test_check_ascii(self):
259+
obj = {'countries': ['United Kingdom', 'France', 'España']}
260+
renderer = JSONRenderer()
261+
content = renderer.render(obj, 'application/json')
262+
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}')
263+
264+
265+
class UnicodeJSONRendererTests(TestCase):
266+
"""
267+
Tests specific for the Unicode JSON Renderer
268+
"""
269+
def test_proper_encoding(self):
270+
obj = {'countries': ['United Kingdom', 'France', 'España']}
271+
renderer = UnicodeJSONRenderer()
272+
content = renderer.render(obj, 'application/json')
273+
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}')
274+
257275

258276
class JSONPRendererTests(TestCase):
259277
"""

0 commit comments

Comments
 (0)