Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
588c3b9
chg: remove unused question_type xls + code
lindsay-stevens Sep 1, 2025
848117f
chg: update and expand tests on loops
lindsay-stevens Sep 3, 2025
e296c9c
add: test coverage for unmatched group begin/end
lindsay-stevens Sep 4, 2025
9002113
chg: improve error messages for unmatched groups
lindsay-stevens Sep 4, 2025
a4e05b1
add: test coverage for name uniqueness rules
lindsay-stevens Sep 9, 2025
f8fd743
chg: make name uniqueness rules more consistent
lindsay-stevens Sep 9, 2025
7a3d3cd
chg: move entities tests into folder
lindsay-stevens Sep 10, 2025
a187792
add: allow entities to be declared for a repeat
lindsay-stevens Sep 10, 2025
ba42054
add: name to unmatched control error messages
lindsay-stevens Sep 15, 2025
cdce738
chg: improve error messages
lindsay-stevens Sep 15, 2025
c736045
dev: merge branch 'master' into 'pyxform-775'
lindsay-stevens Sep 19, 2025
2caa1e2
add: set entity id when creating new repeat
lindsay-stevens Sep 19, 2025
0af166a
chg: set new entities version when repeat used
lindsay-stevens Sep 19, 2025
df6eee6
chg: improve entities xpath helper, refactor tests
lindsay-stevens Sep 19, 2025
1e481b9
chg: standardise/fix/split action node output
lindsay-stevens Oct 1, 2025
026bfe0
fix: repeat entity label binding path too low
lindsay-stevens Oct 2, 2025
0014ca9
fix: repeat entity attribute binding paths too low
lindsay-stevens Oct 7, 2025
983b990
add: error if entity save_to in undeclared repeat
lindsay-stevens Oct 7, 2025
3c8287e
fix: only emit repeat setvalue with create mode
lindsay-stevens Oct 9, 2025
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
39 changes: 19 additions & 20 deletions pyxform/xls2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@
RE_OSM = re.compile(
r"(?P<osm_command>(" + "|".join(aliases.osm) + r")) (?P<list_name>\S+)"
)
INVALID_CONTROL_END = (
"[row : {row}] Unmatched 'end {control_type}'. "
Comment thread
lindsay-stevens marked this conversation as resolved.
Outdated
"No matching 'begin {control_type}' was found."
)
INVALID_CONTROL_BEGIN = (
"[row : {row}] Unmatched 'begin {control_type}'. "
"No matching 'end {control_type}' was found."
)


def dealias_types(dict_array):
Expand Down Expand Up @@ -519,6 +527,7 @@ def workbook_to_json(
"control_type": None,
"control_name": None,
"parent_children": json_dict.get(constants.CHILDREN),
"row_number": None,
}
]
# If a group has a table-list appearance flag
Expand All @@ -533,12 +542,9 @@ def workbook_to_json(

# row by row, validate questions, throwing errors and adding warnings where needed.
for row_number, row in enumerate(survey_sheet.data, start=2):
if stack[-1] is not None:
prev_control_type = stack[-1]["control_type"]
parent_children_array = stack[-1]["parent_children"]
else:
prev_control_type = None
parent_children_array = []
prev_control_type = stack[-1]["control_type"]
parent_children_array = stack[-1]["parent_children"]

# Disabled should probably be first
# so the attributes below can be disabled.
if "disabled" in row:
Expand All @@ -557,7 +563,6 @@ def workbook_to_json(

# Get question type
question_type = row.get(constants.TYPE)
question_name = row.get(constants.NAME)

if not question_type:
# if name and label are also missing,
Expand Down Expand Up @@ -772,16 +777,11 @@ def workbook_to_json(
parse_dict = end_control_parse.groupdict()
if parse_dict.get("end") and "type" in parse_dict:
control_type = aliases.control[parse_dict["type"]]
control_name = question_name
if prev_control_type != control_type or len(stack) == 1:
raise PyXFormError(
ROW_FORMAT_STRING % row_number
+ " Unmatched end statement. Previous control type: "
+ str(prev_control_type)
+ ", Control type: "
+ str(control_type)
+ ", Control name: "
+ str(control_name)
INVALID_CONTROL_END.format(
row=row_number, control_type=control_type
)
)
stack.pop()
table_list = None
Expand All @@ -792,9 +792,6 @@ def workbook_to_json(
if row["type"] == "note":
# autogenerate names for notes without them
row["name"] = "generated_note_name_" + str(row_number)
# elif 'group' in row['type'].lower():
# # autogenerate names for groups without them
# row['name'] = "generated_group_name_" + str(row_number)
else:
raise PyXFormError(
ROW_FORMAT_STRING % row_number + " Question or group with no name."
Expand Down Expand Up @@ -937,6 +934,7 @@ def workbook_to_json(
"control_type": control_type,
"control_name": control_name,
"parent_children": child_list,
"row_number": row_number,
}
)
continue
Expand Down Expand Up @@ -1370,8 +1368,9 @@ def workbook_to_json(

if len(stack) != 1:
raise PyXFormError(
"Unmatched begin statement: "
+ str(stack[-1]["control_type"] + " (" + stack[-1]["control_name"] + ")")
INVALID_CONTROL_BEGIN.format(
row=stack[-1]["row_number"], control_type=stack[-1]["control_type"]
)
)

if settings.get("flat", False):
Expand Down
29 changes: 11 additions & 18 deletions tests/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest import TestCase

from pyxform.builder import create_survey_element_from_dict
from pyxform.xls2json import INVALID_CONTROL_BEGIN, INVALID_CONTROL_END
from pyxform.xls2xform import convert

from tests.pyxform_test_case import PyxformTestCase
Expand Down Expand Up @@ -88,7 +89,9 @@ def test_group__no_end_error(self):
| | text | q1 | Q1 |
"""
self.assertPyxformXform(
md=md, errored=True, error__contains=["Unmatched begin statement: group (g1)"]
md=md,
errored=True,
error__contains=[INVALID_CONTROL_BEGIN.format(row=2, control_type="group")],
)

def test_group__no_end_error__different_end_type(self):
Expand All @@ -103,10 +106,7 @@ def test_group__no_end_error__different_end_type(self):
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[
"[row : 4] Unmatched end statement. Previous control type: group, Control "
"type: repeat, Control name: None"
],
error__contains=[INVALID_CONTROL_END.format(row=4, control_type="repeat")],
)

def test_group__no_end_error__with_another_closed_group(self):
Expand All @@ -120,7 +120,9 @@ def test_group__no_end_error__with_another_closed_group(self):
| | end group | | |
"""
self.assertPyxformXform(
md=md, errored=True, error__contains=["Unmatched begin statement: group (g1)"]
md=md,
errored=True,
error__contains=[INVALID_CONTROL_BEGIN.format(row=2, control_type="group")],
)

def test_group__no_begin_error(self):
Expand All @@ -134,10 +136,7 @@ def test_group__no_begin_error(self):
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[
"[row : 3] Unmatched end statement. Previous control type: None, Control "
"type: group, Control name: None"
],
error__contains=[INVALID_CONTROL_END.format(row=3, control_type="group")],
)

def test_group__no_begin_error__with_another_closed_group(self):
Expand All @@ -153,10 +152,7 @@ def test_group__no_begin_error__with_another_closed_group(self):
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[
"[row : 5] Unmatched end statement. Previous control type: None, Control "
"type: group, Control name: None"
],
error__contains=[INVALID_CONTROL_END.format(row=5, control_type="group")],
)

def test_group__no_begin_error__with_another_closed_repeat(self):
Expand All @@ -172,10 +168,7 @@ def test_group__no_begin_error__with_another_closed_repeat(self):
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[
"[row : 4] Unmatched end statement. Previous control type: repeat, Control"
" type: group, Control name: None"
],
error__contains=[INVALID_CONTROL_END.format(row=4, control_type="group")],
)


Expand Down
Loading