Commits

James Crasta committed 9002b8f

FieldList can now take validators on the entire FieldList. Closes #106.

Comments (0)

Files changed (3)

 - Tests complete in python 3.3.
 - Localization for ru, fr.
 - Minor fixes in documentation for clarity.
+- FieldList now can take validators on the entire FieldList.
 - ext.sqlalchemy model_form:
   * Fix issue in ColumnDefault conversion
   * Support Enum type
 from wtforms.compat import text_type
 
 
-PYTHON_VERSION = sys.version_info 
+PYTHON_VERSION = sys.version_info
 
 class DummyPostData(dict):
     def getlist(self, key):
         self.assertEqual(label('hello'), """<label for="test">hello</label>""")
         self.assertEqual(TextField('hi').bind(Form(), 'a').label.text, 'hi')
         if PYTHON_VERSION < (3,):
-            self.assertEqual(repr(label), "Label(u'test', u'Caption')") 
+            self.assertEqual(repr(label), "Label(u'test', u'Caption')")
         else:
-            self.assertEqual(repr(label), "Label('test', 'Caption')") 
+            self.assertEqual(repr(label), "Label('test', 'Caption')")
 
     def test_auto_label(self):
         t1 = TextField().bind(Form(), 'foo_bar')
         a = TextField(default='hello')
 
     def setUp(self):
-        self.field = self.F().a 
+        self.field = self.F().a
 
     def test_unbound_field(self):
         unbound = self.F.a
         self.assertEqual(str(self.field), str(self.field()))
 
     def test_unicode_coerce(self):
-        self.assertEqual(text_type(self.field), self.field()) 
+        self.assertEqual(text_type(self.field), self.field())
 
     def test_process_formdata(self):
         Field.process_formdata(self.field, [42])
         self.assertEqual(stoponly.errors, [])
 
         stopmessage = self._init_field("stopmessage")
-        self.assertEqual(stopmessage.errors, ["stop with message"]) 
+        self.assertEqual(stopmessage.errors, ["stop with message"])
 
     def test_post(self):
         a = self._init_field("p")
         self.assertEqual(a.data, ['foo', 'flaf', 'bar'])
         self.assertRaises(AssertionError, a.append_entry)
 
+    def test_validators(self):
+        def validator(form, field):
+            if field.data and field.data[0] == 'fail':
+                raise ValueError('fail')
+            elif len(field.data) > 2:
+                raise ValueError('too many')
+
+        F = make_form(a = FieldList(self.t, validators=[validator]))
+
+        # Case 1: length checking validators work as expected.
+        fdata = DummyPostData({'a-0': ['hello'], 'a-1': ['bye'], 'a-2': ['test3']})
+        form = F(fdata)
+        assert not form.validate()
+        self.assertEqual(form.a.errors, ['too many'])
+
+        # Case 2: checking a value within.
+        fdata['a-0'] = ['fail']
+        form = F(fdata)
+        assert not form.validate()
+        self.assertEqual(form.a.errors, ['fail'])
+
+        # Case 3: normal field validator still works
+        form = F(DummyPostData({'a-0': ['']}))
+        assert not form.validate()
+        self.assertEqual(form.a.errors, [[u'This field is required.']])
 
 
 if __name__ == '__main__':

wtforms/fields/core.py

 
         # Run validators
         if not stop_validation:
-            for validator in itertools.chain(self.validators, extra_validators):
-                try:
-                    validator(form, self)
-                except StopValidation as e:
-                    if e.args and e.args[0]:
-                        self.errors.append(e.args[0])
-                    stop_validation = True
-                    break
-                except ValueError as e:
-                    self.errors.append(e.args[0])
+            chain = itertools.chain(self.validators, extra_validators)
+            stop_validation = self._run_validation_chain(form, chain)
 
         # Call post_validate
         try:
 
         return len(self.errors) == 0
 
+    def _run_validation_chain(self, form, validators):
+        """
+        Run a validation chain, stopping if any validator raises StopValidation.
+
+        :param form: The Form instance this field beongs to.
+        :param validators: a sequence or iterable of validator callables.
+        :return: True if validation was stopped, False otherwise.
+        """
+        for validator in validators:
+            try:
+                validator(form, self)
+            except StopValidation as e:
+                if e.args and e.args[0]:
+                    self.errors.append(e.args[0])
+                return True
+            except ValueError as e:
+                self.errors.append(e.args[0])
+
+        return False
+
     def pre_validate(self, form):
         """
         Override if you need field-level validation. Runs before any other
         super(FieldList, self).__init__(label, validators, default=default, **kwargs)
         if self.filters:
             raise TypeError('FieldList does not accept any filters. Instead, define them on the enclosed field.')
-        if validators:
-            raise TypeError('FieldList does not accept any validators. Instead, define them on the enclosed field.')
         assert isinstance(unbound_field, UnboundField), 'Field must be unbound, not a field class'
         self.unbound_field = unbound_field
         self.min_entries = min_entries
                     yield int(k)
 
     def validate(self, form, extra_validators=tuple()):
+        """
+        Validate this FieldList.
+
+        Note that FieldList validation differs from normal field validation in
+        that FieldList validates all its enclosed fields first before running any
+        of its own validators.
+        """
         self.errors = []
-        success = True
+
+        # Run validators on all entries within
         for subfield in self.entries:
             if not subfield.validate(form):
-                success = False
                 self.errors.append(subfield.errors)
 
-        return success
+        chain = itertools.chain(self.validators, extra_validators)
+        stop_validation = self._run_validation_chain(form, chain)
+
+        return len(self.errors) == 0
 
     def populate_obj(self, obj, name):
         values = getattr(obj, name, None)