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 <
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
+ )