Skip to content

Commit

Permalink
Use relative references for repeats only.
Browse files Browse the repository at this point in the history
Fix #4
  • Loading branch information
ukanga committed Apr 5, 2018
1 parent 4d11bcc commit 31d3fc4
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 21 deletions.
14 changes: 7 additions & 7 deletions pyxform/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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'),
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions pyxform/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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)

Expand Down
85 changes: 78 additions & 7 deletions pyxform/survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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 '<output value="' + self._var_repl_function(matchobj) + '" />'
return '<output value="' + _var_repl_function(matchobj) + '" />'

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.
Expand All @@ -668,7 +739,7 @@ def insert_output_values(self, text):
# the net effect &lt gets translated again to &amp;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
Expand Down
7 changes: 4 additions & 3 deletions pyxform/survey_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down
101 changes: 101 additions & 0 deletions pyxform/tests_v1/test_repeat.py
Original file line number Diff line number Diff line change
@@ -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=[
'<section jr:template="">',
'<A/>',
'<B/>',
'</section>',
],
model__contains=[
"""<bind nodeset="/test_repeat/section/A" """
"""type="string"/>""",
"""<bind nodeset="/test_repeat/section/B" """
"""relevant=" current()/../A ='oat'" """
"""type="string"/>""",
"""<bind nodeset="/test_repeat/section2/sectiona/E" """
"""relevant=" current()/../D ='oat'" type="string"/>""",
"""<bind nodeset="/test_repeat/section3/sectionc/K" """
"""relevant=" current()/../../F ='oat'" type="string"/>""",
"""<bind nodeset="/test_repeat/section3/sectionc/L" """
"""relevant=" current()/../../sectionb/G ='oat'" """
"""type="string"/>""",
"""<bind nodeset="/test_repeat/section3/sectiond/N" """
"""relevant=" current()/../../sectionb/G ='oat'" """
"""type="string"/>"""
],
xml__contains=[
'<group ref="/test_repeat/section">',
'<label>Section</label>',
'</group>',
"""<label> B w <output value=" current()/../A "/> </label>""",
"""<label> E w <output value=" /test_repeat/Z "/> </label>""",
"""<label> Noted <output value=" current()/../FF "/> w """
"""<output value=" current()/../sectionb/H "/> </label></input>"""
],
run_odk_validate=True
)

0 comments on commit 31d3fc4

Please sign in to comment.