Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pyxform/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,15 @@ class ErrorCode(Enum):
"Move this question out of the repeat, or choose a different question type."
),
)
SURVEY_011 = Detail(
name="Survey sheet - invalid 'required' on group or repeat",
msg=(
"[row : {row}] On the 'survey' sheet, the 'required' value is invalid. "
"Groups, repeats, and loops cannot be required. "
"Either remove the 'required' value in this row, or move it to a "
"question inside."
),
)


class PyXFormError(Exception):
Expand Down
10 changes: 9 additions & 1 deletion pyxform/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from typing import TYPE_CHECKING
from xml.dom.minidom import Attr

from pyxform import constants
from pyxform import aliases, constants
from pyxform.errors import PyXFormError
from pyxform.external_instance import ExternalInstance
from pyxform.survey_element import SURVEY_ELEMENT_FIELDS, SurveyElement
from pyxform.utils import DetachableElement, node
Expand Down Expand Up @@ -90,6 +91,13 @@ def add_children(self, children):

def validate(self):
super().validate()
required = self.bind.get("required") if isinstance(self.bind, dict) else None
if required and aliases.yes_no.get(required, True):
raise PyXFormError(
f"The 'required' value on the {self.type} '{self.name}' is invalid. "
"Groups, repeats, and loops cannot be required. "
"Either remove the 'required' value, or move it to a question inside."
)
for element in self.children:
element.validate()
self._validate_uniqueness_of_element_names()
Expand Down
5 changes: 5 additions & 0 deletions pyxform/xls2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,11 @@ def workbook_to_json(
+ f" {control_type.capitalize()} has no label: {msg_dict}"
)

bind = row.get(constants.BIND, {})
required = bind.get("required") if isinstance(bind, dict) else None
if required and aliases.yes_no.get(required, True):
raise PyXFormError(ErrorCode.SURVEY_011.value.format(row=row_number))

new_json_dict = row.copy()
new_json_dict[constants.TYPE] = control_type
child_list = []
Expand Down
66 changes: 65 additions & 1 deletion tests/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from unittest import TestCase

from pyxform.builder import create_survey_element_from_dict
from pyxform.errors import ErrorCode
from pyxform.errors import ErrorCode, PyXFormError
from pyxform.xls2xform import convert

from tests.pyxform_test_case import PyxformTestCase
Expand Down Expand Up @@ -799,6 +799,47 @@ def test_group__no_begin_error__with_another_closed_repeat(self):
error__contains=[ErrorCode.SURVEY_001.value.format(row=4, type="group")],
)

def test_group__required__error(self):
"""Should raise an error if 'required' is used on a group."""
md = """
| survey |
| | type | name | label | required |
| | begin group | g1 | G1 | yes |
| | text | q1 | Q1 | |
| | end group | | | |
"""
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[ErrorCode.SURVEY_011.value.format(row=2)],
)

def test_repeat__required__error(self):
"""Should raise an error if 'required' is used on a repeat."""
md = """
| survey |
| | type | name | label | required |
| | begin repeat | r1 | R1 | yes |
| | text | q1 | Q1 | |
| | end repeat | | | |
"""
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[ErrorCode.SURVEY_011.value.format(row=2)],
)

def test_group__required_no__ok(self):
"""Should not raise an error if 'required' is explicitly 'no' on a group."""
md = """
| survey |
| | type | name | label | required |
| | begin group | g1 | G1 | no |
| | text | q1 | Q1 | |
| | end group | | | |
"""
self.assertPyxformXform(md=md)

def test_empty_group__no_question__error(self):
"""Should raise an error for an empty group with no questions."""
md = """
Expand Down Expand Up @@ -918,3 +959,26 @@ def test_to_json_round_trip(self):
expected = convert(xlsform=md, form_name="group")._survey.to_json_dict()
observed = create_survey_element_from_dict(expected).to_json_dict()
self.assertEqual(expected, observed)

def test_group_required_bind__error(self):
"""Should raise an error if a group is built with a required bind."""
d = {
"name": "data",
"title": "data",
"type": "survey",
"id_string": "data",
"children": [
{
"type": "group",
"name": "g1",
"label": "G1",
"bind": {"required": "true()"},
"children": [
{"type": "text", "name": "q1", "label": "Q1"},
],
}
],
}
survey = create_survey_element_from_dict(d)
with self.assertRaises(PyXFormError):
survey.xml()