diff --git a/pyxform/errors.py b/pyxform/errors.py index bc6b3eba..59fb2e3d 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -79,6 +79,14 @@ class ErrorCode(Enum): "type '{type}' with value '{value}'." ), ) + SURVEY_003: Detail = Detail( + name="Survey Sheet - invalid geoshape/geotrace parameter 'incremental'", + msg=( + "[row : {row}] On the '{sheet}' sheet, the '{column}' value is invalid. " + "For geoshape and geotrace questions, the 'incremental' parameter may either " + "be 'true' or not included." + ), + ) class PyXFormError(Exception): diff --git a/pyxform/validators/pyxform/question_types.py b/pyxform/validators/pyxform/question_types.py index 8e24eca4..5a959524 100644 --- a/pyxform/validators/pyxform/question_types.py +++ b/pyxform/validators/pyxform/question_types.py @@ -4,6 +4,7 @@ from collections.abc import Collection, Iterable +from pyxform import aliases from pyxform.errors import ErrorCode, PyXFormError from pyxform.validators.pyxform.pyxform_reference import ( is_pyxform_reference_candidate, @@ -90,3 +91,12 @@ def process_trigger( return trigger else: return None + + +def validate_geo_parameter_incremental(value: str) -> None: + """For geoshape and geotrace, the 'incremental' parameter can only resolve to 'true'.""" + incremental = aliases.yes_no.get(value, None) + if incremental is None or not incremental: + raise PyXFormError( + code=ErrorCode.SURVEY_003, + ) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index be689c80..9e3577f7 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -1331,7 +1331,7 @@ def workbook_to_json( ) else: parameters_generic.validate( - parameters=parameters, allowed=("allow-mock-accuracy",) + parameters=parameters, allowed=("allow-mock-accuracy", "incremental") ) if "allow-mock-accuracy" in parameters: @@ -1366,6 +1366,19 @@ def workbook_to_json( "Parameter warning-accuracy must have a numeric value" ) from wa_err + if "incremental" in parameters: + try: + qt.validate_geo_parameter_incremental(value=parameters["incremental"]) + except PyXFormError as e: + e.context.update( + sheet=constants.SURVEY, + column=constants.PARAMETERS, + row=row_number, + ) + raise + else: + new_dict["control"]["incremental"] = "true" + parent_children_array.append(new_dict) continue # TODO: Consider adding some question_type validation here. diff --git a/tests/test_geo.py b/tests/test_geo.py index 0144bbda..1b46acda 100644 --- a/tests/test_geo.py +++ b/tests/test_geo.py @@ -2,6 +2,9 @@ Test geo widgets. """ +from pyxform import constants as co +from pyxform.errors import ErrorCode + from tests.pyxform_test_case import PyxformTestCase @@ -66,3 +69,117 @@ def test_geo_widgets_types(self): 'type="string"/>', ], ) + + +class TestParameterIncremental(PyxformTestCase): + def test_not_emitted_by_default(self): + """Should find that the parameter is not included as a default control attribute.""" + md = """ + | survey | + | | type | name | label | + | | {type} | q1 | Q1 | + """ + types = ["geoshape", "geotrace", "geopoint", "integer", "note"] + for t in types: + with self.subTest(t): + self.assertPyxformXform( + md=md.format(type=t), + xml__xpath_match=[ + "/h:html/h:body/x:input[@ref='/test_name/q1' and not(@incremental)]", + ], + ) + + def test_with_incremental__geoshape_geotrace__ok(self): + """Should find that the parameter is emitted as a control attribute if specified.""" + md = """ + | survey | + | | type | name | label | parameters | + | | {type} | q1 | Q1 | incremental=true | + """ + types = ["geoshape", "geotrace"] + for t in types: + with self.subTest(t): + self.assertPyxformXform( + md=md.format(type=t), + xml__xpath_match=[ + f"/h:html/h:head/x:model/x:bind[@nodeset='/test_name/q1' and @type='{t}']", + "/h:html/h:body/x:input[@ref='/test_name/q1' and @incremental='true']", + ], + ) + + def test_with_incremental__geoshape_geotrace____aliases__ok(self): + """Should find that the parameter is emitted as a control attribute if specified.""" + md = """ + | survey | + | | type | name | label | parameters | + | | {type} | q1 | Q1 | incremental={value} | + """ + types = ["geoshape", "geotrace"] + values = ["yes", "true()"] + for t in types: + for v in values: + with self.subTest((t, v)): + self.assertPyxformXform( + md=md.format(type=t, value=v), + xml__xpath_match=[ + f"/h:html/h:head/x:model/x:bind[@nodeset='/test_name/q1' and @type='{t}']", + "/h:html/h:body/x:input[@ref='/test_name/q1' and @incremental='true']", + ], + ) + + def test_with_incremental__geoshape_geotrace____wrong_value__error(self): + """Should raise an error if an unrecognised value is specified.""" + md = """ + | survey | + | | type | name | label | parameters | + | | {type} | q1 | Q1 | incremental={value} | + """ + types = ["geoshape", "geotrace"] + values = ["", "yeah", "false"] + for t in types: + for v in values: + with self.subTest((t, v)): + self.assertPyxformXform( + md=md.format(type=t, value=v), + errored=True, + error__contains=[ + ErrorCode.SURVEY_003.value.format( + sheet=co.SURVEY, column=co.PARAMETERS, row=2 + ) + ], + ) + + def test_with_incremental__wrong_type_with_params__error(self): + """Should raise an error if specified for other question types with parameters.""" + md = """ + | survey | + | | type | name | label | parameters | + | | {type} | q1 | Q1 | incremental=true | + """ + types = ["geopoint", "audio"] + for t in types: + with self.subTest(t): + self.assertPyxformXform( + md=md.format(type=t), + errored=True, + error__contains=[ + "The following are invalid parameter(s): 'incremental'." + ], + ) + + def test_with_incremental__wrong_type_no_params__ok(self): + """Should not raise an error if specified for other question types without parameters.""" + md = """ + | survey | + | | type | name | label | parameters | + | | {type} | q1 | Q1 | incremental=true | + """ + types = ["integer", "note"] + for t in types: + with self.subTest(t): + self.assertPyxformXform( + md=md.format(type=t), + xml__xpath_match=[ + "/h:html/h:body/x:input[@ref='/test_name/q1' and not(@incremental)]", + ], + )