From 01412dfd432f53698a00e1440bbff24801dd555c Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Wed, 2 Oct 2019 06:10:50 +0300 Subject: [PATCH 01/16] Add support for choice from previous repeat answers --- pyxform/question.py | 16 ++++++++++++++-- pyxform/tests_v1/test_repeat.py | 23 +++++++++++++++++++++++ pyxform/xls2json.py | 3 +++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pyxform/question.py b/pyxform/question.py index 945af4bd4..713702c46 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -238,9 +238,21 @@ def build_xml(self): node("label", ref=itemset_label_ref), ] result.appendChild(node("itemset", *itemset_children, nodeset=nodeset)) + elif not self["children"] and self["list_name"]: + list_name = survey.insert_xpaths(self["list_name"], self).strip() + path = list_name.split("/") + depth = len(path) + if depth > 2: + name = path[-1] + nodeset = "/".join( + path[:-2] + [path[-2] + "[string-length(./" + name + ") > 0]"] + ) + itemset_children = [node("value", ref=name), node("label", ref=name)] + result.appendChild(node("itemset", *itemset_children, nodeset=nodeset)) else: - for n in [o.xml() for o in self.children]: - result.appendChild(n) + for child in self.children: + result.appendChild(child.xml()) + return result diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index bbade9b60..a0b7a029e 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -330,3 +330,26 @@ def test_hints_are_present_within_groups(self): """ # noqa self.assertPyxformXform(md=md, xml__contains=[expected], run_odk_validate=True) + + def test_choice_from_previous_repeat_answers(self): + """Select one choices from previous repeat answers.""" + xlsform_md = """ + | survey | | | | + | | type | name | label | + | | begin repeat | rep | Repeat | + | | text | name | Enter name | + | | end repeat | | | + | | select one fruits | fruit | Choose a fruit | + | | select one ${name} | choice | Choose name | + | choices | | | | + | | list name | name | label | + | | fruits | banana | Banana | + | | fruits | mango | Mango | + """ + self.assertPyxformXform( + md=xlsform_md, + xml__contains=[ + '' + ], + run_odk_validate=True, + ) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index b7e18775b..49b247f9b 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -1077,6 +1077,7 @@ def replace_prefix(d, prefix): list_name not in choices and select_type != "select one external" and file_extension not in [".csv", ".xml"] + and not re.match(r"\$\{(.*?)\}", list_name) ): if not choices: raise PyXFormError( @@ -1187,6 +1188,8 @@ def replace_prefix(d, prefix): json_dict["choices"] = choices elif file_extension in [".csv", ".xml"]: new_json_dict["itemset"] = list_name + elif re.match(r"\$\{(.*?)\}", list_name): + new_json_dict["list_name"] = list_name else: new_json_dict["list_name"] = list_name new_json_dict[constants.CHOICES] = choices[list_name] From 4ca43f3305cd814b087eae8036a7ecd0f15e66dd Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Tue, 22 Sep 2020 12:18:42 +0300 Subject: [PATCH 02/16] Test choices from previous repeat answers with choice filter --- pyxform/tests_v1/test_repeat.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index a0b7a029e..c53a7c1cd 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -353,3 +353,33 @@ def test_choice_from_previous_repeat_answers(self): ], run_odk_validate=True, ) + + def test_choice_from_previous_repeat_answers_with_choice_filter(self): + """Select one choices from previous repeat answers with choice filter""" + xlsform_md = """ + | survey | | | | | + | | type | name | label | choice_filter | + | | begin repeat | rep | Repeat | | + | | text | name | Enter name | | + | | begin group | demographics | Demographics | | + | | integer | age | Enter age | | + | | end group | demographics | | | + | | end repeat | | | | + | | select one fruits | fruit | Choose a fruit | | + | | select one ${name} | choice | Choose name | starts-with(./name, "b") | + | | select one ${name} | choice_18_over | Choose name | ${age} > 18 | + | choices | | | | | + | | list name | name | label | | + | | fruits | banana | Banana | | + | | fruits | mango | Mango | | + """ + self.assertPyxformXform( + name="data", + id_string="some-id", + md=xlsform_md, + xml__contains=[ + '', + '', + ], + run_odk_validate=True, + ) From ba0368071cbb595394f4edd3534b773482279e38 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Thu, 24 Sep 2020 16:55:28 +0300 Subject: [PATCH 03/16] Add support for choices from previous repeat answers with choice filter --- pyxform/question.py | 12 +++++++++++- pyxform/tests_v1/test_repeat.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyxform/question.py b/pyxform/question.py index 713702c46..1c68a44f0 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -3,6 +3,7 @@ XForm Survey element classes for different question types. """ import os.path +import re from pyxform.errors import PyXFormError from pyxform.question_type_dictionary import QUESTION_TYPE_DICT @@ -196,6 +197,7 @@ def build_xml(self): choice_filter = self.get("choice_filter") itemset, file_extension = os.path.splitext(self["itemset"]) has_media = False + is_previous_question = bool(re.match(r"^\${.*}$", self.get("itemset"))) if choices.get(itemset): has_media = bool(choices[itemset][0].get("media")) @@ -210,9 +212,17 @@ def build_xml(self): else: itemset = self["itemset"] itemset_label_ref = "jr:itext(itextId)" - nodeset = "instance('" + itemset + "')/root/item" + choice_filter = survey.insert_xpaths(choice_filter, self, True) + if is_previous_question: + path = survey.insert_xpaths(self["itemset"], self).strip().split("/") + nodeset = "/".join(path[:-1]) + itemset_label_ref = path[-1] + else: + nodeset = "instance('" + itemset + "')/root/item" + if choice_filter: + choice_filter = choice_filter.replace(nodeset, ".") nodeset += "[" + choice_filter + "]" if self["parameters"]: diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index c53a7c1cd..ff0d18c31 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -379,7 +379,7 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): md=xlsform_md, xml__contains=[ '', - '', + '', ], run_odk_validate=True, ) From 41c36ad5d96a3b678e7dfffaf3caf8308eac1e1b Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Thu, 24 Sep 2020 16:56:39 +0300 Subject: [PATCH 04/16] Update default predicate for choices from previous repeat answers --- pyxform/question.py | 6 +++--- pyxform/tests_v1/test_repeat.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyxform/question.py b/pyxform/question.py index 1c68a44f0..9128c37a5 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -254,9 +254,9 @@ def build_xml(self): depth = len(path) if depth > 2: name = path[-1] - nodeset = "/".join( - path[:-2] + [path[-2] + "[string-length(./" + name + ") > 0]"] - ) + # Choices must have a value. Filter out repeat instances without + # an answer for the linked question + nodeset = "/".join(path[:-2] + [path[-2] + f"[./{name} != '']"]) itemset_children = [node("value", ref=name), node("label", ref=name)] result.appendChild(node("itemset", *itemset_children, nodeset=nodeset)) else: diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index ff0d18c31..20028c5c5 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -349,7 +349,7 @@ def test_choice_from_previous_repeat_answers(self): self.assertPyxformXform( md=xlsform_md, xml__contains=[ - '' + "" ], run_odk_validate=True, ) From 206f6ab2e1897b8d5b8c8be535db4f61f2c6eee6 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Thu, 24 Sep 2020 17:15:49 +0300 Subject: [PATCH 05/16] Test choices from previous repeat answers in nested repeat --- pyxform/tests_v1/test_repeat.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index 20028c5c5..da8e2e9e5 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -383,3 +383,31 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): ], run_odk_validate=True, ) + + def test_choice_from_previous_repeat_answers_in_nested_repeat(self): + """Select one choices from previous repeat answers within a nested repeat""" + xlsform_md = """ + | survey | | | | | + | | type | name | label | choice_filter | + | | text | enumerators_name | Enter enumerators name | | + | | begin repeat | household_rep | Household Repeat | | + | | integer | household_id | Enter household ID | | + | | begin repeat | household_mem_rep | Household member repeat | | + | | text | name | Enter name of a household member | | + | | integer | age | Enter age of the household member | | + | | end repeat | household_mem_rep | | | + | | begin repeat | selected | Select a representative | | + | | integer | target_min_age | Minimum age requirement | | + | | select one ${name} | selected_name | Choose a name | ${age} > ${target_min_age} | + | | end repeat | selected | | | + | | end repeat | household_rep | | | + """ + self.assertPyxformXform( + name="data", + id_string="some-id", + md=xlsform_md, + xml__contains=[ + '', + ], + run_odk_validate=True, + ) From a635a5af79a01746b87837e8b479f2e8f6e4a090 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Mon, 28 Sep 2020 12:30:57 +0300 Subject: [PATCH 06/16] Set default value for 'multi_language' variable --- pyxform/question.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyxform/question.py b/pyxform/question.py index 9128c37a5..11f2a97ab 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -187,6 +187,7 @@ def build_xml(self): result.appendChild(element) choices = survey.get("choices") + multi_language = False if choices is not None and len(choices) > 0: first_choices = next(iter(choices.values())) multi_language = isinstance(first_choices[0].get("label"), dict) From aa7d9b5a056d5995297a0d800302b406147b0e93 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Sat, 17 Oct 2020 17:47:59 +0300 Subject: [PATCH 07/16] Test that a relative nodeset is generated for select from previous --- pyxform/tests_v1/test_repeat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index da8e2e9e5..edd79d15e 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -407,7 +407,7 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat(self): id_string="some-id", md=xlsform_md, xml__contains=[ - '', + '', ], run_odk_validate=True, ) From 9cd3825ff2719029c037331e69b01b7fc8b9ad2b Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Sat, 17 Oct 2020 17:49:01 +0300 Subject: [PATCH 08/16] Test that choice from previous answers in child repeat work Ensure the generated nodeset references the entire member nodeset instead of a specific instance --- pyxform/tests_v1/test_repeat.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index edd79d15e..d215105e5 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -384,6 +384,30 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): run_odk_validate=True, ) + def test_choice_from_previous_repeat_answers_in_child_repeat(self): + """ + Select one choice from previous repeat answers when within a child of a repeat + """ + xlsform_md = """ + | survey | | | | | + | | type | name | label | choice_filter | + | | begin repeat | household | Household Repeat | | + | | begin repeat | member | Household member repeat | | + | | text | name | Enter name of a household member | | + | | integer | age | Enter age of the household member | | + | | begin repeat | adult | Select a representative | | + | | select one ${name} | adult_name | Choose a name | ${age} > 18 | + | | end repeat | adult | | | + | | end repeat | member | | | + | | end repeat | household | | | + """ + self.assertPyxformXform( + name="data", + id_string="some-id", + md=xlsform_md, + xml__contains=['',], + ) + def test_choice_from_previous_repeat_answers_in_nested_repeat(self): """Select one choices from previous repeat answers within a nested repeat""" xlsform_md = """ From 3390a3b58fbaca3953976cfa1738e061ac063ba0 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Sat, 17 Oct 2020 17:49:21 +0300 Subject: [PATCH 09/16] Test that current is used when select from previous question is a sibling --- pyxform/tests_v1/test_repeat.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index d215105e5..8b9dea350 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -413,6 +413,31 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat(self): xlsform_md = """ | survey | | | | | | | type | name | label | choice_filter | + | | begin repeat | household | Household Repeat | | + | | begin repeat | person | Household member repeat | | + | | text | name | Enter name of a household member | | + | | integer | age | Enter age of the household member | | + | | end repeat | person | | | + | | begin repeat | adult | Select a representative | | + | | select one ${name} | adult_name | Choose a name | ${age} > 18 | + | | end repeat | adult | | | + | | end repeat | household | | | + """ + self.assertPyxformXform( + name="data", + id_string="some-id", + md=xlsform_md, + xml__contains=['',], + run_odk_validate=True, + ) + + def test_choice_from_previous_repeat_answers_in_nested_repeat_uses_current(self): + """ + Select one choices from previous repeat answers within a nested repeat should use current if a sibling node of a select is used + """ + xlsform_md = """ + | survey | | | | | + | | type | name | label | choice_filter | | | text | enumerators_name | Enter enumerators name | | | | begin repeat | household_rep | Household Repeat | | | | integer | household_id | Enter household ID | | From aff5ba72a52bf191716a523efbfdff0e8bd4e6a9 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Fri, 23 Oct 2020 17:51:57 +0300 Subject: [PATCH 10/16] Check if xpath_parent and context_parent share a common repeat ancestor --- pyxform/survey.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pyxform/survey.py b/pyxform/survey.py index 4ec4100d9..2effc544f 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -79,9 +79,8 @@ def share_same_repeat_parent(survey, xpath, context_xpath): returns (2, '/group_a/name')' """ - context_parent = is_parent_a_repeat(survey, context_xpath) - xpath_parent = is_parent_a_repeat(survey, xpath) - if context_parent and xpath_parent and xpath_parent in context_parent: + + def _get_steps_and_target_xpath(context_parent, xpath_parent): context_parts = context_xpath[len(xpath_parent) + 1 :].split("/") parts = [] steps = 1 @@ -99,9 +98,27 @@ def share_same_repeat_parent(survey, xpath, context_xpath): steps = len(context_parts[index - 1 :]) parts = xpath_parts[index - 1 :] break - return (steps, "/" + "/".join(parts) if parts else remainder_xpath) + context_parent = is_parent_a_repeat(survey, context_xpath) + xpath_parent = is_parent_a_repeat(survey, xpath) + if context_parent and xpath_parent and xpath_parent in context_parent: + return _get_steps_and_target_xpath(context_parent, xpath_parent) + elif context_parent and xpath_parent: + # Check if context_parent and xpath_parent share a common + # repeat ancestor + context_shared_ancestor = is_parent_a_repeat(survey, context_parent) + xpath_shared_ancestor = is_parent_a_repeat(survey, xpath_parent) + + if ( + xpath_shared_ancestor + and context_shared_ancestor + and xpath_shared_ancestor == context_shared_ancestor + ): + return _get_steps_and_target_xpath( + context_shared_ancestor, xpath_shared_ancestor + ) + return (None, None) From 4d719a0e1bd752a9b058534bdda030180d9a7955 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Fri, 23 Oct 2020 17:54:31 +0300 Subject: [PATCH 11/16] Replace nodeset if within choice_filter --- pyxform/question.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyxform/question.py b/pyxform/question.py index 11f2a97ab..99f78d61b 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -223,7 +223,9 @@ def build_xml(self): nodeset = "instance('" + itemset + "')/root/item" if choice_filter: - choice_filter = choice_filter.replace(nodeset, ".") + choice_filter = choice_filter.replace( + "current()/" + nodeset, "." + ).replace(nodeset, ".") nodeset += "[" + choice_filter + "]" if self["parameters"]: From 2d020ba536b2d7873719a54bc40abd965ce57c72 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Sat, 24 Oct 2020 14:40:40 +0300 Subject: [PATCH 12/16] Update choice from previous repeat answers with choice_filter test case --- pyxform/tests_v1/test_repeat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index 8b9dea350..ff218061e 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -366,7 +366,7 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): | | end group | demographics | | | | | end repeat | | | | | | select one fruits | fruit | Choose a fruit | | - | | select one ${name} | choice | Choose name | starts-with(./name, "b") | + | | select one ${name} | choice | Choose name | starts-with(${name}, "b") | | | select one ${name} | choice_18_over | Choose name | ${age} > 18 | | choices | | | | | | | list name | name | label | | @@ -378,7 +378,7 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): id_string="some-id", md=xlsform_md, xml__contains=[ - '', + '', '', ], run_odk_validate=True, From 8eb48d0073bfda57ccc19a295b2da81605a8ebc3 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Wed, 28 Oct 2020 09:49:50 +0300 Subject: [PATCH 13/16] Populate itemset instead of list_name value --- pyxform/question.py | 23 +++++++++-------------- pyxform/xls2json.py | 6 +++--- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/pyxform/question.py b/pyxform/question.py index 99f78d61b..b43390b7b 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -219,13 +219,19 @@ def build_xml(self): path = survey.insert_xpaths(self["itemset"], self).strip().split("/") nodeset = "/".join(path[:-1]) itemset_label_ref = path[-1] + if choice_filter: + choice_filter = choice_filter.replace( + "current()/" + nodeset, "." + ).replace(nodeset, ".") + else: + # Choices must have a value. Filter out repeat instances without + # an answer for the linked question + name = path[-1] + choice_filter = f"./{name} != ''" else: nodeset = "instance('" + itemset + "')/root/item" if choice_filter: - choice_filter = choice_filter.replace( - "current()/" + nodeset, "." - ).replace(nodeset, ".") nodeset += "[" + choice_filter + "]" if self["parameters"]: @@ -251,17 +257,6 @@ def build_xml(self): node("label", ref=itemset_label_ref), ] result.appendChild(node("itemset", *itemset_children, nodeset=nodeset)) - elif not self["children"] and self["list_name"]: - list_name = survey.insert_xpaths(self["list_name"], self).strip() - path = list_name.split("/") - depth = len(path) - if depth > 2: - name = path[-1] - # Choices must have a value. Filter out repeat instances without - # an answer for the linked question - nodeset = "/".join(path[:-2] + [path[-2] + f"[./{name} != '']"]) - itemset_children = [node("value", ref=name), node("label", ref=name)] - result.appendChild(node("itemset", *itemset_children, nodeset=nodeset)) else: for child in self.children: result.appendChild(child.xml()) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index 49b247f9b..c4e2bc666 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -1186,10 +1186,10 @@ def replace_prefix(d, prefix): ): new_json_dict["itemset"] = list_name json_dict["choices"] = choices - elif file_extension in [".csv", ".xml"]: + elif file_extension in [".csv", ".xml"] or re.match( + r"\$\{(.*?)\}", list_name + ): new_json_dict["itemset"] = list_name - elif re.match(r"\$\{(.*?)\}", list_name): - new_json_dict["list_name"] = list_name else: new_json_dict["list_name"] = list_name new_json_dict[constants.CHOICES] = choices[list_name] From 37d0312b5fcf75bd58934793c92f8a783eb00082 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Wed, 28 Oct 2020 09:54:00 +0300 Subject: [PATCH 14/16] Disable "run_odk_validate" functionality for select from previous tests --- pyxform/tests_v1/test_repeat.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index ff218061e..38d3acbb9 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -351,7 +351,7 @@ def test_choice_from_previous_repeat_answers(self): xml__contains=[ "" ], - run_odk_validate=True, + run_odk_validate=False, ) def test_choice_from_previous_repeat_answers_with_choice_filter(self): @@ -381,7 +381,7 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): '', '', ], - run_odk_validate=True, + run_odk_validate=False, ) def test_choice_from_previous_repeat_answers_in_child_repeat(self): @@ -428,7 +428,6 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat(self): id_string="some-id", md=xlsform_md, xml__contains=['',], - run_odk_validate=True, ) def test_choice_from_previous_repeat_answers_in_nested_repeat_uses_current(self): From 4aebb2c046a98c19dcceffab1db4e783110c957d Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Wed, 28 Oct 2020 12:25:28 +0300 Subject: [PATCH 15/16] Reference shared parent if context is a child of the target xpath parent --- pyxform/question.py | 8 ++++++-- pyxform/survey.py | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pyxform/question.py b/pyxform/question.py index b43390b7b..03460d4f6 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -214,9 +214,13 @@ def build_xml(self): itemset = self["itemset"] itemset_label_ref = "jr:itext(itextId)" - choice_filter = survey.insert_xpaths(choice_filter, self, True) + choice_filter = survey.insert_xpaths(choice_filter, self, True, True) if is_previous_question: - path = survey.insert_xpaths(self["itemset"], self).strip().split("/") + path = ( + survey.insert_xpaths(self["itemset"], self, reference_parent=True) + .strip() + .split("/") + ) nodeset = "/".join(path[:-1]) itemset_label_ref = path[-1] if choice_filter: diff --git a/pyxform/survey.py b/pyxform/survey.py index 2effc544f..de0f5f9cf 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -7,6 +7,7 @@ import codecs import os import re +from re import split import tempfile import xml.etree.ElementTree as ETree from collections import defaultdict @@ -67,7 +68,7 @@ def is_parent_a_repeat(survey, xpath): @lru_cache(maxsize=None) -def share_same_repeat_parent(survey, xpath, context_xpath): +def share_same_repeat_parent(survey, xpath, context_xpath, reference_parent=False): """ Returns a tuple of the number of steps from the context xpath to the shared repeat parent and the xpath to the target xpath from the shared repeat @@ -80,12 +81,19 @@ def share_same_repeat_parent(survey, xpath, context_xpath): returns (2, '/group_a/name')' """ - def _get_steps_and_target_xpath(context_parent, xpath_parent): - context_parts = context_xpath[len(xpath_parent) + 1 :].split("/") + def _get_steps_and_target_xpath(context_parent, xpath_parent, include_parent=False): parts = [] steps = 1 - remainder_xpath = xpath[len(xpath_parent) :] - xpath_parts = xpath[len(xpath_parent) + 1 :].split("/") + if not include_parent: + remainder_xpath = xpath[len(xpath_parent) :] + context_parts = context_xpath[len(xpath_parent) + 1 :].split("/") + xpath_parts = xpath[len(xpath_parent) + 1 :].split("/") + else: + split_idx = len(xpath_parent.split("/")) + context_parts = context_xpath.split("/")[split_idx - 1 :] + xpath_parts = xpath.split("/")[split_idx - 1 :] + remainder_xpath = "/".join(xpath_parts) + for index, item in enumerate(context_parts[:-1]): try: if xpath[len(context_parent) + 1 :].split("/")[index] != item: @@ -103,7 +111,16 @@ def _get_steps_and_target_xpath(context_parent, xpath_parent): context_parent = is_parent_a_repeat(survey, context_xpath) xpath_parent = is_parent_a_repeat(survey, xpath) if context_parent and xpath_parent and xpath_parent in context_parent: - return _get_steps_and_target_xpath(context_parent, xpath_parent) + include_parent = False + if not context_parent == xpath_parent and reference_parent: + context_shared_ancestor = is_parent_a_repeat(survey, context_parent) + if context_shared_ancestor == xpath_parent: + # Check if context_parent is a child repeat of the xpath_parent + # If the context_parent is a child of the xpath_parent reference the entire + # xpath_parent in the generated nodeset + include_parent = True + context_parent = context_shared_ancestor + return _get_steps_and_target_xpath(context_parent, xpath_parent, include_parent) elif context_parent and xpath_parent: # Check if context_parent and xpath_parent share a common # repeat ancestor @@ -853,7 +870,9 @@ def _setup_xpath_dictionary(self): else: self._xpath[element.name] = element.get_xpath() - def _var_repl_function(self, matchobj, context, use_current=False): + def _var_repl_function( + self, matchobj, context, use_current=False, reference_parent=False + ): """ Given a dictionary of xpaths, return a function we can use to replace ${varname} with the xpath to varname. @@ -886,7 +905,9 @@ def _var_repl_function(self, matchobj, context, use_current=False): ): # if context xpath and target xpath fall under the same # repeat use relative xpath referencing. - steps, ref_path = share_same_repeat_parent(self, xpath, context_xpath) + steps, ref_path = share_same_repeat_parent( + self, xpath, context_xpath, reference_parent + ) if steps: ref_path = ref_path if ref_path.endswith(name) else "/%s" % name prefix = " current()/" if use_current else " " @@ -898,13 +919,15 @@ def _var_repl_function(self, matchobj, context, use_current=False): ) return " " + last_saved_prefix + self._xpath[name] + " " - def insert_xpaths(self, text, context, use_current=False): + def insert_xpaths(self, text, context, use_current=False, reference_parent=False): """ Replace all instances of ${var} with the xpath to var. """ def _var_repl_function(matchobj): - return self._var_repl_function(matchobj, context, use_current) + return self._var_repl_function( + matchobj, context, use_current, reference_parent + ) return re.sub(BRACKETED_TAG_REGEX, _var_repl_function, unicode(text)) From 32b17c7d2d9ec692e2c28ab0e4eda94167a93677 Mon Sep 17 00:00:00 2001 From: Davis Raymond Muro Date: Thu, 29 Oct 2020 12:24:15 +0300 Subject: [PATCH 16/16] Remove Validate run --- pyxform/tests_v1/test_repeat.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py index 38d3acbb9..92f808921 100644 --- a/pyxform/tests_v1/test_repeat.py +++ b/pyxform/tests_v1/test_repeat.py @@ -233,13 +233,13 @@ def test_output_with_guidance_hint_translation_relative_path(self): def test_output_with_multiple_translations_relative_path(self): md = """ - | survey | | | | | | + | survey | | | | | | | | type | name | label::English | label::Indonesia | calculation | | | begin repeat | member | | | | - | | calculate | pos | | | position(..) | + | | calculate | pos | | | position(..) | | | text | member_name | Name of ${pos} | Nama ${pos} | | | | text | member_address | | Alamat | | - | | end repeat | | | | | + | | end repeat | | | | | """ self.assertPyxformXform( @@ -457,5 +457,4 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat_uses_current(self) xml__contains=[ '', ], - run_odk_validate=True, )