Repository: XLSForm/pyxform (python)

Issue:
Trigger referring to repeat question has relative ref attribute
#### Software and hardware versions 
Last commit in master of Oct 29th, 2020

#### Problem description
If a trigger refers to a question inside a repeat, the `ref` attribute value is relative. Though this is strictly speaking correct in XForms, in pyxform we've so far always used absolute references and could do so in this case too. This would be more consistent. The real problem is that a relative reference doesn't work in Enketo is not so easy to add support for.

#### Steps to reproduce the problem

type | name | label | trigger | calculation
-- | -- | -- | -- | --
begin repeat | rep |   |   |  
text | one | Enter text |   |  
text | three | Enter text (triggered) | ${one} | concat('d', 'e')
end repeat |   |   |   |  

This form doesn't pass ODK Validate (different issue) so run with `--skip_validate`.

The output contains ```<setvalue event="xforms-value-changed" ref=" ../three " value="concat('d', 'e')" />```

Note the inconsistency with the other ref attributes in the same body that are all absolute including the `three` question itself.

#### Desired behavior

The desired output would contain ```<setvalue event="xforms-value-changed" ref=" /data/rep/three " value="concat('d', 'e')" />``` instead. 

#### Additional information

Both `ref="/data/rep/three"` attributes in the (desired) output refer to the same node (in the current repeat instance). They are binding expressions. As far as I can tell they have the exact same meaning in XForms.

Pull request:
Fix 477 trigger in repeat
Closes #
Fix #477  
#### Why is this the best possible solution? Were any other approaches considered?
The simplest one.
#### What are the regression risks?
None.
#### Does this change require updates to documentation? If so, please file an issue [here](https://github.com/XLSForm/xlsform.github.io) and include the link below.
No.
#### Before submitting this PR, please make sure you have:
- [x] included test cases for core behavior and edge cases in `tests_v1`
- [x] run `nosetests` and verified all tests pass
- [x] run `black pyxform` to format code
- [x] verified that any code or assets from external sources are properly credited in comments

Patch:
diff --git a/pyxform/question.py b/pyxform/question.py
index 03460d4..ed93215 100644
--- a/pyxform/question.py
+++ b/pyxform/question.py
@@ -58,7 +58,9 @@ class Question(SurveyElement):
         if nested_setvalues:
             for setvalue in nested_setvalues:
                 setvalue_attrs = {
-                    "ref": self.get_root().insert_xpaths("${%s}" % setvalue[0], self),
+                    "ref": self.get_root()
+                    .insert_xpaths("${%s}" % setvalue[0], self.get_root())
+                    .strip(),
                     "event": "xforms-value-changed",
                 }
                 if not (setvalue[1] == ""):


Test patch:
diff --git a/pyxform/tests_v1/test_trigger.py b/pyxform/tests_v1/test_trigger.py
index e2f6ebc..d3be5b5 100644
--- a/pyxform/tests_v1/test_trigger.py
+++ b/pyxform/tests_v1/test_trigger.py
@@ -50,7 +50,7 @@ class TriggerSetvalueTests(PyxformTestCase):
             xml__contains=[
                 '<bind nodeset="/trigger-column/b" type="dateTime"/>',
                 '<input ref="/trigger-column/a">',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/b " value="now()"/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/b" value="now()"/>',
             ],
             xml__excludes=[
                 '<bind nodeset="/trigger-column/b" type="dateTime" calculate="now()"/>'
@@ -75,7 +75,7 @@ class TriggerSetvalueTests(PyxformTestCase):
                 '<bind nodeset="/trigger-column/c" type="dateTime"/>',
                 '<input ref="/trigger-column/a">',
                 '<input ref="/trigger-column/c">',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/c " value="now()"/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/c" value="now()"/>',
             ],
             xml__excludes=[
                 '<bind nodeset="/trigger-column/c" type="dateTime" calculate="now()"/>'
@@ -101,8 +101,8 @@ class TriggerSetvalueTests(PyxformTestCase):
                 '<bind nodeset="/trigger-column/c" type="dateTime"/>',
                 '<input ref="/trigger-column/a">',
                 '<input ref="/trigger-column/c">',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/b " value="1+1"/>',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/c " value="now()"/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/b" value="1+1"/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/c" value="now()"/>',
             ],
             xml__excludes=[
                 '<bind nodeset="/trigger-column/b" type="int" calculate="1+1"/>',
@@ -127,7 +127,7 @@ class TriggerSetvalueTests(PyxformTestCase):
                 '<bind nodeset="/trigger-column/d" type="dateTime"/>',
                 '<input ref="/trigger-column/a">',
                 '<input ref="/trigger-column/d">',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/d "/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/d"/>',
             ],
             xml__excludes=[
                 '<bind nodeset="/trigger-column/d" type="dateTime" calculate=""/>'
@@ -150,7 +150,7 @@ class TriggerSetvalueTests(PyxformTestCase):
             xml__contains=[
                 '<bind nodeset="/trigger-column/e" type="decimal"/>',
                 '<input ref="/trigger-column/a">',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/e "/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/e"/>',
             ],
             xml__excludes=[
                 '<bind nodeset="/trigger-column/e" type="decimal" calculate=""/>',
@@ -209,7 +209,7 @@ class TriggerSetvalueTests(PyxformTestCase):
                 '<input ref="/trigger-column/a">',
                 '<group ref="/trigger-column/grp">',
                 '<input ref="/trigger-column/grp/c">',
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/grp/c " value="now()"/>',
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/grp/c" value="now()"/>',
             ],
             xml__excludes=[
                 '<bind nodeset="/trigger-column/c" type="dateTime" calculate="now()"/>',
@@ -226,7 +226,7 @@ class TriggerSetvalueTests(PyxformTestCase):
             |        | integer  | b    |             | decimal-date-time(${a}) | ${a}    |
             """,
             xml__contains=[
-                '<setvalue event="xforms-value-changed" ref=" /trigger-column/b " value="decimal-date-time( /trigger-column/a )"/>'
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/b" value="decimal-date-time( /trigger-column/a )"/>'
             ],
         )
 
@@ -244,6 +244,38 @@ class TriggerSetvalueTests(PyxformTestCase):
             |        | choices            | aa   | AA          |
             """,
             xml__contains=[
-                '<setvalue event="xforms-value-changed" ref=" /trigger-select_trigger/b " value="string-length( /trigger-select_trigger/a )"/>\n    </select1>'
+                '<setvalue event="xforms-value-changed" ref="/trigger-select_trigger/b" value="string-length( /trigger-select_trigger/a )"/>\n    </select1>'
+            ],
+        )
+
+    def test_trigger_column_in_repeat_should_have_expanded_xpath(self):
+        self.assertPyxformXform(
+            name="trigger-column",
+            md="""
+            | survey |              |       |                        |              |         |
+            |        | type         | name  | label                  | calculation  | trigger |
+            |        | begin repeat | rep   |                        |              |         |
+            |        | dateTime     | one   | Enter text             |              |         |
+            |        | dateTime     | three | Enter text (triggered) | now()        | ${one}  |
+            |        | end repeat   |       |                        |              |         |
+            """,
+            xml__contains=[
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/rep/three" value="now()"/>'
+            ],
+        )
+
+    def test_trigger_column_in_repeat_should_have_expanded_xpath_in_value(self):
+        self.assertPyxformXform(
+            name="trigger-column",
+            md="""
+            | survey |              |       |                        |                        |         |
+            |        | type         | name  | label                  | calculation            | trigger |
+            |        | begin repeat | rep   |                        |                        |         |
+            |        | dateTime     | one   | Enter text             |                        |         |
+            |        | dateTime     | three | Enter text (triggered) | string-length(${one})  | ${one}  |
+            |        | end repeat   |       |                        |                        |         |
+            """,
+            xml__contains=[
+                '<setvalue event="xforms-value-changed" ref="/trigger-column/rep/three" value="string-length( ../one )"/>'
             ],
         )