From 02e0b88cbdd52b38e0125983a2297919bfc8202f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Fri, 24 Apr 2020 18:36:35 +0200 Subject: [PATCH 1/2] black --- src/wtforms/fields/core.py | 2 +- tests/test_fields.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py index 0fefcad4d..8415ca932 100644 --- a/src/wtforms/fields/core.py +++ b/src/wtforms/fields/core.py @@ -398,7 +398,7 @@ def bind(self, form, name, prefix="", translations=None, **kwargs): def __repr__(self): return "".format( - self.field_class.__name__, self.args, self.kwargs, + self.field_class.__name__, self.args, self.kwargs ) diff --git a/tests/test_fields.py b/tests/test_fields.py index 15fb5e11c..cde794501 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1154,7 +1154,7 @@ def _build_value(self, key, form_input, expected_html, data=unset_value): data = form_input if expected_html.startswith("type="): expected_html = ''.format( - key, key, expected_html, form_input, + key, key, expected_html, form_input ) return { "key": key, From 22636b55eda9300b549c8bbaae6f9ae31595d445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Fri, 24 Apr 2020 18:50:46 +0200 Subject: [PATCH 2/2] Implemented form-level errors --- CHANGES.rst | 1 + docs/forms.rst | 9 +++++++++ src/wtforms/form.py | 7 ++++++- tests/test_form.py | 25 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ad5b983b3..5a4f49890 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -25,6 +25,7 @@ Unreleased :pr:`608` - Choices shortcut for :class:`~fields.core.SelectMultipleField`. :issue:`603` :pr:`605` +- Forms can have form-level errors. :issue:`55` :pr:`595` Version 2.3.1 diff --git a/docs/forms.rst b/docs/forms.rst index e6b32f81b..7de7d6533 100644 --- a/docs/forms.rst +++ b/docs/forms.rst @@ -47,6 +47,15 @@ The Form class A dict containing a list of errors for each field. Empty if the form hasn't been validated, or there were no errors. + If present, the key ``None`` contains the content of + :attr:`~wtforms.forms.Form.form_errors`. + + .. attribute:: form_errors + + A list of form-level errors. Those are errors that does not concern a + particuliar field, but the whole form consistency. Those errors are + often set when overriding :meth:`~wtforms.forms.Form.validate`. + .. attribute:: meta This is an object which contains various configuration options and also diff --git a/src/wtforms/form.py b/src/wtforms/form.py index 29def6d23..26e833148 100644 --- a/src/wtforms/form.py +++ b/src/wtforms/form.py @@ -46,6 +46,8 @@ def __init__(self, fields, prefix="", meta=_default_meta): field = meta.bind_field(self, unbound_field, options) self._fields[name] = field + self.form_errors = [] + def __iter__(self): """Iterate form fields in creation order.""" return iter(self._fields.values()) @@ -140,7 +142,10 @@ def data(self): @property def errors(self): - return {name: f.errors for name, f in self._fields.items() if f.errors} + errors = {name: f.errors for name, f in self._fields.items() if f.errors} + if self.form_errors: + errors[None] = self.form_errors + return errors class FormMeta(type): diff --git a/tests/test_form.py b/tests/test_form.py index 619267308..27ad94805 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -174,6 +174,31 @@ def extra(form, field): form = F2(test="nope", other="extra") assert not form.validate(extra_validators={"other": [extra]}) + def test_form_level_errors(self): + class F(Form): + a = IntegerField() + b = IntegerField() + + def validate(self): + if not super().validate(): + return False + + if (self.a.data + self.b.data) % 2 != 0: + self.form_errors.append("a + b should be even") + return False + + return True + + f = F(a=1, b=1) + assert f.validate() + assert not f.form_errors + assert not f.errors + + f = F(a=0, b=1) + assert not f.validate() + assert ["a + b should be even"] == f.form_errors + assert ["a + b should be even"] == f.errors[None] + def test_field_adding_disabled(self): form = self.F() with pytest.raises(TypeError):