diff --git a/formiodata/components/component.py b/formiodata/components/component.py index 8c3cd22..0e944f4 100644 --- a/formiodata/components/component.py +++ b/formiodata/components/component.py @@ -61,7 +61,7 @@ def load(self, component_owner, parent=None, data=None, all_data=None): self.builder.component_ids[self.id] = self - # parh + # path self.set_builder_paths() builder_path_keys = [p.key for p in self.builder_path] builder_path_key = '.'.join(builder_path_keys) @@ -330,3 +330,12 @@ def is_visible(self): return self.conditionally_visible else: return not self.hidden + + def validation_errors(self): + errors = {} + if self.required and not self.value: + msg_tmpl = '{{field}} is required' + if self.i18n.get(self.language): + msg_tmpl = self.i18n[self.language].get(msg_tmpl, msg_tmpl) + errors['required'] = msg_tmpl.replace('{{field}}', self.label) + return errors diff --git a/formiodata/components/grid_base.py b/formiodata/components/grid_base.py index 66e540a..bf0b214 100644 --- a/formiodata/components/grid_base.py +++ b/formiodata/components/grid_base.py @@ -107,6 +107,22 @@ def child_component_owner(self): def initEmpty(self): return self.raw.get('initEmpty') + def validation_errors(self): + errors = [] + for row_idx, row in enumerate(self.rows): + row_errors = {} + for component_key, component in row.input_components.items(): + component_errors = component.validation_errors() + if bool(component_errors): + # scalar (not grid) components retrieve a Dict + # from method validation_errors() + row_errors[component_key] = component_errors + if bool(row_errors): + errors.append(row_errors) + else: + errors.append({}) + return errors + def render(self): for row in self.rows: row.render() diff --git a/formiodata/form.py b/formiodata/form.py index 8f395c3..c8142b0 100644 --- a/formiodata/form.py +++ b/formiodata/form.py @@ -5,7 +5,7 @@ import logging import re -from collections import OrderedDict +from collections import defaultdict, OrderedDict from formiodata.builder import Builder @@ -120,6 +120,22 @@ def get_component_by_path(self, component_path): component = components[path_node] return component + def validation_errors(self): + """ + @return errors dict: Dictionary where key is component key and + value is a Dictionary with errors. + """ + errors = defaultdict(dict) + for component_key, component in self.input_components.items(): + component_errors = component.validation_errors() + if isinstance(component_errors, dict): + for error_type, val in component_errors.items(): + vals = {error_type: val} + errors[component_key].update(vals) + elif isinstance(component_errors, list): + errors[component_key] = component_errors + return errors + def render_components(self, force=False): for key, component in self.input_components.items(): if force or component.html_component == "": diff --git a/tests/data/test_example_builder.json b/tests/data/test_example_builder.json index 06abb9c..4bef874 100644 --- a/tests/data/test_example_builder.json +++ b/tests/data/test_example_builder.json @@ -117,7 +117,7 @@ "allowCalculateOverride": false, "validateOn": "change", "validate": { - "required": false, + "required": true, "pattern": "", "customMessage": "", "custom": "", @@ -518,7 +518,7 @@ "maxLength": "", "minLength": "", "pattern": "", - "required": false, + "required": true, "strictDateValidation": false, "multiple": false, "unique": false @@ -2988,7 +2988,7 @@ "allowCalculateOverride": false, "validateOn": "change", "validate": { - "required": false, + "required": true, "pattern": "", "customMessage": "", "custom": "", diff --git a/tests/data/test_example_form_validation_errors.json b/tests/data/test_example_form_validation_errors.json new file mode 100644 index 0000000..f33b8be --- /dev/null +++ b/tests/data/test_example_form_validation_errors.json @@ -0,0 +1,41 @@ +{ + "firstName": "", + "email": "yourmail@yourlife.io", + "birthdate": "", + "appointmentDateTime": "", + "lastName": "", + "phoneNumber": "", + "cardinalDirection": "", + "favouriteSeason": "", + "favouriteFood": [], + "monthDayYear": "00/00/0000", + "monthYear": "00/00/0000", + "dayMonthYear": "00/00/0000", + "dayMonth": "00/00/0000", + "day": "00/00/0000", + "month": "00/00/0000", + "year": "00/00/0000", + "survey": null, + "signature": "", + "dataGrid": [ + { + "textField": "", + "checkbox": false + }, + { + "textField": "Some #1 text here", + "checkbox": false + }, + { + "textField": "", + "checkbox": false + }, + { + "textField": "Some #2 text here", + "checkbox": false + } + ], + "uploadBase64": [], + "uploadUrl": [], + "submit": true +} diff --git a/tests/test_component.py b/tests/test_component.py index 2fd6a2a..e42a167 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -22,6 +22,7 @@ def setUp(self): self.builder_i18n_nl = Builder(self.builder_json, language='nl', i18n=self._i18n()) self.form_i18n_nl = Form(self.form_json, self.builder_i18n_nl) + self.form_empty_i18n_nl = Form(self.form_empty_json, self.builder_i18n_nl) def _i18n(self): return { @@ -46,8 +47,10 @@ def _i18n(self): 'Month Day Year': 'Maand dag jaar', 'Day Month Year': 'Dag maand jaar', 'May': 'Mei', + 'Text Field': 'Tekstveld', 'Upload Base64': 'Upload binair naar ASCII', - 'Upload Url': 'Upload naar locatie' + 'Upload Url': 'Upload naar locatie', + '{{field}} is required': '{{field}} is verplicht' } } diff --git a/tests/test_validation_error_simple.py b/tests/test_validation_error_simple.py new file mode 100644 index 0000000..1b605ca --- /dev/null +++ b/tests/test_validation_error_simple.py @@ -0,0 +1,86 @@ +# Copyright Nova Code (http://www.novacode.nl) +# See LICENSE file for full licensing details. + +from tests.utils import readfile +from test_component import ComponentTestCase + +from formiodata.form import Form + + +class ValidationErrorSimpleTestCase(ComponentTestCase): + + def setUp(self): + super(ValidationErrorSimpleTestCase, self).setUp() + self.form_validation_errors_json = readfile('data', 'test_example_form_validation_errors.json') + self.form_validation_errors = Form(self.form_validation_errors_json, self.builder) + self.form_validation_errors_i18n_nl = Form(self.form_validation_errors_json, self.builder_i18n_nl) + + def test_required_components_in_builder(self): + firstName = self.builder.input_components['firstName'] + self.assertTrue(firstName.required) + + lastName = self.builder.input_components['lastName'] + self.assertTrue(lastName.required) + + email = self.builder.input_components['email'] + self.assertFalse(email.required) + + def test_required_components_form_validation_errors(self): + errors = self.form_validation_errors.validation_errors() + + self.assertEqual( + errors['firstName']['required'], + 'First Name is required' + ) + self.assertEqual( + errors['lastName']['required'], + 'Last Name is required' + ) + self.assertEqual( + errors['dataGrid'][0]['textField']['required'], + 'Text Field is required' + ) + self.assertEqual( + errors['dataGrid'][1], + {} + ) + self.assertEqual( + errors['dataGrid'][2]['textField']['required'], + 'Text Field is required' + ) + self.assertEqual( + errors['dataGrid'][3], + {} + ) + + def test_required_components_form_validation_errors_i18n_nl(self): + errors = self.form_validation_errors_i18n_nl.validation_errors() + + self.assertEqual( + errors['firstName']['required'], + 'Voornaam is verplicht' + ) + self.assertEqual( + errors['lastName']['required'], + 'Achternaam is verplicht' + ) + self.assertEqual( + errors['dataGrid'][0]['textField']['required'], + 'Tekstveld is verplicht' + ) + self.assertEqual( + errors['dataGrid'][1], + {} + ) + self.assertEqual( + errors['dataGrid'][2]['textField']['required'], + 'Tekstveld is verplicht' + ) + self.assertEqual( + errors['dataGrid'][3], + {} + ) + + def test_not_required_components_form(self): + errors = self.form_validation_errors_i18n_nl.validation_errors() + self.assertNotIn('email', errors)