From 431f49cb5f7b35745b5c867c25d4d9658a901600 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 7 Jan 2026 17:27:45 +1100 Subject: [PATCH] add: allow ${instanceID} reference for longitudinal / entities workflows - add tests for related instance_id / instanceID behaviour - existing test_create_repeat asserts via xpe.model_instance_meta that the entity meta group doesn't contain an instanceID element. --- pyxform/xls2json.py | 2 + tests/test_settings.py | 150 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index 2bac009f..be689c80 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -1423,6 +1423,8 @@ def workbook_to_json( "type": "calculate", } ) + # Allow ${instanceID} reference for longitudinal / entities workflows. + element_names.update(("instanceID",)) if "instance_name" in settings: # Automatically add an instanceName element: diff --git a/tests/test_settings.py b/tests/test_settings.py index d7b2ed28..5aee6172 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -146,6 +146,156 @@ def test_instance_name_from_reference__name_not_found__error(self): ], ) + def test_instance_id__exists_in_survey_meta_by_default(self): + """Should find an instanceID child in the survey-level meta element.""" + md = """ + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """ + /h:html/h:head/x:model/x:instance/x:test_name/x:meta/x:instanceID + """, + """ + /h:html/h:head/x:model/x:bind[ + @nodeset='/test_name/meta/instanceID' + and @type='string' + and @readonly='true()' + and @jr:preload='uid' + ] + """, + ], + ) + + def test_instance_id__bind_can_be_modified_with_setting(self): + """Should find that the instance_id bind can be changed via instance_id setting.""" + md = """ + | settings | + | | instance_id | + | | x | + + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """ + /h:html/h:head/x:model/x:instance/x:test_name/x:meta/x:instanceID + """, + """ + /h:html/h:head/x:model/x:bind[ + @nodeset='/test_name/meta/instanceID' + and @type='string' + and @readonly='true()' + and @jr:preload='x' + ] + """, + ], + ) + + def test_instance_id__can_be_excluded_with_omit_instanceID__no_meta(self): + """Should find that instanceID can be excluded with omit_instanceID setting..""" + md = """ + | settings | + | | omit_instanceID | + | | yes | + + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + # The meta block is not emitted if it would be empty. + """ + /h:html/h:head/x:model/x:instance/x:test_name[not(./x:meta)] + """, + """ + /h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/meta/instanceID'])] + """, + ], + ) + + def test_instance_id__can_be_excluded_with_omit_instanceID__with_meta(self): + """Should find that instanceID can be excluded with omit_instanceID setting..""" + md = """ + | settings | + | | omit_instanceID | instance_name | + | | yes | x | + + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """ + /h:html/h:head/x:model/x:instance/x:test_name/x:meta[not(./x:instanceID)] + """, + """ + /h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/meta/instanceID'])] + """, + ], + ) + + def test_instance_id__can_be_used_as_reference_variable(self): + """Should find that ${instanceID} resolves to the survey-level meta child.""" + md = """ + | survey | + | | type | name | label | calculation | read_only | + | | text | q1 | ${instanceID} | | | + | | text | q2 | Q2 | ${instanceID} | yes | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """ + /h:html/h:head/x:model/x:instance/x:test_name/x:meta/x:instanceID + """, + """ + /h:html/h:body/x:input[@ref='/test_name/q1']/x:label/x:output[ + @value=' /test_name/meta/instanceID ' + ] + """, + """ + /h:html/h:head/x:model/x:bind[ + @nodeset='/test_name/q2' + and @type='string' + and @readonly='true()' + and @calculate=' /test_name/meta/instanceID ' + ] + """, + ], + ) + + def test_instance_id__can_be_used_as_reference_variable__error(self): + """Should find that ${instanceID} resolves to the survey-level meta child.""" + md = """ + | settings | + | | omit_instanceID | + | | yes | + + | survey | + | | type | name | label | calculation | read_only | + | | text | q1 | ${instanceID} | | | + """ + self.assertPyxformXform( + md=md, + errored=True, + error__contains=[ + ErrorCode.PYREF_003.value.format( + sheet="survey", column="label", row=2, q="instanceID" + ), + ], + ) + class TestNamespaces(PyxformTestCase): """