Merge branch 'master' into 3.0-beta

This commit is contained in:
Tom Christie 2014-11-19 14:04:31 +00:00
commit bc83dfece4
11 changed files with 134 additions and 54 deletions

View File

@ -2,6 +2,8 @@ language: python
python: 2.7
sudo: false
env:
- TOX_ENV=flake8
- TOX_ENV=py3.4-django1.7

View File

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

View File

@ -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}
)

View File

@ -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)]

View File

@ -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
])
unique_constraint_names -= set([None])
# Get the model field that is refered too.
date_field = model._meta.get_field(date_field_name)
# 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)
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
# 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:
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
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

View File

@ -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>

View File

@ -0,0 +1 @@
<span>Lists are not currently supported in HTML input.</span>

View File

@ -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>

View File

@ -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)

View File

@ -793,7 +793,8 @@ class TestChoiceField(FieldValues):
'amazing': ['`amazing` is not a valid choice.']
}
outputs = {
'good': 'good'
'good': 'good',
'': ''
}
field = serializers.ChoiceField(
choices=[

View File

@ -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'))>]
""")