diff --git a/pyxform/constants.py b/pyxform/constants.py index 6f42bb4f1..f27f5cf70 100644 --- a/pyxform/constants.py +++ b/pyxform/constants.py @@ -68,6 +68,7 @@ # The following are the possible sheet names: SURVEY = "survey" SETTINGS = "settings" +EXTERNAL_CHOICES = "external_choices" # These sheet names are for list sheets CHOICES_AND_COLUMNS = "choices and columns" CASCADING_CHOICES = "cascades" @@ -84,6 +85,7 @@ COLUMNS, CHOICES_AND_COLUMNS, SETTINGS, + EXTERNAL_CHOICES, OSM, ] SUPPORTED_FILE_EXTENSIONS = [".xls", ".xlsx", ".xlsm"] diff --git a/pyxform/tests_v1/test_support_external_instances.py b/pyxform/tests_v1/test_support_external_instances.py index 1c3714f8b..60810f260 100644 --- a/pyxform/tests_v1/test_support_external_instances.py +++ b/pyxform/tests_v1/test_support_external_instances.py @@ -165,3 +165,52 @@ def test_non_existent_itext_reference(self): """ ], ) + + def test_external_choices_sheet_values_required(self): + md = """ + | survey | | | | | + | | type | name | label | choice_filter | + | | text | S1 | s1 | | + | | select_one_external counties | county | county | S1=${S1} | + | | select_one_external cities | city | city | S1=${S1} and county=${county} | + | choices | | | | + | | list name | name | label | + | | list | option a | a | + | external_choices | | | + | | list_name | name | + | | counties | Kajiado | + | | counties | Nakuru | + | | cities | Kisumu | + | | cities | Mombasa | + """ + expected = [ + """ + + """ + ] # noqa + self.assertPyxformXform( + md=md, debug=True, model__contins=[expected], run_odk_validate=True + ) + + def test_list_name_not_in_external_choices_sheet_raises_error(self): + self.assertPyxformXform( + md=""" + | survey | | | | | + | | type | name | label | choice_filter | + | | select_one list | S1 | s1 | | + | | select_one_external counties | county | County | S1=${S1} | + | | select_one_external cities | city | City | S1=${S1} and county=${county} | + | choices | | | | + | | list name | name | label | + | | list | option a | a | + | | list | option b | b | + | | list | option c | c | + | external_choices | | | + | | list_name | name | + | | counties | Kajiado | + | | counties | Nakuru | + """, # noqa + errored=True, + error__contains=["List name not in external choices sheet: cities"], + ) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index 9cb681025..bda1feb58 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -406,6 +406,19 @@ def workbook_to_json( # Here the default settings are overridden by those in the settings sheet json_dict.update(settings) + # ########## External Choices sheet ########## + + external_choices_sheet = workbook_dict.get(constants.EXTERNAL_CHOICES, []) + for choice_item in external_choices_sheet: + replace_smart_quotes_in_dict(choice_item) + + external_choices_sheet = dealias_and_group_headers( + external_choices_sheet, aliases.list_header, use_double_colons, default_language + ) + external_choices = group_dictionaries_by_key( + external_choices_sheet, constants.LIST_NAME + ) + # ########## Choices sheet ########## # Columns and "choices and columns" sheets are deprecated, # but we combine them with the choices sheet for backwards-compatibility. @@ -998,10 +1011,18 @@ def replace_prefix(d, prefix): + " select one external is only meant for" " filtered selects." ) - select_type = aliases.select["select_one"] list_name = parse_dict["list_name"] list_file_name, file_extension = os.path.splitext(list_name) - + if ( + select_type == "select one external" + and list_name not in external_choices + ): + raise PyXFormError( + row_format_string % row_number + + "List name not in external choices sheet: " + + list_name + ) + select_type = aliases.select["select_one"] if ( list_name not in choices and select_type != "select one external"