diff --git a/pyxform/question.py b/pyxform/question.py index b69dc54b..f870d308 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -668,6 +668,27 @@ def build_xml(self, survey: "Survey"): class GeoQuestion(_SupportsItemset): + def _build_itemset_reference( + self, question: "Question", survey: "Survey" + ) -> ItemsetNode | None: + """Build a geo-specific itemset node using a reference to a repeat.""" + if self.itemset_has_ref: + path = survey.insert_xpaths( + text=self.itemset, context=question, reference_parent=True + ).strip() + # A default choice_filter is added here as a workaround for Collect appending + # an index predicate (e.g. "/data/r1[1]") to absolute references in a repeat + # context (the whole nodeset is desired for the user to choose from). Assumes + # that Collect will not alter or replace an existing predicate, and also that + # pyxform emits an absolute reference for this topology. See pyxform/#848. + choice_filter = "./geometry != ''" + return ItemsetNode( + value_ref="geometry", + label_ref="geometry", + nodeset=path, + choice_filter=choice_filter, + ) + def build_xml(self, survey: "Survey"): result = self._build_xml(survey=survey) diff --git a/tests/test_geo.py b/tests/test_geo.py index eaab397b..6121ca1f 100644 --- a/tests/test_geo.py +++ b/tests/test_geo.py @@ -522,15 +522,15 @@ def test_entity_list__choice_filter__ok(self): ) def test_pyxform_reference__ok(self): - """Should find that a child itemset is emitted, with reference-specific value/label.""" + """Should find that a child itemset is emitted, with geometry value/label.""" # RG005 RG007 RG010 RG016 md = """ | survey | - | | type | name | label | parameters | - | | begin_repeat | r1 | R1 | | - | | text | q1 | Q1 | | - | | end_repeat | r1 | | | - | | {type} | q2 | Q2 | reference-geometry=${{r1}} | + | | type | name | label | parameters | + | | begin_repeat | r1 | R1 | | + | | text | geometry | Q1 | | + | | end_repeat | r1 | | | + | | {type} | q2 | Q2 | reference-geometry=${{r1}} | """ for t in GEO_TYPES: with self.subTest(t): @@ -541,9 +541,9 @@ def test_pyxform_reference__ok(self): xpq.model_instance_bind("q2", t), xpq.body_itemset( q_name="q2", - nodeset="/test_name[./r1 != '']", - value_ref="r1", - label_ref="r1", + nodeset="/test_name/r1[./geometry != '']", + value_ref="geometry", + label_ref="geometry", extra_q_assertions="and not(@reference-geometry)", ), ], @@ -554,11 +554,12 @@ def test_pyxform_reference__choice_filter__ok(self): # RG005 RG007 RG010 RG014 RG016 md = """ | survey | - | | type | name | label | parameters | choice_filter | - | | begin_repeat | r1 | R1 | | | - | | text | q1 | Q1 | | | - | | end_repeat | r1 | | | | - | | {type} | q2 | Q2 | reference-geometry=${{r1}} | ${{q1}} = 1 | + | | type | name | label | parameters | choice_filter | + | | begin_repeat | r1 | R1 | | | + | | geopoint | geometry | Q1 | | | + | | text | q2 | Q2 | + | | end_repeat | r1 | | | | + | | {type} | q3 | Q3 | reference-geometry=${{r1}} | ${{q2}} = 1 | """ for t in GEO_TYPES: with self.subTest(t): @@ -566,13 +567,41 @@ def test_pyxform_reference__choice_filter__ok(self): md=md.format(type=t), xml__xpath_match=[ xpq.model_instance_item("r1[not(@jr:template)]"), - xpq.model_instance_bind("q2", t), + xpq.model_instance_bind("q3", t), xpq.body_itemset( - q_name="q2", - nodeset="/test_name[ ./r1/q1 = 1]", - value_ref="r1", - label_ref="r1", + q_name="q3", + nodeset="/test_name/r1[ ./q2 = 1]", + value_ref="geometry", + label_ref="geometry", + extra_q_assertions="and not(@reference-geometry)", + ), + ], + ) + + def test_pyxform_reference__inside_repeat__ok(self): + """Should find that a child itemset is emitted, with geometry value/label.""" + # RG005 RG007 RG010 RG016 + md = """ + | survey | + | | type | name | label | parameters | + | | begin_repeat | r1 | R1 | | + | | {type} | geometry | Q1 | reference-geometry=${{r1}} | + | | end_repeat | r1 | | | + """ + for t in GEO_TYPES: + with self.subTest(t): + self.assertPyxformXform( + md=md.format(type=t), + xml__xpath_match=[ + xpq.model_instance_item("r1[not(@jr:template)]"), + xpq.model_instance_bind("r1/geometry", t), + xpq.body_itemset( + q_name="r1/geometry", + nodeset="/test_name/r1[./geometry != '']", + value_ref="geometry", + label_ref="geometry", extra_q_assertions="and not(@reference-geometry)", + body_path="/x:group/x:repeat", ), ], ) @@ -677,6 +706,39 @@ def test_select_from_file__choice_filter__ok(self): ], ) + def test_select_from_file__params_value_label__ok(self): + """Should find that a child itemset is emitted, with default value/label.""" + # RG005 RG006 RG012 RG016 + md = """ + | survey | + | | type | name | label | parameters | + | | select_one_from_file s1{ext} | q1 | Q1 | value=v, label=l | + | | {type} | q2 | Q2 | reference-geometry=s1 | + """ + for t in GEO_TYPES: + for ext in co.EXTERNAL_INSTANCE_EXTENSIONS: + with self.subTest((t, ext)): + self.assertPyxformXform( + md=md.format(type=t, ext=ext), + xml__xpath_match=[ + xpq.model_instance_exists("s1"), + # The "select from file" params are separate to reference-geometry. + xpq.body_itemset( + q_name="q1", + q_type="select1", + nodeset="instance('s1')/root/item", + value_ref="v", + label_ref="l", + ), + xpq.model_instance_bind("q2", t), + xpq.body_itemset( + q_name="q2", + nodeset="instance('s1')/root/item", + extra_q_assertions="and not(@reference-geometry)", + ), + ], + ) + def test_select_one_external__ok(self): """Should find that a child itemset is emitted, with default value/label.""" # RG005 RG006 RG011 RG015 RG016 diff --git a/tests/xpath_helpers/questions.py b/tests/xpath_helpers/questions.py index 30de9053..c1a66611 100644 --- a/tests/xpath_helpers/questions.py +++ b/tests/xpath_helpers/questions.py @@ -111,10 +111,11 @@ def body_itemset( value_ref: str = "name", label_ref: str = "label", extra_q_assertions: str = "", + body_path: str = "", ) -> str: """Body has a direct child control with an itemset, and no inline items.""" return rf""" - /h:html/h:body/x:{q_type}[ + /h:html/h:body{body_path}/x:{q_type}[ @ref="/test_name/{q_name}" and ./x:itemset[@nodeset="{nodeset}"] and ./x:itemset/x:value[@ref="{value_ref}"]