Skip to content

Commit bc83dfe

Browse files
committed
Merge branch 'master' into 3.0-beta
2 parents f269826 + 51b7033 commit bc83dfe

File tree

11 files changed

+136
-56
lines changed

11 files changed

+136
-56
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ language: python
22

33
python: 2.7
44

5+
sudo: false
6+
57
env:
68
- TOX_ENV=flake8
79
- TOX_ENV=py3.4-django1.7

docs/topics/3.0-announcement.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,11 @@ Or modify it on an individual serializer field, using the `coerce_to_string` key
823823

824824
The default JSON renderer will return float objects for uncoerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs.
825825

826+
## Miscellaneous notes.
827+
828+
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
829+
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
830+
826831
## What's coming next.
827832

828833
3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes.

rest_framework/exceptions.py

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
66
"""
77
from __future__ import unicode_literals
8+
9+
from django.utils.translation import ugettext_lazy as _
10+
from django.utils.translation import ungettext_lazy
811
from rest_framework import status
12+
from rest_framework.compat import force_text
913
import math
1014

1115

@@ -15,10 +19,13 @@ class APIException(Exception):
1519
Subclasses should provide `.status_code` and `.default_detail` properties.
1620
"""
1721
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
18-
default_detail = 'A server error occured'
22+
default_detail = _('A server error occured')
1923

2024
def __init__(self, detail=None):
21-
self.detail = detail or self.default_detail
25+
if detail is not None:
26+
self.detail = force_text(detail)
27+
else:
28+
self.detail = force_text(self.default_detail)
2229

2330
def __str__(self):
2431
return self.detail
@@ -31,6 +38,19 @@ def __str__(self):
3138
# from rest_framework import serializers
3239
# raise serializers.ValidationError('Value was invalid')
3340

41+
def force_text_recursive(data):
42+
if isinstance(data, list):
43+
return [
44+
force_text_recursive(item) for item in data
45+
]
46+
elif isinstance(data, dict):
47+
return dict([
48+
(key, force_text_recursive(value))
49+
for key, value in data.items()
50+
])
51+
return force_text(data)
52+
53+
3454
class ValidationError(APIException):
3555
status_code = status.HTTP_400_BAD_REQUEST
3656

@@ -39,67 +59,85 @@ def __init__(self, detail):
3959
# The details should always be coerced to a list if not already.
4060
if not isinstance(detail, dict) and not isinstance(detail, list):
4161
detail = [detail]
42-
self.detail = detail
62+
self.detail = force_text_recursive(detail)
4363

4464
def __str__(self):
4565
return str(self.detail)
4666

4767

4868
class ParseError(APIException):
4969
status_code = status.HTTP_400_BAD_REQUEST
50-
default_detail = 'Malformed request.'
70+
default_detail = _('Malformed request.')
5171

5272

5373
class AuthenticationFailed(APIException):
5474
status_code = status.HTTP_401_UNAUTHORIZED
55-
default_detail = 'Incorrect authentication credentials.'
75+
default_detail = _('Incorrect authentication credentials.')
5676

5777

5878
class NotAuthenticated(APIException):
5979
status_code = status.HTTP_401_UNAUTHORIZED
60-
default_detail = 'Authentication credentials were not provided.'
80+
default_detail = _('Authentication credentials were not provided.')
6181

6282

6383
class PermissionDenied(APIException):
6484
status_code = status.HTTP_403_FORBIDDEN
65-
default_detail = 'You do not have permission to perform this action.'
85+
default_detail = _('You do not have permission to perform this action.')
6686

6787

6888
class MethodNotAllowed(APIException):
6989
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
70-
default_detail = "Method '%s' not allowed."
90+
default_detail = _("Method '%s' not allowed.")
7191

7292
def __init__(self, method, detail=None):
73-
self.detail = detail or (self.default_detail % method)
93+
if detail is not None:
94+
self.detail = force_text(detail)
95+
else:
96+
self.detail = force_text(self.default_detail) % method
7497

7598

7699
class NotAcceptable(APIException):
77100
status_code = status.HTTP_406_NOT_ACCEPTABLE
78-
default_detail = "Could not satisfy the request Accept header"
101+
default_detail = _('Could not satisfy the request Accept header')
79102

80103
def __init__(self, detail=None, available_renderers=None):
81-
self.detail = detail or self.default_detail
104+
if detail is not None:
105+
self.detail = force_text(detail)
106+
else:
107+
self.detail = force_text(self.default_detail)
82108
self.available_renderers = available_renderers
83109

84110

85111
class UnsupportedMediaType(APIException):
86112
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
87-
default_detail = "Unsupported media type '%s' in request."
113+
default_detail = _("Unsupported media type '%s' in request.")
88114

89115
def __init__(self, media_type, detail=None):
90-
self.detail = detail or (self.default_detail % media_type)
116+
if detail is not None:
117+
self.detail = force_text(detail)
118+
else:
119+
self.detail = force_text(self.default_detail) % media_type
91120

92121

93122
class Throttled(APIException):
94123
status_code = status.HTTP_429_TOO_MANY_REQUESTS
95-
default_detail = 'Request was throttled.'
96-
extra_detail = " Expected available in %d second%s."
124+
default_detail = _('Request was throttled.')
125+
extra_detail = ungettext_lazy(
126+
'Expected available in %(wait)d second.',
127+
'Expected available in %(wait)d seconds.',
128+
'wait'
129+
)
97130

98131
def __init__(self, wait=None, detail=None):
132+
if detail is not None:
133+
self.detail = force_text(detail)
134+
else:
135+
self.detail = force_text(self.default_detail)
136+
99137
if wait is None:
100-
self.detail = detail or self.default_detail
101138
self.wait = None
102139
else:
103-
format = (detail or self.default_detail) + self.extra_detail
104-
self.detail = format % (wait, wait != 1 and 's' or '')
105140
self.wait = math.ceil(wait)
141+
self.detail += ' ' + force_text(
142+
self.extra_detail % {'wait': self.wait}
143+
)

rest_framework/fields.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,8 @@ def to_internal_value(self, data):
947947
self.fail('invalid_choice', input=data)
948948

949949
def to_representation(self, value):
950+
if value in ('', None):
951+
return value
950952
return self.choice_strings_to_values[six.text_type(value)]
951953

952954

rest_framework/serializers.py

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -720,49 +720,60 @@ def get_fields(self):
720720
# Determine if we need any additional `HiddenField` or extra keyword
721721
# arguments to deal with `unique_for` dates that are required to
722722
# be in the input data in order to validate it.
723-
unique_fields = {}
723+
hidden_fields = {}
724+
724725
for model_field_name, field_name in model_field_mapping.items():
725726
try:
726727
model_field = model._meta.get_field(model_field_name)
727728
except FieldDoesNotExist:
728729
continue
729730

730-
# Deal with each of the `unique_for_*` cases.
731-
for date_field_name in (
731+
# Include each of the `unique_for_*` field names.
732+
unique_constraint_names = set([
732733
model_field.unique_for_date,
733734
model_field.unique_for_month,
734735
model_field.unique_for_year
735-
):
736-
if date_field_name is None:
737-
continue
738-
739-
# Get the model field that is refered too.
740-
date_field = model._meta.get_field(date_field_name)
741-
742-
if date_field.auto_now_add:
743-
default = CreateOnlyDefault(timezone.now)
744-
elif date_field.auto_now:
745-
default = timezone.now
746-
elif date_field.has_default():
747-
default = model_field.default
748-
else:
749-
default = empty
750-
751-
if date_field_name in model_field_mapping:
752-
# The corresponding date field is present in the serializer
753-
if date_field_name not in extra_kwargs:
754-
extra_kwargs[date_field_name] = {}
755-
if default is empty:
756-
if 'required' not in extra_kwargs[date_field_name]:
757-
extra_kwargs[date_field_name]['required'] = True
758-
else:
759-
if 'default' not in extra_kwargs[date_field_name]:
760-
extra_kwargs[date_field_name]['default'] = default
736+
])
737+
unique_constraint_names -= set([None])
738+
739+
# Include each of the `unique_together` field names,
740+
# so long as all the field names are included on the serializer.
741+
for parent_class in [model] + list(model._meta.parents.keys()):
742+
for unique_together_list in parent_class._meta.unique_together:
743+
if set(fields).issuperset(set(unique_together_list)):
744+
unique_constraint_names |= set(unique_together_list)
745+
746+
# Now we have all the field names that have uniqueness constraints
747+
# applied, we can add the extra 'required=...' or 'default=...'
748+
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
749+
for unique_constraint_name in unique_constraint_names:
750+
# Get the model field that is refered too.
751+
unique_constraint_field = model._meta.get_field(unique_constraint_name)
752+
753+
if getattr(unique_constraint_field, 'auto_now_add', None):
754+
default = CreateOnlyDefault(timezone.now)
755+
elif getattr(unique_constraint_field, 'auto_now', None):
756+
default = timezone.now
757+
elif unique_constraint_field.has_default():
758+
default = model_field.default
759+
else:
760+
default = empty
761+
762+
if unique_constraint_name in model_field_mapping:
763+
# The corresponding field is present in the serializer
764+
if unique_constraint_name not in extra_kwargs:
765+
extra_kwargs[unique_constraint_name] = {}
766+
if default is empty:
767+
if 'required' not in extra_kwargs[unique_constraint_name]:
768+
extra_kwargs[unique_constraint_name]['required'] = True
761769
else:
762-
# The corresponding date field is not present in the,
763-
# serializer. We have a default to use for the date, so
764-
# add in a hidden field that populates it.
765-
unique_fields[date_field_name] = HiddenField(default=default)
770+
if 'default' not in extra_kwargs[unique_constraint_name]:
771+
extra_kwargs[unique_constraint_name]['default'] = default
772+
elif default is not empty:
773+
# The corresponding field is not present in the,
774+
# serializer. We have a default to use for it, so
775+
# add in a hidden field that populates it.
776+
hidden_fields[unique_constraint_name] = HiddenField(default=default)
766777

767778
# Now determine the fields that should be included on the serializer.
768779
for field_name in fields:
@@ -838,12 +849,16 @@ def get_fields(self):
838849
'validators', 'queryset'
839850
]:
840851
kwargs.pop(attr, None)
852+
853+
if extras.get('default') and kwargs.get('required') is False:
854+
kwargs.pop('required')
855+
841856
kwargs.update(extras)
842857

843858
# Create the serializer field.
844859
ret[field_name] = field_cls(**kwargs)
845860

846-
for field_name, field in unique_fields.items():
861+
for field_name, field in hidden_fields.items():
847862
ret[field_name] = field
848863

849864
return ret

rest_framework/templates/rest_framework/horizontal/list_fieldset.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
<legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
66
</div>
77
{% endif %}
8+
<!--
89
<ul>
910
{% for child in field.value %}
1011
<li>TODO</li>
1112
{% endfor %}
1213
</ul>
14+
-->
15+
<p>Lists are not currently supported in HTML input.</p>
1316
</fieldset>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<span>Lists are not currently supported in HTML input.</span>

rest_framework/templates/rest_framework/vertical/list_fieldset.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
{% for field_item in field.value.field_items.values() %}
55
{{ renderer.render_field(field_item, layout=layout) }}
66
{% endfor %} -->
7+
<p>Lists are not currently supported in HTML input.</p>
78
</fieldset>

rest_framework/validators.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ def enforce_required_fields(self, attrs):
9393
The `UniqueTogetherValidator` always forces an implied 'required'
9494
state on the fields it applies to.
9595
"""
96+
if self.instance is not None:
97+
return
98+
9699
missing = dict([
97100
(field_name, self.missing_message)
98101
for field_name in self.fields
@@ -105,8 +108,17 @@ def filter_queryset(self, attrs, queryset):
105108
"""
106109
Filter the queryset to all instances matching the given attributes.
107110
"""
111+
# If this is an update, then any unprovided field should
112+
# have it's value set based on the existing instance attribute.
113+
if self.instance is not None:
114+
for field_name in self.fields:
115+
if field_name not in attrs:
116+
attrs[field_name] = getattr(self.instance, field_name)
117+
118+
# Determine the filter keyword arguments and filter the queryset.
108119
filter_kwargs = dict([
109-
(field_name, attrs[field_name]) for field_name in self.fields
120+
(field_name, attrs[field_name])
121+
for field_name in self.fields
110122
])
111123
return queryset.filter(**filter_kwargs)
112124

tests/test_fields.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,8 @@ class TestChoiceField(FieldValues):
793793
'amazing': ['`amazing` is not a valid choice.']
794794
}
795795
outputs = {
796-
'good': 'good'
796+
'good': 'good',
797+
'': ''
797798
}
798799
field = serializers.ChoiceField(
799800
choices=[

tests/test_validators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ def test_repr(self):
8888
expected = dedent("""
8989
UniquenessTogetherSerializer():
9090
id = IntegerField(label='ID', read_only=True)
91-
race_name = CharField(max_length=100)
92-
position = IntegerField()
91+
race_name = CharField(max_length=100, required=True)
92+
position = IntegerField(required=True)
9393
class Meta:
9494
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
9595
""")

0 commit comments

Comments
 (0)