mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-23 01:57:00 +03:00
Merge branch 'master' into 3.0-beta
This commit is contained in:
commit
bc83dfece4
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}
|
||||||
|
)
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<span>Lists are not currently supported in HTML input.</span>
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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=[
|
||||||
|
|
|
@ -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'))>]
|
||||||
""")
|
""")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user