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 python: 2.7
sudo: false
env: env:
- TOX_ENV=flake8 - TOX_ENV=flake8
- TOX_ENV=py3.4-django1.7 - 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. 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. ## 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. 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`) (`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
""" """
from __future__ import unicode_literals 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 import status
from rest_framework.compat import force_text
import math import math
@ -15,10 +19,13 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties. Subclasses should provide `.status_code` and `.default_detail` properties.
""" """
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR 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): 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): def __str__(self):
return self.detail return self.detail
@ -31,6 +38,19 @@ class APIException(Exception):
# from rest_framework import serializers # from rest_framework import serializers
# raise serializers.ValidationError('Value was invalid') # 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): class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST 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. # The details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list): if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail] detail = [detail]
self.detail = detail self.detail = force_text_recursive(detail)
def __str__(self): def __str__(self):
return str(self.detail) return str(self.detail)
@ -47,59 +67,77 @@ class ValidationError(APIException):
class ParseError(APIException): class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.' default_detail = _('Malformed request.')
class AuthenticationFailed(APIException): class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.' default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException): class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Authentication credentials were not provided.' default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException): class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN 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): class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED 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): 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): class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE 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): 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 self.available_renderers = available_renderers
class UnsupportedMediaType(APIException): class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE 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): 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): class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = 'Request was throttled.' default_detail = _('Request was throttled.')
extra_detail = " Expected available in %d second%s." extra_detail = ungettext_lazy(
'Expected available in %(wait)d second.',
'Expected available in %(wait)d seconds.',
'wait'
)
def __init__(self, wait=None, detail=None): 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: if wait is None:
self.detail = detail or self.default_detail
self.wait = None self.wait = None
else: 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.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) self.fail('invalid_choice', input=data)
def to_representation(self, value): def to_representation(self, value):
if value in ('', None):
return value
return self.choice_strings_to_values[six.text_type(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 # Determine if we need any additional `HiddenField` or extra keyword
# arguments to deal with `unique_for` dates that are required to # arguments to deal with `unique_for` dates that are required to
# be in the input data in order to validate it. # 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(): for model_field_name, field_name in model_field_mapping.items():
try: try:
model_field = model._meta.get_field(model_field_name) model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist: except FieldDoesNotExist:
continue continue
# Deal with each of the `unique_for_*` cases. # Include each of the `unique_for_*` field names.
for date_field_name in ( unique_constraint_names = set([
model_field.unique_for_date, model_field.unique_for_date,
model_field.unique_for_month, model_field.unique_for_month,
model_field.unique_for_year model_field.unique_for_year
): ])
if date_field_name is None: unique_constraint_names -= set([None])
continue
# Get the model field that is refered too. # Include each of the `unique_together` field names,
date_field = model._meta.get_field(date_field_name) # 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: # Now we have all the field names that have uniqueness constraints
default = CreateOnlyDefault(timezone.now) # applied, we can add the extra 'required=...' or 'default=...'
elif date_field.auto_now: # arguments that are appropriate to these fields, or add a `HiddenField` for it.
default = timezone.now for unique_constraint_name in unique_constraint_names:
elif date_field.has_default(): # Get the model field that is refered too.
default = model_field.default 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: else:
default = empty if 'default' not in extra_kwargs[unique_constraint_name]:
extra_kwargs[unique_constraint_name]['default'] = default
if date_field_name in model_field_mapping: elif default is not empty:
# The corresponding date field is present in the serializer # The corresponding field is not present in the,
if date_field_name not in extra_kwargs: # serializer. We have a default to use for it, so
extra_kwargs[date_field_name] = {} # add in a hidden field that populates it.
if default is empty: hidden_fields[unique_constraint_name] = HiddenField(default=default)
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)
# Now determine the fields that should be included on the serializer. # Now determine the fields that should be included on the serializer.
for field_name in fields: for field_name in fields:
@ -838,12 +849,16 @@ class ModelSerializer(Serializer):
'validators', 'queryset' 'validators', 'queryset'
]: ]:
kwargs.pop(attr, None) kwargs.pop(attr, None)
if extras.get('default') and kwargs.get('required') is False:
kwargs.pop('required')
kwargs.update(extras) kwargs.update(extras)
# Create the serializer field. # Create the serializer field.
ret[field_name] = field_cls(**kwargs) 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 ret[field_name] = field
return ret 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> <legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
</div> </div>
{% endif %} {% endif %}
<!--
<ul> <ul>
{% for child in field.value %} {% for child in field.value %}
<li>TODO</li> <li>TODO</li>
{% endfor %} {% endfor %}
</ul> </ul>
-->
<p>Lists are not currently supported in HTML input.</p>
</fieldset> </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() %} {% for field_item in field.value.field_items.values() %}
{{ renderer.render_field(field_item, layout=layout) }} {{ renderer.render_field(field_item, layout=layout) }}
{% endfor %} --> {% endfor %} -->
<p>Lists are not currently supported in HTML input.</p>
</fieldset> </fieldset>

View File

@ -93,6 +93,9 @@ class UniqueTogetherValidator:
The `UniqueTogetherValidator` always forces an implied 'required' The `UniqueTogetherValidator` always forces an implied 'required'
state on the fields it applies to. state on the fields it applies to.
""" """
if self.instance is not None:
return
missing = dict([ missing = dict([
(field_name, self.missing_message) (field_name, self.missing_message)
for field_name in self.fields for field_name in self.fields
@ -105,8 +108,17 @@ class UniqueTogetherValidator:
""" """
Filter the queryset to all instances matching the given attributes. 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([ 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) return queryset.filter(**filter_kwargs)

View File

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

View File

@ -88,8 +88,8 @@ class TestUniquenessTogetherValidation(TestCase):
expected = dedent(""" expected = dedent("""
UniquenessTogetherSerializer(): UniquenessTogetherSerializer():
id = IntegerField(label='ID', read_only=True) id = IntegerField(label='ID', read_only=True)
race_name = CharField(max_length=100) race_name = CharField(max_length=100, required=True)
position = IntegerField() position = IntegerField(required=True)
class Meta: class Meta:
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>] validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
""") """)