Skip to content

Commit 42b61ff

Browse files
committed
Merge pull request encode#1 from nschlemm/issue-192-expose-fields-for-options
Merged work in progress for Issue 192 expose fields for options
2 parents fecadac + c0f3a1c commit 42b61ff

24 files changed

+949
-208
lines changed

docs/api-guide/relations.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can
381381

382382
For more information see [the Django documentation on generic relations][generic-relations].
383383

384+
## ManyToManyFields with a Through Model
385+
386+
By default, relational fields that target a ``ManyToManyField`` with a
387+
``through`` model specified are set to read-only.
388+
389+
If you exlicitly specify a relational field pointing to a
390+
``ManyToManyField`` with a through model, be sure to set ``read_only``
391+
to ``True``.
392+
384393
## Advanced Hyperlinked fields
385394

386395
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.

docs/api-guide/renderers.md

Lines changed: 7 additions & 3 deletions
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.
@@ -272,10 +276,10 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
272276
* Load and render a template named `api_exception.html`.
273277
* Render the HTTP status code and text, for example "404 Not Found".
274278

275-
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
276-
277279
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
278280

281+
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
282+
279283
---
280284

281285
# Third party packages

docs/topics/browsable-api.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool]
3535

3636
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
3737

38+
Full Example
39+
40+
{% extends "rest_framework/base.html" %}
41+
42+
{% block bootstrap_theme %}
43+
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
44+
{% endblock %}
45+
46+
{% block bootstrap_navbar_variant %}{% endblock %}
47+
48+
3849
For more specific CSS tweaks, use the `style` block instead.
3950

4051

docs/topics/credits.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ The following people have helped make REST framework great.
127127
* Craig de Stigter - [craigds]
128128
* Pablo Recio - [pyriku]
129129
* Brian Zambrano - [brianz]
130+
* Òscar Vilaplana - [grimborg]
131+
* Ryan Kaskel - [ryankask]
132+
* Andy McKay - [andymckay]
133+
* Matteo Suppo - [matteosuppo]
134+
* Karol Majta - [lolek09]
130135

131136
Many thanks to everyone who's contributed to the project.
132137

@@ -290,3 +295,8 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
290295
[craigds]: https://github.com/craigds
291296
[pyriku]: https://github.com/pyriku
292297
[brianz]: https://github.com/brianz
298+
[grimborg]: https://github.com/grimborg
299+
[ryankask]: https://github.com/ryankask
300+
[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: 5 additions & 7 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,
@@ -137,7 +136,7 @@ def humanize_field(field):
137136
humanized = {
138137
'type': humanize_field_type(field.__class__),
139138
'required': getattr(field, 'required', False),
140-
'label': field.label,
139+
'label': getattr(field, 'label', None),
141140
}
142141
optional_attrs = ['read_only', 'help_text']
143142
for attr in optional_attrs:
@@ -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
"""
@@ -470,7 +470,6 @@ class URLField(CharField):
470470
type_name = 'URLField'
471471

472472
def __init__(self, **kwargs):
473-
kwargs['max_length'] = kwargs.get('max_length', 200)
474473
kwargs['validators'] = [validators.URLValidator()]
475474
super(URLField, self).__init__(**kwargs)
476475

@@ -479,7 +478,6 @@ class SlugField(CharField):
479478
type_name = 'SlugField'
480479

481480
def __init__(self, *args, **kwargs):
482-
kwargs['max_length'] = kwargs.get('max_length', 50)
483481
super(SlugField, self).__init__(*args, **kwargs)
484482

485483

rest_framework/relations.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.core.exceptions import ObjectDoesNotExist, ValidationError
99
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
1010
from django import forms
11+
from django.db.models.fields import BLANK_CHOICE_DASH
1112
from django.forms import widgets
1213
from django.forms.models import ModelChoiceIterator
1314
from django.utils.translation import ugettext_lazy as _
@@ -47,7 +48,7 @@ def __init__(self, *args, **kwargs):
4748
DeprecationWarning, stacklevel=2)
4849
kwargs['required'] = not kwargs.pop('null')
4950

50-
self.queryset = kwargs.pop('queryset', None)
51+
queryset = kwargs.pop('queryset', None)
5152
self.many = kwargs.pop('many', self.many)
5253
if self.many:
5354
self.widget = self.many_widget
@@ -56,6 +57,11 @@ def __init__(self, *args, **kwargs):
5657
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
5758
super(RelatedField, self).__init__(*args, **kwargs)
5859

60+
if not self.required:
61+
self.empty_label = BLANK_CHOICE_DASH[0][1]
62+
63+
self.queryset = queryset
64+
5965
def initialize(self, parent, field_name):
6066
super(RelatedField, self).initialize(parent, field_name)
6167
if self.queryset is None and not self.read_only:
@@ -442,7 +448,7 @@ def from_native(self, value):
442448
raise Exception('Writable related fields must include a `queryset` argument')
443449

444450
try:
445-
http_prefix = value.startswith('http:') or value.startswith('https:')
451+
http_prefix = value.startswith(('http:', 'https:'))
446452
except AttributeError:
447453
msg = self.error_messages['incorrect_type']
448454
raise ValidationError(msg % type(value).__name__)

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

0 commit comments

Comments
 (0)