aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorTom Christie2014-11-19 14:04:31 +0000
committerTom Christie2014-11-19 14:04:31 +0000
commitbc83dfece460b6639d915bd8fb42b3701cd91172 (patch)
tree7616b21fd6d02caef557be43a12e8d7979ee4997 /rest_framework
parentf269826a7d8a980536ffbce369be369c4df58d23 (diff)
parent51b7033e4aeeefe19012a77b09a0b23d4a52a5bc (diff)
downloaddjango-rest-framework-bc83dfece460b6639d915bd8fb42b3701cd91172.tar.bz2
Merge branch 'master' into 3.0-beta
Diffstat (limited to 'rest_framework')
-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
7 files changed, 125 insertions, 53 deletions
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)