Skip to content

Commit

Permalink
Form: Raise error for empty field sets
Browse files Browse the repository at this point in the history
TYPE: Feature
LINK: ogc-1160
  • Loading branch information
Tschuppi81 committed Jul 19, 2024
1 parent 06a504c commit 11be9de
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 21 deletions.
5 changes: 5 additions & 0 deletions src/onegov/form/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/onegov/form/locale/de_ch/LC_MESSAGES/onegov.form.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
6 changes: 6 additions & 0 deletions src/onegov/form/locale/fr_CH/LC_MESSAGES/onegov.form.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
7 changes: 7 additions & 0 deletions src/onegov/form/locale/it_CH/LC_MESSAGES/onegov.form.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down
19 changes: 11 additions & 8 deletions src/onegov/form/parser/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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)

Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
13 changes: 7 additions & 6 deletions src/onegov/form/parser/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@
@overload
def parse_form(
text: str,
enable_indent_check: bool,
enable_edit_checks: bool,
base_class: type[_FormT]
) -> type[_FormT]: ...


@overload
def parse_form(
text: str,
enable_indent_check: bool = False,
enable_edit_checks: bool = False,
*,
base_class: type[_FormT]
) -> type[_FormT]: ...
Expand All @@ -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:
Expand Down
15 changes: 12 additions & 3 deletions src/onegov/form/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
54 changes: 50 additions & 4 deletions tests/onegov/form/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
"""
Expand All @@ -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'

0 comments on commit 11be9de

Please sign in to comment.