diff --git a/pilo/fields.py b/pilo/fields.py index b8d96c2..52a0742 100644 --- a/pilo/fields.py +++ b/pilo/fields.py @@ -79,6 +79,29 @@ def field1(self, value): ] +class FormError(ValueError): + + def __init__(self, *field_errors): + self.field_errors = field_errors + super(FormError, self).__init__(self.message) + + def __str__(self): + return self.message + + def __repr__(self): + return 'FormError({0})'.format( + ', '.join(exc.__class__.__name__ for exc in self.field_errors) + ) + + @property + def message(self): + msg = '\n* '.join( + '{0}: {1}'.format(exc.__class__.__name__, str(exc)) + for exc in self.field_errors + ) + return '\n* {0}\n'.format(msg) + + class FieldError(ValueError): def __init__(self, message, field): @@ -118,9 +141,12 @@ def invalid(self, violation): class RaiseErrors(list, Errors): - def __call__(self, *ex): - self.extend(ex) - raise ex[0] + def __call__(self, *excs): + self.extend(excs) + field_errors = [e for e in self if isinstance(e, FieldError)] + if field_errors: + raise FormError(*field_errors) + raise self[0] class CollectErrors(list, Errors): @@ -137,7 +163,7 @@ class CreatedCountMixin(object): _created_count = 0 - def __init__(self): + def __init__(self): CreatedCountMixin._created_count += 1 self._count = CreatedCountMixin._created_count @@ -1541,7 +1567,7 @@ def __init__(self, *args, **kwargs): if src: errors = self.map(src) if errors: - raise errors[0] + RaiseErrors()(*errors) def _map_source(self, obj): return DefaultSource(obj) @@ -1638,7 +1664,7 @@ def map(self, src=None, tags=None, reset=False, unmapped='ignore', error='collec if error == 'collect': return errors if errors: - raise errors[0] + RaiseErrors()(*errors) return self def has(self, field): diff --git a/tests/test_fields.py b/tests/test_fields.py index d933d67..1c11081 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -3,6 +3,7 @@ import re import pilo +from pilo.fields import Integer, List, String, SubForm from tests import TestCase @@ -374,3 +375,143 @@ def send_to_zoo(self): ]: obj = Animal.type.cast(desc)(desc) self.assertIsInstance(obj, cls) + + +class TestFormExceptions(TestCase): + + def test_exceptions(self): + + class DatingProfile(pilo.Form): + + genders = ['male', 'female', 'neutral'] + + name = String() + email = String() + postal_code = String(length=5) + blurb = String(max_length=100) + gender = String(choices=genders) + sexual_preferences = List(String(choices=genders)) + likes = List(String()) + + profile_with_one_error = dict( + name='William Henry Cavendish III', + email='whc@example.org', + postal_code='9021', # Invalid length + blurb='I am a test fixture', + gender='male', + sexual_preferences=['female', 'neutral'], + likes=['croquet', 'muesli', 'ruses', 'umbrellas', 'wenches'], + ) + profile_with_two_errors = dict( + name='William Henry Cavendish III', + email='whc@example.org', + postal_code='9021', # Invalid postal code + blurb='I am a test fixture', + gender='male', + sexual_preferences=['female', 'neutral'], + # Likes parameter missing + ) + profile_with_three_errors = dict( + name='William Henry Cavendish III', + email='whc@example.org', + postal_code='9021', # Invalid length + blurb='I am a test fixture', + gender='male', + sexual_preferences=['alien'], # Invalid preference + # likes is missing + ) + + with self.assertRaises(pilo.fields.FormError) as ctx: + DatingProfile(profile_with_one_error) + + self.assertEquals( + ctx.exception.message, + '\n' + '* Invalid: postal_code - "9021" must have length >= 5' + '\n' + ) + with self.assertRaises(pilo.fields.FormError) as ctx: + DatingProfile(profile_with_two_errors) + + self.assertEquals( + ctx.exception.message, + '\n' + '* Invalid: postal_code - "9021" must have length >= 5' + '\n' + '* Missing: likes - missing' + '\n' + ) + with self.assertRaises(pilo.fields.FormError) as ctx: + DatingProfile(profile_with_three_errors) + + self.assertEquals( + ctx.exception.message, + '\n' + '* Invalid: postal_code - "9021" must have length >= 5' + '\n' + '* Invalid: sexual_preferences[0] - "alien" is not one of "male", ' + '"female", "neutral"' + '\n' + '* Missing: likes - missing' + '\n' + ) + + def test_exceptions_in_nested_forms(self): + + class DatingProfile(pilo.Form): + + genders = ['male', 'female', 'neutral'] + + name = String() + email = String() + postal_code = String(length=5) + blurb = String(max_length=100) + gender = String(choices=genders) + sexual_preferences = List(String(choices=genders)) + likes = List(String()) + + class Matches(pilo.Form): + + similarity = Integer() + candidates = List(SubForm(DatingProfile)) + + with self.assertRaises(pilo.fields.FormError) as ctx: + Matches( + similarity="Not an integer", + candidates=[ + dict( + name='William Henry Cavendish III', + email='whc@example.org', + postal_code='9021', # Invalid postal code + blurb='I am a test fixture', + gender='male', + sexual_preferences=['female', 'neutral'], + # Likes parameter missing + ), + dict(), + ] + ) + self.assertEquals( + ctx.exception.message, + '\n' + '* Invalid: similarity - "Not an integer" is not an integer' + '\n' + '* Invalid: candidates[0].postal_code - "9021" must have length >= 5' + '\n' + '* Missing: candidates[0].likes - missing' + '\n' + '* Missing: candidates[1].name - missing' + '\n' + '* Missing: candidates[1].email - missing' + '\n' + '* Missing: candidates[1].postal_code - missing' + '\n' + '* Missing: candidates[1].blurb - missing' + '\n' + '* Missing: candidates[1].gender - missing' + '\n' + '* Missing: candidates[1].sexual_preferences - missing' + '\n' + '* Missing: candidates[1].likes - missing' + '\n' + )