Skip to content

Commit 2bf6ee4

Browse files
committed
Introduce ValidationErrorMessage
`ValidationErrorMessage` is a string-like object that holds a code attribute. The code attribute has been removed from ValidationError to be able to maintain better backward compatibility. `ValidationErrorMessage` is abstracted in `ValidationError`'s constructor
1 parent df0d814 commit 2bf6ee4

File tree

8 files changed

+118
-66
lines changed

8 files changed

+118
-66
lines changed

rest_framework/authtoken/serializers.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,18 @@ def validate(self, attrs):
2020
msg = _('User account is disabled.')
2121
raise serializers.ValidationError(
2222
msg,
23-
code='authorization'
24-
)
23+
code='authorization')
2524
else:
2625
msg = _('Unable to log in with provided credentials.')
2726
raise serializers.ValidationError(
2827
msg,
29-
code='authorization'
30-
)
28+
code='authorization')
29+
3130
else:
3231
msg = _('Must include "username" and "password".')
3332
raise serializers.ValidationError(
3433
msg,
35-
code='authorization'
36-
)
34+
code='authorization')
3735

3836
attrs['user'] = user
3937
return attrs

rest_framework/exceptions.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __str__(self):
6161
def build_error_from_django_validation_error(exc_info):
6262
code = getattr(exc_info, 'code', None) or 'invalid'
6363
return [
64-
ValidationError(msg, code=code)
64+
ValidationErrorMessage(msg, code=code)
6565
for msg in exc_info.messages
6666
]
6767

@@ -73,20 +73,30 @@ def build_error_from_django_validation_error(exc_info):
7373
# from rest_framework import serializers
7474
# raise serializers.ValidationError('Value was invalid')
7575

76+
class ValidationErrorMessage(six.text_type):
77+
code = None
78+
79+
def __new__(cls, string, code=None, *args, **kwargs):
80+
self = super(ValidationErrorMessage, cls).__new__(
81+
cls, string, *args, **kwargs)
82+
83+
self.code = code
84+
return self
85+
86+
7687
class ValidationError(APIException):
7788
status_code = status.HTTP_400_BAD_REQUEST
7889

7990
def __init__(self, detail, code=None):
91+
# If code is there, this means we are dealing with a message.
92+
if code and not isinstance(detail, ValidationErrorMessage):
93+
detail = ValidationErrorMessage(detail, code=code)
94+
8095
# For validation errors the 'detail' key is always required.
8196
# The details should always be coerced to a list if not already.
8297
if not isinstance(detail, dict) and not isinstance(detail, list):
8398
detail = [detail]
84-
elif isinstance(detail, dict) or (detail and isinstance(detail[0], ValidationError)):
85-
assert code is None, (
86-
'The `code` argument must not be set for compound errors.')
87-
88-
self.detail = detail
89-
self.code = code
99+
self.detail = _force_text_recursive(detail)
90100

91101
def __str__(self):
92102
return six.text_type(self.detail)

rest_framework/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def run_validators(self, value):
509509
# attempting to accumulate a list of errors.
510510
if isinstance(exc.detail, dict):
511511
raise
512-
errors.append(ValidationError(exc.detail, code=exc.code))
512+
errors.extend(exc.detail)
513513
except DjangoValidationError as exc:
514514
errors.extend(
515515
build_error_from_django_validation_error(exc)

rest_framework/response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self, data=None, status=None,
3838
'`.error`. representation.'
3939
)
4040
raise AssertionError(msg)
41+
4142
self.data = data
4243
self.template_name = template_name
4344
self.exception = exception

rest_framework/serializers.py

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from rest_framework import exceptions
2626
from rest_framework.compat import JSONField as ModelJSONField
2727
from rest_framework.compat import postgres_fields, unicode_to_repr
28+
from rest_framework.exceptions import ValidationErrorMessage
2829
from rest_framework.utils import model_meta
2930
from rest_framework.utils.field_mapping import (
3031
ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs,
@@ -220,14 +221,7 @@ def is_valid(self, raise_exception=False):
220221
self._errors = {}
221222

222223
if self._errors and raise_exception:
223-
return_errors = None
224-
if isinstance(self._errors, list):
225-
return_errors = ReturnList(self._errors, serializer=self)
226-
elif isinstance(self._errors, dict):
227-
return_errors = ReturnDict(self._errors, serializer=self)
228-
229-
raise ValidationError(return_errors)
230-
224+
raise ValidationError(self.errors)
231225
return not bool(self._errors)
232226

233227
@property
@@ -251,42 +245,12 @@ def data(self):
251245
self._data = self.get_initial()
252246
return self._data
253247

254-
def _transform_to_legacy_errors(self, errors_to_transform):
255-
# Do not mutate `errors_to_transform` here.
256-
errors = ReturnDict(serializer=self)
257-
for field_name, values in errors_to_transform.items():
258-
if isinstance(values, list):
259-
errors[field_name] = values
260-
continue
261-
262-
if isinstance(values.detail, list):
263-
errors[field_name] = []
264-
for value in values.detail:
265-
if isinstance(value, ValidationError):
266-
errors[field_name].extend(value.detail)
267-
elif isinstance(value, list):
268-
errors[field_name].extend(value)
269-
else:
270-
errors[field_name].append(value)
271-
272-
elif isinstance(values.detail, dict):
273-
errors[field_name] = {}
274-
for sub_field_name, value in values.detail.items():
275-
errors[field_name][sub_field_name] = []
276-
for validation_error in value:
277-
errors[field_name][sub_field_name].extend(validation_error.detail)
278-
return errors
279-
280248
@property
281249
def errors(self):
282250
if not hasattr(self, '_errors'):
283251
msg = 'You must call `.is_valid()` before accessing `.errors`.'
284252
raise AssertionError(msg)
285-
286-
if isinstance(self._errors, list):
287-
return map(self._transform_to_legacy_errors, self._errors)
288-
else:
289-
return self._transform_to_legacy_errors(self._errors)
253+
return self._errors
290254

291255
@property
292256
def validated_data(self):
@@ -461,7 +425,7 @@ def to_internal_value(self, data):
461425
message = self.error_messages['invalid'].format(
462426
datatype=type(data).__name__
463427
)
464-
error = ValidationError(message, code='invalid')
428+
error = ValidationErrorMessage(message, code='invalid')
465429
raise ValidationError({
466430
api_settings.NON_FIELD_ERRORS_KEY: [error]
467431
})
@@ -478,7 +442,7 @@ def to_internal_value(self, data):
478442
if validate_method is not None:
479443
validated_value = validate_method(validated_value)
480444
except ValidationError as exc:
481-
errors[field.field_name] = exc
445+
errors[field.field_name] = exc.detail
482446
except DjangoValidationError as exc:
483447
errors[field.field_name] = (
484448
exceptions.build_error_from_django_validation_error(exc)
@@ -621,7 +585,7 @@ def to_internal_value(self, data):
621585
message = self.error_messages['not_a_list'].format(
622586
input_type=type(data).__name__
623587
)
624-
error = ValidationError(
588+
error = ValidationErrorMessage(
625589
message,
626590
code='not_a_list'
627591
)
@@ -630,9 +594,11 @@ def to_internal_value(self, data):
630594
})
631595

632596
if not self.allow_empty and len(data) == 0:
633-
message = self.error_messages['empty']
597+
message = ValidationErrorMessage(
598+
self.error_messages['empty'],
599+
code='empty_not_allowed')
634600
raise ValidationError({
635-
api_settings.NON_FIELD_ERRORS_KEY: [ValidationError(message, code='empty_not_allowed')]
601+
api_settings.NON_FIELD_ERRORS_KEY: [message]
636602
})
637603

638604
ret = []

rest_framework/validators.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.utils.translation import ugettext_lazy as _
1313

1414
from rest_framework.compat import unicode_to_repr
15-
from rest_framework.exceptions import ValidationError
15+
from rest_framework.exceptions import ValidationError, ValidationErrorMessage
1616
from rest_framework.utils.representation import smart_repr
1717

1818

@@ -120,9 +120,10 @@ def enforce_required_fields(self, attrs):
120120
return
121121

122122
missing = {
123-
field_name: ValidationError(
123+
field_name: ValidationErrorMessage(
124124
self.missing_message,
125125
code='required')
126+
126127
for field_name in self.fields
127128
if field_name not in attrs
128129
}
@@ -168,8 +169,9 @@ def __call__(self, attrs):
168169
]
169170
if None not in checked_values and qs_exists(queryset):
170171
field_names = ', '.join(self.fields)
171-
raise ValidationError(self.message.format(field_names=field_names),
172-
code='unique')
172+
raise ValidationError(
173+
self.message.format(field_names=field_names),
174+
code='unique')
173175

174176
def __repr__(self):
175177
return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
@@ -207,7 +209,7 @@ def enforce_required_fields(self, attrs):
207209
'required' state on the fields they are applied to.
208210
"""
209211
missing = {
210-
field_name: ValidationError(
212+
field_name: ValidationErrorMessage(
211213
self.missing_message,
212214
code='required')
213215
for field_name in [self.field, self.date_field]
@@ -235,8 +237,9 @@ def __call__(self, attrs):
235237
queryset = self.exclude_current_instance(attrs, queryset)
236238
if qs_exists(queryset):
237239
message = self.message.format(date_field=self.date_field)
238-
error = ValidationError(message, code='unique')
239-
raise ValidationError({self.field: error})
240+
raise ValidationError({
241+
self.field: ValidationErrorMessage(message, code='unique'),
242+
})
240243

241244
def __repr__(self):
242245
return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % (

rest_framework/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def exception_handler(exc, context):
7171
headers['Retry-After'] = '%d' % exc.wait
7272

7373
if isinstance(exc.detail, (list, dict)):
74-
data = exc.detail.serializer.errors
74+
data = exc.detail
7575
else:
7676
data = {'detail': exc.detail}
7777

tests/test_validation_error.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from django.test import TestCase
2+
3+
from rest_framework import serializers, status
4+
from rest_framework.decorators import api_view
5+
from rest_framework.response import Response
6+
from rest_framework.settings import api_settings
7+
from rest_framework.test import APIRequestFactory
8+
from rest_framework.views import APIView
9+
10+
factory = APIRequestFactory()
11+
12+
13+
class ExampleSerializer(serializers.Serializer):
14+
char = serializers.CharField()
15+
integer = serializers.IntegerField()
16+
17+
18+
class ErrorView(APIView):
19+
def get(self, request, *args, **kwargs):
20+
ExampleSerializer(data={}).is_valid(raise_exception=True)
21+
22+
23+
@api_view(['GET'])
24+
def error_view(request):
25+
ExampleSerializer(data={}).is_valid(raise_exception=True)
26+
27+
28+
class TestValidationErrorWithCode(TestCase):
29+
def setUp(self):
30+
self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
31+
32+
def exception_handler(exc, request):
33+
return_errors = {}
34+
for field_name, errors in exc.detail.items():
35+
return_errors[field_name] = []
36+
for error in errors:
37+
return_errors[field_name].append({
38+
'code': error.code,
39+
'message': error
40+
})
41+
42+
return Response(return_errors, status=status.HTTP_400_BAD_REQUEST)
43+
44+
api_settings.EXCEPTION_HANDLER = exception_handler
45+
46+
self.expected_response_data = {
47+
'char': [{
48+
'message': 'This field is required.',
49+
'code': 'required',
50+
}],
51+
'integer': [{
52+
'message': 'This field is required.',
53+
'code': 'required'
54+
}],
55+
}
56+
57+
def tearDown(self):
58+
api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER
59+
60+
def test_class_based_view_exception_handler(self):
61+
view = ErrorView.as_view()
62+
63+
request = factory.get('/', content_type='application/json')
64+
response = view(request)
65+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
66+
self.assertEqual(response.data, self.expected_response_data)
67+
68+
def test_function_based_view_exception_handler(self):
69+
view = error_view
70+
71+
request = factory.get('/', content_type='application/json')
72+
response = view(request)
73+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
74+
self.assertEqual(response.data, self.expected_response_data)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy