diff --git a/src/openforms/formio/registry.py b/src/openforms/formio/registry.py index fb7bdb2605..07955be537 100644 --- a/src/openforms/formio/registry.py +++ b/src/openforms/formio/registry.py @@ -91,7 +91,10 @@ def build_serializer_field(self, component: ComponentT) -> serializers.Field: DeprecationWarning, ) - if is_layout_component(component): + # not considered a layout component (because it doesn't have children) + if component["type"] == "content": + required = False + elif is_layout_component(component): required = False # they do not hold data, they can never be required else: required = ( diff --git a/src/openforms/formio/serializers.py b/src/openforms/formio/serializers.py index 07487efbc8..dea899300c 100644 --- a/src/openforms/formio/serializers.py +++ b/src/openforms/formio/serializers.py @@ -8,6 +8,7 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING, TypeAlias from glom import assign, glom @@ -80,6 +81,26 @@ def _remove_validations_from_field(self, field: serializers.Field) -> None: case serializers.CharField(): field.allow_blank = True + case serializers.ListField(): + field.allow_null = True + field.allow_empty = True + field.min_length = None + field.max_length = None + + def _get_required(self) -> bool: + return any(field.required for field in self.fields.values()) + + def _set_required(self, value: bool) -> None: + # we need a setter because the serializers.Field.__init__ sets the initival + # value, but we actually derive the value via :meth:`_get_required` above + # dynamically based on the children, so we just ignore it. + logging.debug( + "Setting the serializer required property has no effect. " + "This is deliberate" + ) + + required = property(_get_required, _set_required) # type:ignore + def dict_to_serializer( fields: dict[str, FieldOrNestedFields], **kwargs diff --git a/src/openforms/formio/tests/validation/test_content.py b/src/openforms/formio/tests/validation/test_content.py new file mode 100644 index 0000000000..425a43e6f1 --- /dev/null +++ b/src/openforms/formio/tests/validation/test_content.py @@ -0,0 +1,19 @@ +from django.test import SimpleTestCase + +from ...typing import ContentComponent +from .helpers import validate_formio_data + + +class ContentValidationTests(SimpleTestCase): + def test_content_ignored(self): + component: ContentComponent = { + "type": "content", + "key": "Helaas-5.36", + "label": "Nope", + "validate": {"required": False}, + "html": "
Nope
", + } + + is_valid, _ = validate_formio_data(component, {}) + + self.assertTrue(is_valid) diff --git a/src/openforms/formio/tests/validation/test_editgrid.py b/src/openforms/formio/tests/validation/test_editgrid.py index cfd026a832..ab4bbef10c 100644 --- a/src/openforms/formio/tests/validation/test_editgrid.py +++ b/src/openforms/formio/tests/validation/test_editgrid.py @@ -4,7 +4,7 @@ from openforms.typing import JSONObject from ...service import build_serializer -from ...typing import EditGridComponent +from ...typing import EditGridComponent, FieldsetComponent from .helpers import validate_formio_data @@ -183,9 +183,39 @@ def test_optional_editgrid(self): ], } - is_valid, errors = validate_formio_data( + is_valid, _ = validate_formio_data( component, {"optionalRepeatingGroup": []}, ) self.assertTrue(is_valid) + + def test_required_but_empty_editgrid(self): + editgrid: EditGridComponent = { + "type": "editgrid", + "key": "requiredRepeatingGroup", + "label": "Required repeating group", + "validate": {"required": True}, + "components": [ + { + "type": "textfield", + "key": "optionalTextfield", + "label": "Optional Text field", + "validate": {"required": False}, + } + ], + } + component: FieldsetComponent = { + "type": "fieldset", + "key": "fieldset", + "label": "Hidden fieldset", + "hidden": True, + "components": [editgrid], + } + + is_valid, _ = validate_formio_data( + component, + {"requiredRepeatingGroup": []}, + ) + + self.assertTrue(is_valid) diff --git a/src/openforms/formio/tests/validation/test_generic.py b/src/openforms/formio/tests/validation/test_generic.py index 84e325e113..d45798df2d 100644 --- a/src/openforms/formio/tests/validation/test_generic.py +++ b/src/openforms/formio/tests/validation/test_generic.py @@ -3,7 +3,7 @@ from openforms.typing import JSONValue from ...typing import Component -from .helpers import validate_formio_data +from .helpers import extract_error, validate_formio_data class FallbackBehaviourTests(SimpleTestCase): @@ -21,3 +21,17 @@ def test_unknown_component_passthrough(self): is_valid, _ = validate_formio_data(component, data) self.assertTrue(is_valid) + + def test_nested_keys_and_fields_being_required(self): + component: Component = { + "type": "textfield", + "key": "nested.field", + "label": "Nested data", + "validate": {"required": True}, + } + + is_valid, errors = validate_formio_data(component, {}) + + self.assertFalse(is_valid) + error = extract_error(errors, "nested") + self.assertEqual(error.code, "required")