diff --git a/pyxform/section.py b/pyxform/section.py index d37a267c4..552e1283c 100644 --- a/pyxform/section.py +++ b/pyxform/section.py @@ -56,7 +56,7 @@ def __init__( ): # Structure self.bind: dict | None = bind - self.children: list[Section | Question] | None = None + self.children: list[Section | Question] = [] self.control: dict | None = control # instance is for custom instance attrs from survey e.g. instance::abc:xyz self.instance: dict | None = instance @@ -73,6 +73,23 @@ def __init__( kwargs.pop(constants.CHILDREN, None) super().__init__(name=name, label=label, fields=fields, **kwargs) + self._link_children() + + def _link_children(self): + for child in self.children: + child.parent = self + + def add_child(self, child): + self.children.append(child) + child.parent = self + + def add_children(self, children): + if isinstance(children, list | tuple): + for child in children: + self.add_child(child) + else: + self.add_child(children) + def validate(self): super().validate() for element in self.children: diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index d694b17a7..263f18d37 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -121,9 +121,6 @@ def __init__( if len(kwargs) > 0: self.extra_data = kwargs - if hasattr(self, const.CHILDREN): - self._link_children() - # Create a space label for unlabeled elements with the label # appearance tag. # This is because such elements are used to label the # options for selects in a field-list and might want blank labels for @@ -141,24 +138,6 @@ def __init__( def name_for_xpath(self) -> str: return self.name - def _link_children(self): - if self.children is not None: - for child in self.children: - child.parent = self - - def add_child(self, child): - if self.children is None: - self.children = [] - self.children.append(child) - child.parent = self - - def add_children(self, children): - if isinstance(children, list | tuple): - for child in children: - self.add_child(child) - else: - self.add_child(children) - def validate(self): if not is_xml_tag(self.name): invalid_char = re.search(INVALID_XFORM_TAG_REGEXP, self.name) diff --git a/tests/test_fieldlist_labels.py b/tests/test_fieldlist_labels.py deleted file mode 100644 index a824791de..000000000 --- a/tests/test_fieldlist_labels.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Test field-list labels -""" - -from tests.pyxform_test_case import PyxformTestCase - - -class FieldListLabels(PyxformTestCase): - """Test unlabeled group""" - - def test_unlabeled_group(self): - self.assertPyxformXform( - md=""" - | survey | | | | - | | type | name | label | - | | begin_group | my-group | | - | | text | my-text | my-text | - | | end_group | | | - """, - warnings_count=1, - warnings__contains=["[row : 2] Group has no label"], - ) - - def test_unlabeled_group_alternate_syntax(self): - self.assertPyxformXform( - md=""" - | survey | | | | - | | type | name | label::English (en) | - | | begin group | my-group | | - | | text | my-text | my-text | - | | end group | | | - """, - warnings_count=1, - warnings__contains=["[row : 2] Group has no label"], - ) - - def test_unlabeled_group_fieldlist(self): - self.assertPyxformXform( - md=""" - | survey | | | | | - | | type | name | label | appearance | - | | begin_group | my-group | | field-list | - | | text | my-text | my-text | | - | | end_group | | | | - """, - warnings_count=0, - xml__xpath_match=[ - """ - /h:html/h:body/x:group[ - @ref = '/test_name/my-group' and @appearance='field-list' - ] - """ - ], - ) - - def test_unlabeled_group_fieldlist_alternate_syntax(self): - self.assertPyxformXform( - md=""" - | survey | | | | | - | | type | name | label | appearance | - | | begin group | my-group | | field-list | - | | text | my-text | my-text | | - | | end group | | | | - """, - warnings_count=0, - ) - - def test_unlabeled_repeat(self): - self.assertPyxformXform( - md=""" - | survey | | | | - | | type | name | label | - | | begin_repeat | my-repeat | | - | | text | my-text | my-text | - | | end_repeat | | | - """, - warnings_count=1, - warnings__contains=["[row : 2] Repeat has no label"], - ) - - def test_unlabeled_repeat_fieldlist(self): - self.assertPyxformXform( - md=""" - | survey | | | | | - | | type | name | label | appearance | - | | begin_repeat | my-repeat | | field-list | - | | text | my-text | my-text | | - | | end_repeat | | | | - """, - warnings_count=1, - warnings__contains=["[row : 2] Repeat has no label"], - ) diff --git a/tests/test_groups.py b/tests/test_group.py similarity index 83% rename from tests/test_groups.py rename to tests/test_group.py index 23e6b49b7..afec55f29 100644 --- a/tests/test_groups.py +++ b/tests/test_group.py @@ -79,6 +79,40 @@ def test_group_relevant_included_in_bind(self): ], ) + def test_table_list_appearance(self): + md = """ + | survey | + | | type | name | label | hint | appearance | + | | begin_group | tablelist1 | Table_Y_N | | table-list minimal | + | | select_one yes_no | options1a | Q1 | first row! | | + | | select_one yes_no | options1b | Q2 | | | + | | end_group | | | | | + | choices | + | | list_name | name | label | + | | yes_no | yes | Yes | + """ + xml_contains = """ + + + + + + + + + + + + + first row! +""".strip() + self.assertPyxformXform( + name="table-list-appearance-mod", + md=md, + xml__contains=[xml_contains], + ) + class TestGroupParsing(PyxformTestCase): def test_names__group_basic_case__ok(self): @@ -576,6 +610,96 @@ def test_group__no_begin_error__with_another_closed_repeat(self): error__contains=[SURVEY_001.format(row=4, type="group")], ) + def test_empty_group__no_question__error(self): + """Should raise an error for an empty group with no questions.""" + md = """ + | survey | + | | type | name | label | + | | begin group | g1 | G1 | + | | end group | | | + """ + self.assertPyxformXform( + md=md, + run_odk_validate=True, # Error about empty groups is from Validate only. + odk_validate_error__contains=[ + "Group has no children! Group: ${g1}. The XML is invalid." + ], + ) + + def test_empty_group__no_question_control__error(self): + """Should raise an error for an empty group with no question controls.""" + md = """ + | survey | + | | type | name | label | calculation | + | | begin group | g1 | G1 | | + | | text | q1 | | 0 + 0 | + | | end group | | | | + """ + self.assertPyxformXform( + md=md, + run_odk_validate=True, # Error about empty groups is from Validate only. + odk_validate_error__contains=[ + "Group has no children! Group: ${g1}. The XML is invalid." + ], + ) + + def test_unlabeled_group(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label | + | | begin_group | my-group | | + | | text | my-text | my-text | + | | end_group | | | + """, + warnings_count=1, + warnings__contains=["[row : 2] Group has no label"], + ) + + def test_unlabeled_group_alternate_syntax(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label::English (en) | + | | begin group | my-group | | + | | text | my-text | my-text | + | | end group | | | + """, + warnings_count=1, + warnings__contains=["[row : 2] Group has no label"], + ) + + def test_unlabeled_group_fieldlist(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label | appearance | + | | begin_group | my-group | | field-list | + | | text | my-text | my-text | | + | | end_group | | | | + """, + warnings_count=0, + xml__xpath_match=[ + """ + /h:html/h:body/x:group[ + @ref = '/test_name/my-group' and @appearance='field-list' + ] + """ + ], + ) + + def test_unlabeled_group_fieldlist_alternate_syntax(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label::English (en) | appearance | + | | begin group | my-group | | field-list | + | | text | my-text | my-text | | + | | end group | | | | + """, + warnings_count=0, + ) + class TestGroupInternalRepresentations(TestCase): maxDiff = None diff --git a/tests/test_repeat.py b/tests/test_repeat.py index 23fbf9e44..ab131a22f 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -1071,6 +1071,90 @@ def test_calculation_using_node_from_nested_repeat_has_relative_reference(self): ], ) + def test_repeat_adding_template_and_instance(self): + """ + Repeat should add template and instances + """ + self.assertPyxformXform( + md=""" + | survey | | | | + | | type | name | label | + | | text | aa | Text AA | + | | begin repeat | section | Section | + | | text | a | Text A | + | | text | b | Text B | + | | text | c | Text C | + | | note | d | Note D | + | | end repeat | | | + | | | | | + | | begin repeat | repeat_a | Section A | + | | begin repeat | repeat_b | Section B | + | | text | e | Text E | + | | begin repeat | repeat_c | Section C | + | | text | f | Text F | + | | end repeat | | | + | | end repeat | | | + | | text | g | Text G | + | | begin repeat | repeat_d | Section D | + | | note | h | Note H | + | | end repeat | | | + | | note | i | Note I | + | | end repeat | | | + """, + xml__xpath_match=[ + "/h:html/h:head/x:model/x:instance/x:test_name/x:section[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:section[not(@jr:template)]", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a[not(@jr:template)]", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b[not(@jr:template)]", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b/x:repeat_c[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b/x:repeat_c[not(@jr:template)]", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_d[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_d[not(@jr:template)]", + ], + ) + + def test_repeat_adding_template_and_instance_with_group(self): + """ + Repeat should add template and instance even when they are inside grouping + """ + self.assertPyxformXform( + md=""" + | survey | | | | + | | type | name | label | + | | text | aa | Text AA | + | | begin repeat | section | Section | + | | text | a | Text A | + | | text | b | Text B | + | | text | c | Text C | + | | note | d | Note D | + | | end repeat | | | + | | | | | + | | begin group | group_a | Group A | + | | begin repeat | repeat_a | Section A | + | | begin repeat | repeat_b | Section B | + | | text | e | Text E | + | | begin group | group_b | Group B | + | | text | f | Text F | + | | text | g | Text G | + | | note | h | Note H | + | | end group | | | + | | note | i | Note I | + | | end repeat | | | + | | end repeat | | | + | | end group | | | + """, + xml__xpath_match=[ + "/h:html/h:head/x:model/x:instance/x:test_name/x:section[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:section[not(@jr:template)]", + "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a[not(@jr:template)]", + "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a/x:repeat_b[@jr:template='']", + "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a/x:repeat_b[not(@jr:template)]", + ], + ) + class TestRepeatParsing(PyxformTestCase): def test_names__repeat_basic_case__ok(self): @@ -1309,3 +1393,334 @@ def test_names__repeat_same_as_repeat_in_different_context_in_repeat__error(self errored=True, error__contains=[unique_names.NAMES004.format(row=7, value="r2")], ) + + def test_empty_repeat__no_question__ok(self): + """Should not raise an error for an empty repeat with no questions.""" + md = """ + | survey | + | | type | name | label | + | | begin repeat | r1 | R1 | + | | end repeat | | | + """ + self.assertPyxformXform( + md=md, + warnings_count=0, + run_odk_validate=True, # Error about empty groups is from Validate only. + xml__xpath_match=[ + """ + /h:html/h:body/x:group[@ref='/test_name/r1'] + /x:repeat[ + @nodeset='/test_name/r1' + and not(./x:input) + ] + """ + ], + ) + + def test_empty_repeat__no_question_control__ok(self): + """Should not raise an error for an empty repeat with no question controls.""" + md = """ + | survey | + | | type | name | label | calculation | + | | begin repeat | r1 | R1 | | + | | text | r1 | | 0 + 0 | + | | end repeat | | | | + """ + self.assertPyxformXform( + md=md, + warnings_count=0, + run_odk_validate=True, # Error about empty groups is from Validate only. + xml__xpath_match=[ + """ + /h:html/h:body/x:group[@ref='/test_name/r1'] + /x:repeat[ + @nodeset='/test_name/r1' + and not(./x:input) + ] + """ + ], + ) + + def test_unlabeled_repeat(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label | + | | begin_repeat | my-repeat | | + | | text | my-text | my-text | + | | end_repeat | | | + """, + warnings_count=1, + warnings__contains=["[row : 2] Repeat has no label"], + ) + + def test_unlabeled_repeat_alternate_syntax(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label::English (en) | + | | begin_repeat | my-repeat | | + | | text | my-text | my-text | + | | end_repeat | | | + """, + warnings_count=1, + warnings__contains=["[row : 2] Repeat has no label"], + ) + + def test_unlabeled_repeat_fieldlist(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label | appearance | + | | begin_repeat | my-repeat | | field-list | + | | text | my-text | my-text | | + | | end_repeat | | | | + """, + warnings_count=1, + warnings__contains=["[row : 2] Repeat has no label"], + ) + + def test_unlabeled_repeat_fieldlist_alternate_syntax(self): + self.assertPyxformXform( + md=""" + | survey | + | | type | name | label::English (en) | appearance | + | | begin_repeat | my-repeat | | field-list | + | | text | my-text | my-text | | + | | end_repeat | | | | + """, + warnings_count=1, + warnings__contains=["[row : 2] Repeat has no label"], + ) + + +class TestRepeatCount(PyxformTestCase): + """ + Test usages of the survey repeat_count column. + """ + + def test_single_reference__generated_element_same_name__ok(self): + """Should not have a name clash, the referenced item should be used directly.""" + md = """ + | survey | + | | type | name | label | repeat_count | + | | integer | q1_count | Q1 | | + | | begin repeat | r1 | R1 | ${q1_count} | + | | text | q2 | Q2 | | + | | end repeat | | | | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + # repeat_count target output as normal + xpq.model_instance_item("q1_count"), + xpq.model_instance_bind("q1_count", "int"), + xpq.body_control("q1_count", "input"), + # no instance element for generated *_count item + """ + /h:html/h:head/x:model/x:instance/x:test_name[not(./x:r1_count)] + """, + # no binding for generated *_count item + """ + /h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/r1_count'])] + """, + # repeat references existing count element directly. + """ + /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ + @nodeset='/test_name/r1' + and @jr:count=' /test_name/q1_count ' + ] + """, + ], + ) + + def test_single_reference__generated_element_different_name__ok(self): + """Should find that a {repeat_name}_count element is generated for the calculation.""" + md = """ + | survey | + | | type | name | label | repeat_count | + | | integer | q1 | Q1 | | + | | begin repeat | r1 | R1 | ${q1} | + | | text | q2 | Q2 | | + | | end repeat | | | | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + # repeat_count target output as normal + xpq.model_instance_item("q1"), + xpq.model_instance_bind("q1", "int"), + xpq.body_control("q1", "input"), + # no instance element for generated *_count item + """ + /h:html/h:head/x:model/x:instance/x:test_name[not(./x:r1_count)] + """, + # no binding for generated *_count item + """ + /h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/r1_count'])] + """, + # repeat references existing count element directly. + """ + /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ + @nodeset='/test_name/r1' + and @jr:count=' /test_name/q1 ' + ] + """, + ], + ) + + def test_expression__generated_element_same_name__error(self): + """Should find that a duplicate {repeat_name}_count element raises an error.""" + md = """ + | survey | + | | type | name | label | repeat_count | + | | select_multiple l1 | r1_count | Q1 | | + | | begin_repeat | r1 | R1 | count-selected(${r1_count}) | + | | text | q2 | Q2 | | + | | end_repeat | | | | + + | choices | + | | list_name | name | label | + | | l1 | c1 | C1 | + | | l1 | c2 | C2 | + """ + self.assertPyxformXform( + md=md, + errored=True, + error__contains=[unique_names.NAMES001.format(value="r1_count")], + ) + + def test_expression__generated_element_different_name__ok(self): + """Should find that a {repeat_name}_count element is generated for the calculation.""" + # repro for pyxform 781 + md = """ + | survey | + | | type | name | label | repeat_count | + | | select_multiple l1 | q1 | Q1 | | + | | begin_repeat | r1 | R1 | count-selected(${q1}) | + | | text | q2 | Q2 | | + | | end_repeat | | | | + + | choices | + | | list_name | name | label | + | | l1 | c1 | C1 | + | | l1 | c2 | C2 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpq.model_instance_item("r1_count"), + xpq.model_instance_bind("r1_count", "string"), + xpq.model_instance_bind_attr( + "r1_count", "calculate", "count-selected( /test_name/q1 )" + ), + xpq.model_instance_bind_attr("r1_count", "readonly", "true()"), + # repeat references generated repeat_count element. + """ + /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ + @nodeset='/test_name/r1' + and @jr:count=' /test_name/r1_count ' + ] + """, + ], + ) + + def test_manual_xpath__generated_element_same_name__error(self): + """Should find that a duplicate {repeat_name}_count element raises an error.""" + md = """ + | survey | + | | type | name | label | repeat_count | + | | select_multiple l1 | r1_count | Q1 | | + | | begin_repeat | r1 | R1 | count-selected( /test_name/r1_count ) | + | | text | q2 | Q2 | | + | | end_repeat | | | | + + | choices | + | | list_name | name | label | + | | l1 | c1 | C1 | + | | l1 | c2 | C2 | + """ + self.assertPyxformXform( + md=md, + errored=True, + error__contains=[unique_names.NAMES001.format(value="r1_count")], + ) + + def test_manual_xpath__generated_element_different_name__ok(self): + """Should find that a {repeat_name}_count element is generated for the calculation.""" + md = """ + | survey | + | | type | name | label | repeat_count | + | | select_multiple l1 | q1 | Q1 | | + | | begin_repeat | r1 | R1 | count-selected( /test_name/q1 ) | + | | text | q2 | Q2 | | + | | end_repeat | | | | + + | choices | + | | list_name | name | label | + | | l1 | c1 | C1 | + | | l1 | c2 | C2 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpq.model_instance_item("r1_count"), + xpq.model_instance_bind("r1_count", "string"), + xpq.model_instance_bind_attr( + "r1_count", "calculate", "count-selected( /test_name/q1 )" + ), + xpq.model_instance_bind_attr("r1_count", "readonly", "true()"), + # repeat references generated repeat_count element. + """ + /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ + @nodeset='/test_name/r1' + and @jr:count=' /test_name/r1_count ' + ] + """, + ], + ) + + def test_constant_integer__generated_element_same_name__error(self): + """Should find that a duplicate {repeat_name}_count element raises an error.""" + # Seems strange, but according to pyxform 435 it's a javarosa limitation. + md = """ + | survey | + | | type | name | label | repeat_count | + | | integer | r1_count | Q1 | | + | | begin_repeat | r1 | R1 | 2 | + | | text | q2 | Q2 | | + | | end_repeat | | | | + """ + self.assertPyxformXform( + md=md, + errored=True, + error__contains=[unique_names.NAMES001.format(value="r1_count")], + ) + + def test_constant_integer__generated_element_different_name__ok(self): + """Should find that a {repeat_name}_count element is generated for the calculation.""" + # Seems strange, but according to pyxform 435 it's a javarosa limitation. + md = """ + | survey | + | | type | name | label | repeat_count | + | | integer | q1 | Q1 | | + | | begin_repeat | r1 | R1 | 2 | + | | text | q2 | Q2 | | + | | end_repeat | | | | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpq.model_instance_item("r1_count"), + xpq.model_instance_bind("r1_count", "string"), + xpq.model_instance_bind_attr("r1_count", "calculate", "2"), + xpq.model_instance_bind_attr("r1_count", "readonly", "true()"), + # repeat references generated repeat_count element. + """ + /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ + @nodeset='/test_name/r1' + and @jr:count=' /test_name/r1_count ' + ] + """, + ], + ) diff --git a/tests/test_repeat_count.py b/tests/test_repeat_count.py deleted file mode 100644 index 03afd0501..000000000 --- a/tests/test_repeat_count.py +++ /dev/null @@ -1,236 +0,0 @@ -from pyxform.validators.pyxform.unique_names import NAMES001 - -from tests.pyxform_test_case import PyxformTestCase -from tests.xpath_helpers.questions import xpq - - -class TestRepeatCount(PyxformTestCase): - """ - Test usages of the survey repeat_count column. - """ - - def test_single_reference__generated_element_same_name__ok(self): - """Should not have a name clash, the referenced item should be used directly.""" - md = """ - | survey | - | | type | name | label | repeat_count | - | | integer | q1_count | Q1 | | - | | begin repeat | r1 | R1 | ${q1_count} | - | | text | q2 | Q2 | | - | | end repeat | | | | - """ - self.assertPyxformXform( - md=md, - xml__xpath_match=[ - # repeat_count target output as normal - xpq.model_instance_item("q1_count"), - xpq.model_instance_bind("q1_count", "int"), - xpq.body_control("q1_count", "input"), - # no instance element for generated *_count item - """ - /h:html/h:head/x:model/x:instance/x:test_name[not(./x:r1_count)] - """, - # no binding for generated *_count item - """ - /h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/r1_count'])] - """, - # repeat references existing count element directly. - """ - /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ - @nodeset='/test_name/r1' - and @jr:count=' /test_name/q1_count ' - ] - """, - ], - ) - - def test_single_reference__generated_element_different_name__ok(self): - """Should find that a {repeat_name}_count element is generated for the calculation.""" - md = """ - | survey | - | | type | name | label | repeat_count | - | | integer | q1 | Q1 | | - | | begin repeat | r1 | R1 | ${q1} | - | | text | q2 | Q2 | | - | | end repeat | | | | - """ - self.assertPyxformXform( - md=md, - xml__xpath_match=[ - # repeat_count target output as normal - xpq.model_instance_item("q1"), - xpq.model_instance_bind("q1", "int"), - xpq.body_control("q1", "input"), - # no instance element for generated *_count item - """ - /h:html/h:head/x:model/x:instance/x:test_name[not(./x:r1_count)] - """, - # no binding for generated *_count item - """ - /h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/r1_count'])] - """, - # repeat references existing count element directly. - """ - /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ - @nodeset='/test_name/r1' - and @jr:count=' /test_name/q1 ' - ] - """, - ], - ) - - def test_expression__generated_element_same_name__error(self): - """Should find that a duplicate {repeat_name}_count element raises an error.""" - md = """ - | survey | - | | type | name | label | repeat_count | - | | select_multiple l1 | r1_count | Q1 | | - | | begin_repeat | r1 | R1 | count-selected(${r1_count}) | - | | text | q2 | Q2 | | - | | end_repeat | | | | - - | choices | - | | list_name | name | label | - | | l1 | c1 | C1 | - | | l1 | c2 | C2 | - """ - self.assertPyxformXform( - md=md, - errored=True, - error__contains=[NAMES001.format(value="r1_count")], - ) - - def test_expression__generated_element_different_name__ok(self): - """Should find that a {repeat_name}_count element is generated for the calculation.""" - # repro for pyxform 781 - md = """ - | survey | - | | type | name | label | repeat_count | - | | select_multiple l1 | q1 | Q1 | | - | | begin_repeat | r1 | R1 | count-selected(${q1}) | - | | text | q2 | Q2 | | - | | end_repeat | | | | - - | choices | - | | list_name | name | label | - | | l1 | c1 | C1 | - | | l1 | c2 | C2 | - """ - self.assertPyxformXform( - md=md, - xml__xpath_match=[ - xpq.model_instance_item("r1_count"), - xpq.model_instance_bind("r1_count", "string"), - xpq.model_instance_bind_attr( - "r1_count", "calculate", "count-selected( /test_name/q1 )" - ), - xpq.model_instance_bind_attr("r1_count", "readonly", "true()"), - # repeat references generated repeat_count element. - """ - /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ - @nodeset='/test_name/r1' - and @jr:count=' /test_name/r1_count ' - ] - """, - ], - ) - - def test_manual_xpath__generated_element_same_name__error(self): - """Should find that a duplicate {repeat_name}_count element raises an error.""" - md = """ - | survey | - | | type | name | label | repeat_count | - | | select_multiple l1 | r1_count | Q1 | | - | | begin_repeat | r1 | R1 | count-selected( /test_name/r1_count ) | - | | text | q2 | Q2 | | - | | end_repeat | | | | - - | choices | - | | list_name | name | label | - | | l1 | c1 | C1 | - | | l1 | c2 | C2 | - """ - self.assertPyxformXform( - md=md, - errored=True, - error__contains=[NAMES001.format(value="r1_count")], - ) - - def test_manual_xpath__generated_element_different_name__ok(self): - """Should find that a {repeat_name}_count element is generated for the calculation.""" - md = """ - | survey | - | | type | name | label | repeat_count | - | | select_multiple l1 | q1 | Q1 | | - | | begin_repeat | r1 | R1 | count-selected( /test_name/q1 ) | - | | text | q2 | Q2 | | - | | end_repeat | | | | - - | choices | - | | list_name | name | label | - | | l1 | c1 | C1 | - | | l1 | c2 | C2 | - """ - self.assertPyxformXform( - md=md, - xml__xpath_match=[ - xpq.model_instance_item("r1_count"), - xpq.model_instance_bind("r1_count", "string"), - xpq.model_instance_bind_attr( - "r1_count", "calculate", "count-selected( /test_name/q1 )" - ), - xpq.model_instance_bind_attr("r1_count", "readonly", "true()"), - # repeat references generated repeat_count element. - """ - /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ - @nodeset='/test_name/r1' - and @jr:count=' /test_name/r1_count ' - ] - """, - ], - ) - - def test_constant_integer__generated_element_same_name__error(self): - """Should find that a duplicate {repeat_name}_count element raises an error.""" - # Seems strange, but according to pyxform 435 it's a javarosa limitation. - md = """ - | survey | - | | type | name | label | repeat_count | - | | integer | r1_count | Q1 | | - | | begin_repeat | r1 | R1 | 2 | - | | text | q2 | Q2 | | - | | end_repeat | | | | - """ - self.assertPyxformXform( - md=md, - errored=True, - error__contains=[NAMES001.format(value="r1_count")], - ) - - def test_constant_integer__generated_element_different_name__ok(self): - """Should find that a {repeat_name}_count element is generated for the calculation.""" - # Seems strange, but according to pyxform 435 it's a javarosa limitation. - md = """ - | survey | - | | type | name | label | repeat_count | - | | integer | q1 | Q1 | | - | | begin_repeat | r1 | R1 | 2 | - | | text | q2 | Q2 | | - | | end_repeat | | | | - """ - self.assertPyxformXform( - md=md, - xml__xpath_match=[ - xpq.model_instance_item("r1_count"), - xpq.model_instance_bind("r1_count", "string"), - xpq.model_instance_bind_attr("r1_count", "calculate", "2"), - xpq.model_instance_bind_attr("r1_count", "readonly", "true()"), - # repeat references generated repeat_count element. - """ - /h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[ - @nodeset='/test_name/r1' - and @jr:count=' /test_name/r1_count ' - ] - """, - ], - ) diff --git a/tests/test_repeat_template.py b/tests/test_repeat_template.py deleted file mode 100644 index 3fb7a9ff7..000000000 --- a/tests/test_repeat_template.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Test repeat template and instance structure. -""" - -from tests.pyxform_test_case import PyxformTestCase - - -class TestRepeatTemplate(PyxformTestCase): - """ - Ensuring the template and instance structure are added correctly. - """ - - def test_repeat_adding_template_and_instance(self): - """ - Repeat should add template and instances - """ - self.assertPyxformXform( - md=""" - | survey | | | | - | | type | name | label | - | | text | aa | Text AA | - | | begin repeat | section | Section | - | | text | a | Text A | - | | text | b | Text B | - | | text | c | Text C | - | | note | d | Note D | - | | end repeat | | | - | | | | | - | | begin repeat | repeat_a | Section A | - | | begin repeat | repeat_b | Section B | - | | text | e | Text E | - | | begin repeat | repeat_c | Section C | - | | text | f | Text F | - | | end repeat | | | - | | end repeat | | | - | | text | g | Text G | - | | begin repeat | repeat_d | Section D | - | | note | h | Note H | - | | end repeat | | | - | | note | i | Note I | - | | end repeat | | | - """, - xml__xpath_match=[ - "/h:html/h:head/x:model/x:instance/x:test_name/x:section[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:section[not(@jr:template)]", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a[not(@jr:template)]", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b[not(@jr:template)]", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b/x:repeat_c[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_b/x:repeat_c[not(@jr:template)]", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_d[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:repeat_a/x:repeat_d[not(@jr:template)]", - ], - ) - - def test_repeat_adding_template_and_instance_with_group(self): - """ - Repeat should add template and instance even when they are inside grouping - """ - self.assertPyxformXform( - md=""" - | survey | | | | - | | type | name | label | - | | text | aa | Text AA | - | | begin repeat | section | Section | - | | text | a | Text A | - | | text | b | Text B | - | | text | c | Text C | - | | note | d | Note D | - | | end repeat | | | - | | | | | - | | begin group | group_a | Group A | - | | begin repeat | repeat_a | Section A | - | | begin repeat | repeat_b | Section B | - | | text | e | Text E | - | | begin group | group_b | Group B | - | | text | f | Text F | - | | text | g | Text G | - | | note | h | Note H | - | | end group | | | - | | note | i | Note I | - | | end repeat | | | - | | end repeat | | | - | | end group | | | - """, - xml__xpath_match=[ - "/h:html/h:head/x:model/x:instance/x:test_name/x:section[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:section[not(@jr:template)]", - "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a[not(@jr:template)]", - "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a/x:repeat_b[@jr:template='']", - "/h:html/h:head/x:model/x:instance/x:test_name/x:group_a/x:repeat_a/x:repeat_b[not(@jr:template)]", - ], - ) diff --git a/tests/test_table_list.py b/tests/test_table_list.py deleted file mode 100644 index 3cee939e4..000000000 --- a/tests/test_table_list.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Test table list appearance syntax. -""" - -from tests.pyxform_test_case import PyxformTestCase - -MD = ''' -| survey | | | | | | -| | type | name | label | hint |appearance | -| | begin_group | tablelist1 | Table_Y_N | |table-list minimal | -| | select_one yes_no | options1a | Q1 | first row! | | -| | select_one yes_no | options1b | Q2 | | | -| | end_group | | | | | -| choices | | | | | | -| | list_name | name | label | | | -| | yes_no | yes | Yes | | | -0 """ # noqa -''' # nopep8 - -XML_CONTAINS = """ - - - - - - - - - - - - - first row! -""".strip() # nopep8 - - -class TableListTest(PyxformTestCase): - def test_table_list(self): - self.assertPyxformXform( - name="table-list-appearance-mod", - md=MD, - xml__contains=[XML_CONTAINS], - )