diff options
| author | Tom Christie | 2014-10-10 14:16:09 +0100 | 
|---|---|---|
| committer | Tom Christie | 2014-10-10 14:16:09 +0100 | 
| commit | d9a199ca0ddf92f999aa37b396596d0e3e0a26d9 (patch) | |
| tree | c7521c3c046e5f97e52bab51aef8a5367ad52f6e /rest_framework | |
| parent | a0e852a4d52558db93209b4616f030b4ae2dcedb (diff) | |
| download | django-rest-framework-d9a199ca0ddf92f999aa37b396596d0e3e0a26d9.tar.bz2 | |
exceptions.ValidationFailed, not Django's ValidationError
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/authtoken/serializers.py | 8 | ||||
| -rw-r--r-- | rest_framework/exceptions.py | 14 | ||||
| -rw-r--r-- | rest_framework/fields.py | 21 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 60 | ||||
| -rw-r--r-- | rest_framework/views.py | 27 | 
5 files changed, 76 insertions, 54 deletions
| diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index c2c456de..a808d0a3 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -1,7 +1,7 @@  from django.contrib.auth import authenticate  from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers +from rest_framework import exceptions, serializers  class AuthTokenSerializer(serializers.Serializer): @@ -18,13 +18,13 @@ class AuthTokenSerializer(serializers.Serializer):              if user:                  if not user.is_active:                      msg = _('User account is disabled.') -                    raise serializers.ValidationError(msg) +                    raise exceptions.ValidationFailed(msg)              else:                  msg = _('Unable to log in with provided credentials.') -                raise serializers.ValidationError(msg) +                raise exceptions.ValidationFailed(msg)          else:              msg = _('Must include "username" and "password"') -            raise serializers.ValidationError(msg) +            raise exceptions.ValidationFailed(msg)          attrs['user'] = user          return attrs diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 06b5e8a2..b7c2d16d 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -24,6 +24,20 @@ class APIException(Exception):          return self.detail +class ValidationFailed(APIException): +    status_code = status.HTTP_400_BAD_REQUEST + +    def __init__(self, detail): +        # For validation errors the 'detail' key is always required. +        # 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 + +    def __str__(self): +        return str(self.detail) + +  class ParseError(APIException):      status_code = status.HTTP_400_BAD_REQUEST      default_detail = 'Malformed request.' diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 7053acee..b881ad13 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,7 +1,8 @@ -from django import forms  from django.conf import settings  from django.core import validators -from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError as DjangoValidationError +from django.forms import ImageField as DjangoImageField  from django.utils import six, timezone  from django.utils.datastructures import SortedDict  from django.utils.dateparse import parse_date, parse_datetime, parse_time @@ -12,6 +13,7 @@ from rest_framework.compat import (      smart_text, EmailValidator, MinValueValidator, MaxValueValidator,      MinLengthValidator, MaxLengthValidator, URLValidator  ) +from rest_framework.exceptions import ValidationFailed  from rest_framework.settings import api_settings  from rest_framework.utils import html, representation, humanize_datetime  import copy @@ -98,7 +100,7 @@ NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'  NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'  USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'  MISSING_ERROR_MESSAGE = ( -    'ValidationError raised by `{class_name}`, but error key `{key}` does ' +    'ValidationFailed raised by `{class_name}`, but error key `{key}` does '      'not exist in the `error_messages` dictionary.'  ) @@ -263,7 +265,7 @@ class Field(object):      def run_validators(self, value):          """          Test the given value against all the validators on the field, -        and either raise a `ValidationError` or simply return. +        and either raise a `ValidationFailed` or simply return.          """          errors = []          for validator in self.validators: @@ -271,10 +273,12 @@ class Field(object):                  validator.serializer_field = self              try:                  validator(value) -            except ValidationError as exc: +            except ValidationFailed as exc: +                errors.extend(exc.detail) +            except DjangoValidationError as exc:                  errors.extend(exc.messages)          if errors: -            raise ValidationError(errors) +            raise ValidationFailed(errors)      def validate(self, value):          pass @@ -301,7 +305,8 @@ class Field(object):              class_name = self.__class__.__name__              msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)              raise AssertionError(msg) -        raise ValidationError(msg.format(**kwargs)) +        message_string = msg.format(**kwargs) +        raise ValidationFailed(message_string)      @property      def root(self): @@ -946,7 +951,7 @@ class ImageField(FileField):      }      def __init__(self, *args, **kwargs): -        self._DjangoImageField = kwargs.pop('_DjangoImageField', forms.ImageField) +        self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)          super(ImageField, self).__init__(*args, **kwargs)      def to_internal_value(self, data): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 3bd7b17b..2f683562 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,10 +10,11 @@ python primitives.  2. The process of marshalling between python primitives and request and  response content is handled by parsers and renderers.  """ -from django.core.exceptions import ImproperlyConfigured, ValidationError +from django.core.exceptions import ImproperlyConfigured  from django.db import models  from django.utils import six  from django.utils.datastructures import SortedDict +from rest_framework.exceptions import ValidationFailed  from rest_framework.fields import empty, set_value, Field, SkipField  from rest_framework.settings import api_settings  from rest_framework.utils import html, model_meta, representation @@ -100,14 +101,14 @@ class BaseSerializer(Field):          if not hasattr(self, '_validated_data'):              try:                  self._validated_data = self.run_validation(self._initial_data) -            except ValidationError as exc: +            except ValidationFailed as exc:                  self._validated_data = {} -                self._errors = exc.message_dict +                self._errors = exc.detail              else:                  self._errors = {}          if self._errors and raise_exception: -            raise ValidationError(self._errors) +            raise ValidationFailed(self._errors)          return not bool(self._errors) @@ -175,24 +176,34 @@ class BoundField(object):      def __getattr__(self, attr_name):          return getattr(self._field, attr_name) +    @property +    def _proxy_class(self): +        return self._field.__class__ + +    def __repr__(self): +        return '<%s value=%s errors=%s>' % ( +            self.__class__.__name__, self.value, self.errors +        ) + + +class NestedBoundField(BoundField): +    """ +    This BoundField additionally implements __iter__ and __getitem__ +    in order to support nested bound fields. This class is the type of +    BoundField that is used for serializer fields. +    """      def __iter__(self):          for field in self.fields.values():              yield self[field.field_name]      def __getitem__(self, key): -        assert hasattr(self, 'fields'), '"%s" is not a nested field. Cannot perform indexing.' % self.name          field = self.fields[key]          value = self.value.get(key) if self.value else None          error = self.errors.get(key) if self.errors else None +        if isinstance(field, Serializer): +            return NestedBoundField(field, value, error, prefix=self.name + '.')          return BoundField(field, value, error, prefix=self.name + '.') -    @property -    def _proxy_class(self): -        return self._field.__class__ - -    def __repr__(self): -        return '<%s value=%s errors=%s>' % (self.__class__.__name__, self.value, self.errors) -  class BindingDict(object):      """ @@ -308,7 +319,7 @@ class Serializer(BaseSerializer):              return None          if not isinstance(data, dict): -            raise ValidationError({ +            raise ValidationFailed({                  api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data']              }) @@ -317,9 +328,9 @@ class Serializer(BaseSerializer):              self.run_validators(value)              value = self.validate(value)              assert value is not None, '.validate() should return the validated data' -        except ValidationError as exc: -            raise ValidationError({ -                api_settings.NON_FIELD_ERRORS_KEY: exc.messages +        except ValidationFailed as exc: +            raise ValidationFailed({ +                api_settings.NON_FIELD_ERRORS_KEY: exc.detail              })          return value @@ -338,15 +349,15 @@ class Serializer(BaseSerializer):                  validated_value = field.run_validation(primitive_value)                  if validate_method is not None:                      validated_value = validate_method(validated_value) -            except ValidationError as exc: -                errors[field.field_name] = exc.messages +            except ValidationFailed as exc: +                errors[field.field_name] = exc.detail              except SkipField:                  pass              else:                  set_value(ret, field.source_attrs, validated_value)          if errors: -            raise ValidationError(errors) +            raise ValidationFailed(errors)          return ret @@ -385,6 +396,8 @@ class Serializer(BaseSerializer):          field = self.fields[key]          value = self.data.get(key)          error = self.errors.get(key) if hasattr(self, '_errors') else None +        if isinstance(field, Serializer): +            return NestedBoundField(field, value, error)          return BoundField(field, value, error) @@ -538,9 +551,12 @@ class ModelSerializer(Serializer):          ret = SortedDict()          model = getattr(self.Meta, 'model')          fields = getattr(self.Meta, 'fields', None) +        exclude = getattr(self.Meta, 'exclude', None)          depth = getattr(self.Meta, 'depth', 0)          extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) +        assert not fields and exclude,  "Cannot set both 'fields' and 'exclude'." +          extra_kwargs = self._include_additional_options(extra_kwargs)          # Retrieve metadata about fields & relationships on the model class. @@ -551,12 +567,6 @@ class ModelSerializer(Serializer):              fields = self._get_default_field_names(declared_fields, info)              exclude = getattr(self.Meta, 'exclude', None)              if exclude is not None: -                warnings.warn( -                    "The `Meta.exclude` option is pending deprecation. " -                    "Use the explicit `Meta.fields` instead.", -                    PendingDeprecationWarning, -                    stacklevel=3 -                )                  for field_name in exclude:                      fields.remove(field_name) diff --git a/rest_framework/views.py b/rest_framework/views.py index 979229eb..292431c8 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -3,7 +3,7 @@ Provides an APIView class that is the base of all views in REST framework.  """  from __future__ import unicode_literals -from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS +from django.core.exceptions import PermissionDenied  from django.http import Http404  from django.views.decorators.csrf import csrf_exempt  from rest_framework import status, exceptions @@ -63,27 +63,20 @@ def exception_handler(exc):          if getattr(exc, 'wait', None):              headers['Retry-After'] = '%d' % exc.wait -        return Response({'detail': exc.detail}, -                        status=exc.status_code, -                        headers=headers) +        if isinstance(exc.detail, (list, dict)): +            data = exc.detail +        else: +            data = {'detail': exc.detail} -    elif isinstance(exc, ValidationError): -        # ValidationErrors may include the non-field key named '__all__'. -        # When returning a response we map this to a key name that can be -        # modified in settings. -        if NON_FIELD_ERRORS in exc.message_dict: -            errors = exc.message_dict.pop(NON_FIELD_ERRORS) -            exc.message_dict[api_settings.NON_FIELD_ERRORS_KEY] = errors -        return Response(exc.message_dict, -                        status=status.HTTP_400_BAD_REQUEST) +        return Response(data, status=exc.status_code, headers=headers)      elif isinstance(exc, Http404): -        return Response({'detail': 'Not found'}, -                        status=status.HTTP_404_NOT_FOUND) +        data = {'detail': 'Not found'} +        return Response(data, status=status.HTTP_404_NOT_FOUND)      elif isinstance(exc, PermissionDenied): -        return Response({'detail': 'Permission denied'}, -                        status=status.HTTP_403_FORBIDDEN) +        data = {'detail': 'Permission denied'} +        return Response(data, status=status.HTTP_403_FORBIDDEN)      # Note: Unhandled exceptions will raise a 500 error.      return None | 
