diff --git a/src/onegov/form/errors.py b/src/onegov/form/errors.py index b773e9c210..adea519b5c 100644 --- a/src/onegov/form/errors.py +++ b/src/onegov/form/errors.py @@ -29,6 +29,11 @@ def __init__(self, line: int): self.line = line +class EmptyFieldsetError(FormError): + def __init__(self, field_name: str): + self.field_name = field_name + + class FieldCompileError(FormError): def __init__(self, field_name: str): self.field_name = field_name diff --git a/src/onegov/form/locale/de_ch/LC_MESSAGES/onegov.form.po b/src/onegov/form/locale/de_ch/LC_MESSAGES/onegov.form.po index 2142b4c50b..0ab341126e 100644 --- a/src/onegov/form/locale/de_ch/LC_MESSAGES/onegov.form.po +++ b/src/onegov/form/locale/de_ch/LC_MESSAGES/onegov.form.po @@ -175,6 +175,12 @@ msgstr "" "Ein Minimalpreis kann nur gesetzt werden, wenn mindestens ein " "kostenpflichtiges Feld definiert ist." +#, python-format +msgid "The '{label}' group is empty and will not be visible. Either " +"remove the empty group or add fields to it." +msgstr "Die Gruppe '{label}' ist leer und wird nicht sichtbar sein. " +"Entweder entfernen Sie die leere Gruppe oder fügen Felder hinzu." + #, python-format msgid "" "Invalid field type for field '{label}'. For filters only 'select' or " diff --git a/src/onegov/form/locale/fr_CH/LC_MESSAGES/onegov.form.po b/src/onegov/form/locale/fr_CH/LC_MESSAGES/onegov.form.po index 07be845909..397293c830 100644 --- a/src/onegov/form/locale/fr_CH/LC_MESSAGES/onegov.form.po +++ b/src/onegov/form/locale/fr_CH/LC_MESSAGES/onegov.form.po @@ -174,6 +174,12 @@ msgstr "" "Un total de prix minimum ne peut être paramétré que si au moins un champ " "payant est défini." +#, python-format +msgid "The '{label}' group is empty and will not be visible. Either " +"remove the empty group or add fields to it." +msgstr "Le groupe '{label}' est vide et ne sera pas visible. Supprimez le " +"groupe vide ou ajoutez des champs." + #, python-format msgid "" "Invalid field type for field '{label}'. For filters only 'select' or " diff --git a/src/onegov/form/locale/it_CH/LC_MESSAGES/onegov.form.po b/src/onegov/form/locale/it_CH/LC_MESSAGES/onegov.form.po index a861e80bbd..80ef0770e5 100644 --- a/src/onegov/form/locale/it_CH/LC_MESSAGES/onegov.form.po +++ b/src/onegov/form/locale/it_CH/LC_MESSAGES/onegov.form.po @@ -172,6 +172,13 @@ msgstr "" "È possibile impostare un prezzo minimo totale solo se è definito almeno un " "campo prezzo." +#, python-format +msgid "The '{label}' group is empty and will not be visible. Either " +"remove the empty group or add fields to it." +msgstr "" +"Il gruppo '{label}' è vuoto e non sarà visibile. Rimuovere il gruppo vuoto " +"o aggiungere campi." + #, python-format msgid "" "Invalid field type for field '{label}'. For filters only 'select' or " diff --git a/src/onegov/form/parser/core.py b/src/onegov/form/parser/core.py index 2e0625e87c..70e7b885ed 100644 --- a/src/onegov/form/parser/core.py +++ b/src/onegov/form/parser/core.py @@ -1156,18 +1156,19 @@ class CheckboxField(OptionsField, Field): @lru_cache(maxsize=1) def parse_formcode( formcode: str, - enable_indent_check: bool = False + enable_edit_checks: bool = False ) -> list[Fieldset]: """ Takes the given formcode and returns an intermediate representation that can be used to generate forms or do other things. :param formcode: string representing formcode to be parsed - :param enable_indent_check: bool to activate indent check while parsing. - Should only be active originating from forms.validators.py + :param enable_edit_checks: bool to activate additional check after + editing the form. Should only be active originating from + forms.validators.py """ # CustomLoader is inherited from SafeLoader so no security issue here parsed = yaml.load( # nosec B506 - '\n'.join(translate_to_yaml(formcode, enable_indent_check)), + '\n'.join(translate_to_yaml(formcode, enable_edit_checks)), CustomLoader ) @@ -1188,6 +1189,8 @@ def parse_formcode( parse_field_block(block, field_classes, used_ids, fs) for block in (fieldset[label] or ()) ] + if enable_edit_checks and not fs.fields: + raise errors.EmptyFieldsetError(label) fieldsets.append(fs) @@ -1328,14 +1331,14 @@ def validate_indent(indent: str) -> bool: def translate_to_yaml( text: str, - enable_indent_check: bool = False + enable_edit_checks: bool = False ) -> 'Iterator[str]': """ Takes the given form text and constructs an easier to parse yaml string. :param text: string to be parsed - :param enable_indent_check: bool to activate indent check while parsing. - Should only be active originating from forms.validators.py + :param enable_edit_checks: bool to activate additional checks after + editing a form. Should only be active originating from forms.validators.py """ lines = ((ix, l) for ix, l in prepare(text)) @@ -1352,7 +1355,7 @@ def escape_double(text: str) -> str: for ix, line in lines: indent = ' ' * (4 + (len(line) - len(line.lstrip()))) - if enable_indent_check and not validate_indent(indent): + if enable_edit_checks and not validate_indent(indent): raise errors.InvalidIndentSyntax(line=ix + 1) # the top level are the fieldsets diff --git a/src/onegov/form/parser/form.py b/src/onegov/form/parser/form.py index d7a6efe8be..4ee739e3ff 100644 --- a/src/onegov/form/parser/form.py +++ b/src/onegov/form/parser/form.py @@ -54,7 +54,7 @@ @overload def parse_form( text: str, - enable_indent_check: bool, + enable_edit_checks: bool, base_class: type[_FormT] ) -> type[_FormT]: ... @@ -62,7 +62,7 @@ def parse_form( @overload def parse_form( text: str, - enable_indent_check: bool = False, + enable_edit_checks: bool = False, *, base_class: type[_FormT] ) -> type[_FormT]: ... @@ -71,27 +71,28 @@ def parse_form( @overload def parse_form( text: str, - enable_indent_check: bool = False, + enable_edit_checks: bool = False, base_class: type[Form] = Form ) -> type[Form]: ... def parse_form( text: str, - enable_indent_check: bool = False, + enable_edit_checks: bool = False, base_class: type[Form] = Form ) -> type[Form]: """ Takes the given form text, parses it and returns a WTForms form class (not an instance of it). :type text: string form text to be parsed - :param enable_indent_check: bool to activate indent check while parsing. + :param enable_edit_checks: bool to activate additional checks after + editing a form. :param base_class: Form base class """ builder = WTFormsClassBuilder(base_class) - for fieldset in parse_formcode(text, enable_indent_check): + for fieldset in parse_formcode(text, enable_edit_checks): builder.set_current_fieldset(fieldset.label) for field in fieldset.fields: diff --git a/src/onegov/form/validators.py b/src/onegov/form/validators.py index 22d5c1ab6c..58e3e50963 100644 --- a/src/onegov/form/validators.py +++ b/src/onegov/form/validators.py @@ -11,7 +11,8 @@ from dateutil.relativedelta import relativedelta from mimetypes import types_map from onegov.form import _ -from onegov.form.errors import DuplicateLabelError, InvalidIndentSyntax +from onegov.form.errors import (DuplicateLabelError, InvalidIndentSyntax, + EmptyFieldsetError) from onegov.form.errors import FieldCompileError from onegov.form.errors import InvalidFormSyntax from onegov.form.errors import MixedTypeError @@ -190,6 +191,9 @@ class ValidFormDefinition: "A minimum price total can only be set if at least one priced field " "is defined." ) + empty_fieldset = _( + "The '{label}' group is empty and will not be visible. Either remove " + "the empty group or add fields to it.") def __init__( self, @@ -219,6 +223,11 @@ def __call__(self, form: 'Form', field: 'Field') -> 'Form | None': raise ValidationError( field.gettext(self.indent).format(line=exception.line) ) from exception + except EmptyFieldsetError as exception: + raise ValidationError( + field.gettext(self.empty_fieldset).format( + label=exception.field_name) + ) from exception except DuplicateLabelError as exception: raise ValidationError( field.gettext(self.duplicate).format(label=exception.label) @@ -305,13 +314,13 @@ def __call__(self, form: 'Form', field: 'Field') -> 'Form | None': def _parse_form( self, field: 'Field', - enable_indent_check: bool = True + enable_edit_checks: bool = True ) -> 'Form': # XXX circular import from onegov.form import parse_form return parse_form(field.data, - enable_indent_check=enable_indent_check)() + enable_edit_checks=enable_edit_checks)() class ValidFilterFormDefinition(ValidFormDefinition): diff --git a/tests/onegov/form/test_parser.py b/tests/onegov/form/test_parser.py index f66ca8bb7f..aa2aa96034 100644 --- a/tests/onegov/form/test_parser.py +++ b/tests/onegov/form/test_parser.py @@ -1046,7 +1046,7 @@ def test_normalization(): assert norm == "Ich möchte die Bestellung mittels Post erhalten" -@pytest.mark.parametrize('indent,indent_check,shall_raise', [ +@pytest.mark.parametrize('indent,edit_checks,shall_raise', [ # indent check active while parsing ('', True, False), (' ', True, True), @@ -1055,7 +1055,7 @@ def test_normalization(): # no indent check while parsing ('', False, False), ]) -def test_indentation_error_while_parsing(indent, indent_check, shall_raise): +def test_indentation_error(indent, edit_checks, shall_raise): # wrong indent see 'Telefonnummer' text = dedent( """ @@ -1069,11 +1069,57 @@ def test_indentation_error_while_parsing(indent, indent_check, shall_raise): if shall_raise: with pytest.raises(InvalidIndentSyntax) as excinfo: - parse_formcode(text, enable_indent_check=indent_check) + parse_formcode(text, enable_edit_checks=edit_checks) assert excinfo.value.line == 6 else: try: - parse_formcode(text, enable_indent_check=indent_check) + parse_formcode(text, enable_edit_checks=edit_checks) except InvalidIndentSyntax as e: pytest.fail('Unexpected exception {}'.format(type(e).__name__)) + + +def test_empty_fieldset_error(): + fieldsets = parse_formcode('\n'.join(( + "# Section 1", + "# Section 2", + "First Name *= ___", + "Last Name *= ___", + "E-mail *= @@@" + )), enable_edit_checks=False) + assert len(fieldsets) == 2 + + with pytest.raises(errors.EmptyFieldsetError) as e: + parse_formcode('\n'.join(( + "# Section 1", + "# Section 2", + "First Name *= ___", + "Last Name *= ___", + "E-mail *= @@@" + )), enable_edit_checks=True) + + assert e.value.field_name == 'Section 1' + + with pytest.raises(errors.EmptyFieldsetError) as e: + parse_formcode('\n'.join(( + "# Section 1", + "First Name *= ___", + "Last Name *= ___", + "# Section 2", + "E-mail *= @@@", + "# Section 3", + )), enable_edit_checks=True) + + assert e.value.field_name == 'Section 3' + + with pytest.raises(errors.EmptyFieldsetError) as e: + parse_formcode('\n'.join(( + "# Section 1", + "First Name *= ___", + "Last Name *= ___", + "# Section 2", + "# Section 3", + "E-mail *= @@@", + )), enable_edit_checks=True) + + assert e.value.field_name == 'Section 2'