From f573aaee4eabb9bf677c350219c8feb2333a6fa1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Nov 2014 17:25:05 +0000 Subject: List serializer no explicitly renders as 'not supported for HTML input' --- rest_framework/templates/rest_framework/horizontal/list_fieldset.html | 3 +++ rest_framework/templates/rest_framework/inline/list_fieldset.html | 1 + rest_framework/templates/rest_framework/vertical/list_fieldset.html | 1 + 3 files changed, 5 insertions(+) create mode 100644 rest_framework/templates/rest_framework/inline/list_fieldset.html 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 @@ {% endif %} + +
Lists are not currently supported in HTML input.
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 @@ +Lists are not currently supported in HTML input. 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 %} --> +Lists are not currently supported in HTML input.
-- cgit v1.2.3 From 5cda5ad18e2871c0e0d80c1aed448b56301db8b3 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Tue, 18 Nov 2014 13:06:08 -0500 Subject: Use the new build env on Travis faster vms, more ram, more cpu, better vm boot times official docs coming soon--- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) 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 -- cgit v1.2.3 From e49d22dbda5ac889ab89f277e17752c840819de2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 09:31:26 +0000 Subject: Allow blank choices to render. Closes #2071. --- rest_framework/fields.py | 2 ++ tests/test_fields.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) 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/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=[ -- cgit v1.2.3 From 6cb6510132b319c96b28bea732032aaf2d495895 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 12:15:05 +0000 Subject: Use translatable error strings. Refs #2063. --- rest_framework/exceptions.py | 74 +++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 18 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} + ) -- cgit v1.2.3 From 8586290df80ac8448d71cdb3326bc822c399cad1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Nov 2014 13:55:10 +0000 Subject: Apply defaults and requiredness to unique_together fields. Closes #2092. --- rest_framework/serializers.py | 83 +++++++++++++++++++++++++------------------ rest_framework/validators.py | 14 +++++++- tests/test_validators.py | 4 +-- 3 files changed, 64 insertions(+), 37 deletions(-) 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/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_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 = [