diff --git a/pyxform/question.py b/pyxform/question.py index 593b9103d..37b61c24f 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -23,7 +23,7 @@ def xml_instance(self): attributes = {} attributes.update(self.get(u'instance', {})) for key, value in attributes.items(): - attributes[key] = survey.insert_xpaths(value) + attributes[key] = survey.insert_xpaths(value, self) if self.get(u"default"): return node( self.name, unicode(self.get(u"default")), **attributes @@ -45,7 +45,7 @@ def xml_control(self): survey = self.get_root() # Resolve field references in attributes for key, value in control_dict.items(): - control_dict[key] = survey.insert_xpaths(value) + control_dict[key] = survey.insert_xpaths(value, self) control_dict['ref'] = self.get_xpath() result = node(**control_dict) @@ -57,7 +57,7 @@ def xml_control(self): if self['query']: choice_filter = self.get('choice_filter') query = "instance('" + self['query'] + "')/root/item" - choice_filter = survey.insert_xpaths(choice_filter) + choice_filter = survey.insert_xpaths(choice_filter, self) if choice_filter: query += '[' + choice_filter + ']' result.setAttribute('query', query) @@ -71,7 +71,7 @@ def xml_control(self): survey = self.get_root() # Resolve field references in attributes for key, value in control_dict.items(): - control_dict[key] = survey.insert_xpaths(value) + control_dict[key] = survey.insert_xpaths(value, self) control_dict['ref'] = self.get_xpath() return node( u"trigger", @@ -144,7 +144,7 @@ def xml_control(self): control_dict = self.control.copy() # Resolve field references in attributes for key, value in control_dict.items(): - control_dict[key] = survey.insert_xpaths(value) + control_dict[key] = survey.insert_xpaths(value, self) control_dict['ref'] = self.get_xpath() result = node(**control_dict) @@ -162,7 +162,7 @@ def xml_control(self): itemset = self['itemset'] itemset_label_ref = "jr:itext(itextId)" nodeset = "instance('" + itemset + "')/root/item" - choice_filter = survey.insert_xpaths(choice_filter) + choice_filter = survey.insert_xpaths(choice_filter, self) if choice_filter: nodeset += '[' + choice_filter + ']' itemset_children = [node('value', ref='name'), @@ -254,7 +254,7 @@ def xml_control(self): survey = self.get_root() # Resolve field references in attributes for key, value in control_dict.items(): - control_dict[key] = survey.insert_xpaths(value) + control_dict[key] = survey.insert_xpaths(value, self) control_dict['ref'] = self.get_xpath() params = self.get('parameters', {}) control_dict.update(params) diff --git a/pyxform/section.py b/pyxform/section.py index 9d26822f2..8e58b1bee 100644 --- a/pyxform/section.py +++ b/pyxform/section.py @@ -34,7 +34,7 @@ def xml_instance(self, **kwargs): survey = self.get_root() # Resolve field references in attributes for key, value in attributes.items(): - attributes[key] = survey.insert_xpaths(value) + attributes[key] = survey.insert_xpaths(value, self) result = node(self.name, **attributes) for child in self.children: if child.get(u"flat"): @@ -87,7 +87,7 @@ def xml_control(self): survey = self.get_root() # Resolve field references in attributes for key, value in control_dict.items(): - control_dict[key] = survey.insert_xpaths(value) + control_dict[key] = survey.insert_xpaths(value, self) repeat_node = node(u"repeat", nodeset=self.get_xpath(), **control_dict) for n in Section.xml_control(self): @@ -138,7 +138,7 @@ def xml_control(self): # Resolve field references in attributes for key, value in attributes.items(): - attributes[key] = survey.insert_xpaths(value) + attributes[key] = survey.insert_xpaths(value, self) if not self.get('flat'): attributes['ref'] = self.get_xpath() @@ -153,7 +153,8 @@ def xml_control(self): if u"intent" in control_dict: survey = self.get_root() - attributes['intent'] = survey.insert_xpaths(control_dict['intent']) + attributes['intent'] = survey.insert_xpaths(control_dict['intent'], + self) return node(u"group", *children, **attributes) diff --git a/pyxform/survey.py b/pyxform/survey.py index 40583ae33..b6db42d5f 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -29,7 +29,64 @@ def register_nsmap(): register_nsmap() +def is_parent_a_repeat(survey, xpath): + """ + Returns the XPATH of the first repeat of the given xpath in the survey, + otherwise False will be returned. + """ + parent_xpath = '/'.join(xpath.split('/')[:-1]) + if not parent_xpath: + return False + + repeats = [ + item for item in survey.iter_descendants() + if item.get_xpath() == parent_xpath and item.type == 'repeat'] + + return parent_xpath \ + if any(repeats) else is_parent_a_repeat(survey, parent_xpath) + + +def share_same_repeat_parent(survey, xpath, context_xpath): + """ + 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 + parent. + + For example, + xpath = /data/repeat_a/group_a/name + context_xpath = /data/repeat_a/group_b/age + + 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: + steps = 1 + remainder_xpath = xpath[len(xpath_parent):] + context_parts = context_xpath[len(xpath_parent) + 1:].split('/') + xpath_parts = xpath[len(xpath_parent) + 1:].split('/') + for index, item in enumerate(context_parts[:-1]): + try: + if xpath[len(context_parent) + 1:].split('/')[index] != item: + steps = len(context_parts[index:]) + remainder_xpath = "/" + "/".join(xpath_parts[index:]) + break + else: + remainder_xpath = "/" + "/".join( + remainder_xpath.split('/')[index + 2:]) + except IndexError: + steps = len(context_parts[index - 1:]) + remainder_xpath = "/".join(xpath_parts[index - 1:]) + break + return (steps, remainder_xpath) + + return (None, None) + + class Survey(Section): + """ + Survey class - represents the full XForm XML. + """ FIELDS = Section.FIELDS.copy() FIELDS.update( @@ -612,7 +669,7 @@ def _setup_xpath_dictionary(self): else: self._xpath[element.name] = element.get_xpath() - def _var_repl_function(self, matchobj): + def _var_repl_function(self, matchobj, context): """ Given a dictionary of xpaths, return a function we can use to replace ${varname} with the xpath to varname. @@ -627,32 +684,46 @@ def _var_repl_function(self, matchobj): raise PyXFormError(intro + " There are multiple survey elements" " with this name.") + # if context xpath and target xpath fall under the same repeat use + # relative xpath referencing. + steps, ref_path = share_same_repeat_parent(self, self._xpath[name], + context.get_xpath()) + if steps: + ref_path = ref_path if ref_path.endswith(name) else "/%s" % name + return " current()/" + "/".join([".."] * steps) + ref_path + " " + return " " + self._xpath[name] + " " - def insert_xpaths(self, text): + def insert_xpaths(self, text, context): """ Replace all instances of ${var} with the xpath to var. """ bracketed_tag = r"\$\{(.*?)\}" + def _var_repl_function(matchobj): + return self._var_repl_function(matchobj, context) - return re.sub(bracketed_tag, self._var_repl_function, unicode(text)) + return re.sub(bracketed_tag, _var_repl_function, unicode(text)) - def _var_repl_output_function(self, matchobj): + def _var_repl_output_function(self, matchobj, context): """ A regex substitution function that will replace ${varname} with an output element that has the xpath to varname. """ + def _var_repl_function(matchobj): + return self._var_repl_function(matchobj, context) # if matchobj.group(1) not in self._xpath: # raise PyXFormError("There is no survey element with this name.", # matchobj.group(1)) - return '' + return '' - def insert_output_values(self, text): + def insert_output_values(self, text, context): """ Replace all the ${variables} in text with xpaths. Returns that and a boolean indicating if there were any ${variables} present. """ + def _var_repl_output_function(matchobj): + return self._var_repl_output_function(matchobj, context) # There was a bug where escaping is completely turned off in labels # where variable replacement is used. # For exampke, `${name} < 3` causes an error but `< 3` does not. @@ -668,7 +739,7 @@ def insert_output_values(self, text): # the net effect < gets translated again to &lt; if unicode(xml_text).find('{') != -1: result = re.sub( - bracketed_tag, self._var_repl_output_function, + bracketed_tag, _var_repl_output_function, unicode(xml_text)) return result, not result == xml_text return text, False diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index c1813acf8..0bef18e5f 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -284,7 +284,8 @@ def xml_label(self): return node(u"label", ref=ref) else: survey = self.get_root() - label, output_inserted = survey.insert_output_values(self.label) + label, output_inserted = survey.insert_output_values(self.label, + self) return node(u"label", label, toParseString=output_inserted) def xml_hint(self): @@ -293,7 +294,7 @@ def xml_hint(self): return node(u"hint", ref="jr:itext('%s')" % path) else: hint, output_inserted = self.get_root().insert_output_values( - self.hint) + self.hint, self) return node(u"hint", hint, toParseString=output_inserted) def xml_label_and_hint(self): @@ -338,7 +339,7 @@ def xml_binding(self): if k == u'jr:noAppErrorString' and type(v) is dict: v = "jr:itext('%s')" % self._translation_path( u'jr:noAppErrorString') - bind_dict[k] = survey.insert_xpaths(v) + bind_dict[k] = survey.insert_xpaths(v, context=self) return node(u"bind", nodeset=self.get_xpath(), **bind_dict) return None diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py new file mode 100644 index 000000000..74f6bb010 --- /dev/null +++ b/pyxform/tests_v1/test_repeat.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +""" +test_repeat.py +""" +from pyxform.tests_v1.pyxform_test_case import PyxformTestCase + + +class TestRepeat(PyxformTestCase): + """ + TestRepeat class. + """ + def test_repeat_relative_reference(self): + """ + Test relative reference in repeats. + """ + self.assertPyxformXform( + debug=True, + name="test_repeat", + title="Relative Paths in repeats", + md=""" + | survey | | | | | + | | type | name | relevant | label | + | | text | Z | | Fruit | + | | begin repeat | section | | Section | + | | text | AA | | Anything really | + | | text | A | | A oat | + | | text | B | ${A}='oat' | B w ${A} | + | | note | note1 | | Noted ${AA} w ${A} | + | | end repeat | | | | + | | | | | | + | | begin repeat | section2 | | Section 2 | + | | text | C | | C | + | | begin group | sectiona | | Section A | + | | text | D | | D oat | + | | text | E | ${D}='oat' | E w ${Z} | + | | note | note2 | | Noted ${C} w ${E} | + | | end group | | | | + | | note | note3 | | Noted ${C} w ${E} | + | | end repeat | | | | + | | | | | | + | | begin repeat | section3 | | Section 3 | + | | text | FF | | F any text | + | | text | F | | F oat | + | | begin group | sectionb | | Section B | + | | text | G | | G oat | + | | text | H | ${G}='oat' | H w ${Z} | + | | note | note4 | | Noted ${H} w ${Z} | + | | end group | | | | + | | begin repeat | sectionc | | Section B | + | | text | I | | I | + | | text | J | ${I}='oat' | J w ${Z} | + | | text | K | ${F}='oat' | K w ${Z} | + | | text | L | ${G}='oat' | K w ${Z} | + | | note | note5 | | Noted ${FF} w ${H} | + | | note | note6 | | JKL #${J}#${K}#${L} | + | | end repeat | | | | + | | note | note7 | | Noted ${FF} w ${H} | + | | begin group | sectiond | | Section D | + | | text | M | | M oat | + | | text | N | ${G}='oat' | N w ${Z} | + | | text | O | ${M}='oat' | O w ${Z} | + | | note | note8 | | NO #${N} #${O} | + | | end group | | | | + | | note | note9 | | ${FF} ${H} ${N} ${N} | + | | end repeat | | | | + | | | | | | + """, + instance__contains=[ + '
', + '', + '', + '
', + ], + model__contains=[ + """""", + """""", + """""", + """""", + """""", + """""" + ], + xml__contains=[ + '', + '', + '', + """""", + """""", + """""" + ], + run_odk_validate=True + )