aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2014-11-19 14:04:31 +0000
committerTom Christie2014-11-19 14:04:31 +0000
commitbc83dfece460b6639d915bd8fb42b3701cd91172 (patch)
tree7616b21fd6d02caef557be43a12e8d7979ee4997
parentf269826a7d8a980536ffbce369be369c4df58d23 (diff)
parent51b7033e4aeeefe19012a77b09a0b23d4a52a5bc (diff)
downloaddjango-rest-framework-bc83dfece460b6639d915bd8fb42b3701cd91172.tar.bz2
Merge branch 'master' into 3.0-beta
-rw-r--r--.travis.yml2
-rw-r--r--docs/topics/3.0-announcement.md5
-rw-r--r--rest_framework/exceptions.py74
-rw-r--r--rest_framework/fields.py2
-rw-r--r--rest_framework/serializers.py83
-rw-r--r--rest_framework/templates/rest_framework/horizontal/list_fieldset.html3
-rw-r--r--rest_framework/templates/rest_framework/inline/list_fieldset.html1
-rw-r--r--rest_framework/templates/rest_framework/vertical/list_fieldset.html1
-rw-r--r--rest_framework/validators.py14
-rw-r--r--tests/test_fields.py3
-rw-r--r--tests/test_validators.py4
11 files changed, 136 insertions, 56 deletions
diff --git a/.travis.yml b/.travis.yml
index 4c06acf5..9b2e4738 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,8 @@ language: python
python: 2.7
+sudo: false
+
env:
- TOX_ENV=flake8
- TOX_ENV=py3.4-django1.7
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 06fdc9fd..958be2d6 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -823,6 +823,11 @@ Or modify it on an individual serializer field, using the `coerce_to_string` key
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.
+## Miscellaneous notes.
+
+* 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.
+* 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.
+
## What's coming next.
3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes.
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 0b06d6e6..dbab6684 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -5,7 +5,11 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
+
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy
from rest_framework import status
+from rest_framework.compat import force_text
import math
@@ -15,10 +19,13 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = 'A server error occured'
+ default_detail = _('A server error occured')
def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
def __str__(self):
return self.detail
@@ -31,6 +38,19 @@ class APIException(Exception):
# from rest_framework import serializers
# raise serializers.ValidationError('Value was invalid')
+def force_text_recursive(data):
+ if isinstance(data, list):
+ return [
+ force_text_recursive(item) for item in data
+ ]
+ elif isinstance(data, dict):
+ return dict([
+ (key, force_text_recursive(value))
+ for key, value in data.items()
+ ])
+ return force_text(data)
+
+
class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
@@ -39,7 +59,7 @@ class ValidationError(APIException):
# The details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]
- self.detail = detail
+ self.detail = force_text_recursive(detail)
def __str__(self):
return str(self.detail)
@@ -47,59 +67,77 @@ class ValidationError(APIException):
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
- default_detail = 'Malformed request.'
+ default_detail = _('Malformed request.')
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Incorrect authentication credentials.'
+ default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Authentication credentials were not provided.'
+ default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
- default_detail = 'You do not have permission to perform this action.'
+ default_detail = _('You do not have permission to perform this action.')
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = "Method '%s' not allowed."
+ default_detail = _("Method '%s' not allowed.")
def __init__(self, method, detail=None):
- self.detail = detail or (self.default_detail % method)
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail) % method
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = "Could not satisfy the request Accept header"
+ default_detail = _('Could not satisfy the request Accept header')
def __init__(self, detail=None, available_renderers=None):
- self.detail = detail or self.default_detail
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = "Unsupported media type '%s' in request."
+ default_detail = _("Unsupported media type '%s' in request.")
def __init__(self, media_type, detail=None):
- self.detail = detail or (self.default_detail % media_type)
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail) % media_type
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = 'Request was throttled.'
- extra_detail = " Expected available in %d second%s."
+ default_detail = _('Request was throttled.')
+ extra_detail = ungettext_lazy(
+ 'Expected available in %(wait)d second.',
+ 'Expected available in %(wait)d seconds.',
+ 'wait'
+ )
def __init__(self, wait=None, detail=None):
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
+
if wait is None:
- self.detail = detail or self.default_detail
self.wait = None
else:
- format = (detail or self.default_detail) + self.extra_detail
- self.detail = format % (wait, wait != 1 and 's' or '')
self.wait = math.ceil(wait)
+ self.detail += ' ' + force_text(
+ self.extra_detail % {'wait': self.wait}
+ )
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 36afe7a9..bb43708d 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -947,6 +947,8 @@ class ChoiceField(Field):
self.fail('invalid_choice', input=data)
def to_representation(self, value):
+ if value in ('', None):
+ return value
return self.choice_strings_to_values[six.text_type(value)]
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 84282cdb..2e34dbe7 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -720,49 +720,60 @@ class ModelSerializer(Serializer):
# Determine if we need any additional `HiddenField` or extra keyword
# arguments to deal with `unique_for` dates that are required to
# be in the input data in order to validate it.
- unique_fields = {}
+ hidden_fields = {}
+
for model_field_name, field_name in model_field_mapping.items():
try:
model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist:
continue
- # Deal with each of the `unique_for_*` cases.
- for date_field_name in (
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names = set([
model_field.unique_for_date,
model_field.unique_for_month,
model_field.unique_for_year
- ):
- if date_field_name is None:
- continue
-
- # Get the model field that is refered too.
- date_field = model._meta.get_field(date_field_name)
-
- if date_field.auto_now_add:
- default = CreateOnlyDefault(timezone.now)
- elif date_field.auto_now:
- default = timezone.now
- elif date_field.has_default():
- default = model_field.default
- else:
- default = empty
-
- if date_field_name in model_field_mapping:
- # The corresponding date field is present in the serializer
- if date_field_name not in extra_kwargs:
- extra_kwargs[date_field_name] = {}
- if default is empty:
- if 'required' not in extra_kwargs[date_field_name]:
- extra_kwargs[date_field_name]['required'] = True
- else:
- if 'default' not in extra_kwargs[date_field_name]:
- extra_kwargs[date_field_name]['default'] = default
+ ])
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(fields).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is refered too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = model_field.default
+ else:
+ default = empty
+
+ if unique_constraint_name in model_field_mapping:
+ # The corresponding field is present in the serializer
+ if unique_constraint_name not in extra_kwargs:
+ extra_kwargs[unique_constraint_name] = {}
+ if default is empty:
+ if 'required' not in extra_kwargs[unique_constraint_name]:
+ extra_kwargs[unique_constraint_name]['required'] = True
else:
- # The corresponding date field is not present in the,
- # serializer. We have a default to use for the date, so
- # add in a hidden field that populates it.
- unique_fields[date_field_name] = HiddenField(default=default)
+ if 'default' not in extra_kwargs[unique_constraint_name]:
+ extra_kwargs[unique_constraint_name]['default'] = default
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
# Now determine the fields that should be included on the serializer.
for field_name in fields:
@@ -838,12 +849,16 @@ class ModelSerializer(Serializer):
'validators', 'queryset'
]:
kwargs.pop(attr, None)
+
+ if extras.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
+
kwargs.update(extras)
# Create the serializer field.
ret[field_name] = field_cls(**kwargs)
- for field_name, field in unique_fields.items():
+ for field_name, field in hidden_fields.items():
ret[field_name] = field
return ret
diff --git a/rest_framework/templates/rest_framework/horizontal/list_fieldset.html b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
index a30514c6..a9ff04a6 100644
--- a/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
+++ b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
@@ -5,9 +5,12 @@
<legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
</div>
{% endif %}
+ <!--
<ul>
{% for child in field.value %}
<li>TODO</li>
{% endfor %}
</ul>
+ -->
+ <p>Lists are not currently supported in HTML input.</p>
</fieldset>
diff --git a/rest_framework/templates/rest_framework/inline/list_fieldset.html b/rest_framework/templates/rest_framework/inline/list_fieldset.html
new file mode 100644
index 00000000..2ae56d7c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/list_fieldset.html
@@ -0,0 +1 @@
+<span>Lists are not currently supported in HTML input.</span>
diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
index 74bbf448..1d86c7f2 100644
--- a/rest_framework/templates/rest_framework/vertical/list_fieldset.html
+++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
@@ -4,4 +4,5 @@
{% for field_item in field.value.field_items.values() %}
{{ renderer.render_field(field_item, layout=layout) }}
{% endfor %} -->
+ <p>Lists are not currently supported in HTML input.</p>
</fieldset>
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index fa4f1847..7ca4e6a9 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -93,6 +93,9 @@ class UniqueTogetherValidator:
The `UniqueTogetherValidator` always forces an implied 'required'
state on the fields it applies to.
"""
+ if self.instance is not None:
+ return
+
missing = dict([
(field_name, self.missing_message)
for field_name in self.fields
@@ -105,8 +108,17 @@ class UniqueTogetherValidator:
"""
Filter the queryset to all instances matching the given attributes.
"""
+ # If this is an update, then any unprovided field should
+ # have it's value set based on the existing instance attribute.
+ if self.instance is not None:
+ for field_name in self.fields:
+ if field_name not in attrs:
+ attrs[field_name] = getattr(self.instance, field_name)
+
+ # Determine the filter keyword arguments and filter the queryset.
filter_kwargs = dict([
- (field_name, attrs[field_name]) for field_name in self.fields
+ (field_name, attrs[field_name])
+ for field_name in self.fields
])
return queryset.filter(**filter_kwargs)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 5db381ac..13525632 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -793,7 +793,8 @@ class TestChoiceField(FieldValues):
'amazing': ['`amazing` is not a valid choice.']
}
outputs = {
- 'good': 'good'
+ 'good': 'good',
+ '': ''
}
field = serializers.ChoiceField(
choices=[
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 86614b10..1df0641c 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -88,8 +88,8 @@ class TestUniquenessTogetherValidation(TestCase):
expected = dedent("""
UniquenessTogetherSerializer():
id = IntegerField(label='ID', read_only=True)
- race_name = CharField(max_length=100)
- position = IntegerField()
+ race_name = CharField(max_length=100, required=True)
+ position = IntegerField(required=True)
class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
""")