Skip to content

Commit

Permalink
Merge pull request #672 from lindsay-stevens/pyxform-602
Browse files Browse the repository at this point in the history
602: preserve order of columns when building secondary instance
  • Loading branch information
lognaturel authored Dec 1, 2023
2 parents 7aa346e + 723f73a commit efd4768
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 45 deletions.
2 changes: 1 addition & 1 deletion pyxform/survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def _generate_static_instances(list_name, choice_list) -> InstanceInfo:
itext_id = "-".join([list_name, str(idx)])
choice_element_list.append(node("itextId", itext_id))

for name, value in sorted(choice.items()):
for name, value in choice.items():
if isinstance(value, str) and name != "label":
choice_element_list.append(node(name, str(value)))
if (
Expand Down
6 changes: 2 additions & 4 deletions pyxform/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,8 @@ def node(*args, **kwargs) -> DetachableElement:
assert len(unicode_args) <= 1
parsed_string = False

# Convert the kwargs xml attribute dictionary to a xml.dom.minidom.Element. Sort the
# attributes to guarantee a consistent order across Python versions.
# See pyxform_test_case.reorder_attributes for details.
for k, v in iter(sorted(kwargs.items())):
# Convert the kwargs xml attribute dictionary to a xml.dom.minidom.Element.
for k, v in iter(kwargs.items()):
if k in blocked_attributes:
continue
if k == "toParseString":
Expand Down
11 changes: 7 additions & 4 deletions pyxform/xls2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,22 @@ def merge_dicts(dict_a, dict_b, default_key="default"):
if dict_b is None or dict_b == {}:
return dict_a

if type(dict_a) is not dict:
if not isinstance(dict_a, dict):
if default_key in dict_b:
return dict_b
dict_a = {default_key: dict_a}
if type(dict_b) is not dict:
if not isinstance(dict_b, dict):
if default_key in dict_a:
return dict_a
dict_b = {default_key: dict_b}

all_keys = set(dict_a.keys()).union(set(dict_b.keys()))
# Union keys but retain order (as opposed to set()), preferencing dict_a then dict_b.
# E.g. {"a": 1, "b": 2} + {"c": 3, "a": 4} -> {"a": None, "b": None, "c": None}
all_keys = {k: None for k in dict_a.keys()}
all_keys.update({k: None for k in dict_b.keys()})

out_dict = dict()
for key in all_keys:
for key in all_keys.keys():
out_dict[key] = merge_dicts(dict_a.get(key), dict_b.get(key), default_key)
return out_dict

Expand Down
2 changes: 0 additions & 2 deletions tests/fixtures/strings.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ test_simple_integer_question_type_multilingual_control = <input ref="/test/integ
test_simple_integer_question_type_multilingual_binding = <bind nodeset="/test/integer_q" type="int"/>
test_simple_date_question_type_multilingual_control = <input ref="/test/date_q"><label ref="jr:itext('/test/date_q:label')"/></input>
test_simple_date_question_type_multilingual_binding = <bind nodeset="/test/date_q" type="date"/>
test_simple_phone_number_question_type_multilingual_control = <input ref="/test/phone_number_q"><label ref="jr:itext('/test/phone_number_q:label')"/><hint>Enter numbers only.</hint></input>
test_simple_phone_number_question_type_multilingual_binding = <bind constraint="regex(., '^\d*$')" nodeset="/test/phone_number_q" type="string"/>
test_simple_select_all_question_multilingual_control = <select ref="/test/select_all_q"><label ref="jr:itext('/test/select_all_q:label')"/><item><label ref="jr:itext('/test/select_all_q/f:label')"/><value>f</value></item><item><label ref="jr:itext('/test/select_all_q/g:label')"/><value>g</value></item><item><label ref="jr:itext('/test/select_all_q/h:label')"/><value>h</value></item></select>
test_simple_select_all_question_multilingual_binding = <bind nodeset="/test/select_all_q" type="string"/>
test_simple_decimal_question_multilingual_control = <input ref="/test/decimal_q"><label ref="jr:itext('/test/decimal_q:label')"/></input>
Expand Down
33 changes: 22 additions & 11 deletions tests/j2x_question_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,31 @@ def test_simple_phone_number_question_type_multilingual(self):
"name": "phone_number_q",
}

expected_phone_number_control_xml = self.config.get(
self.cls_name, "test_simple_phone_number_question_type_multilingual_control"
)

expected_phone_number_binding_xml = self.config.get(
self.cls_name, "test_simple_phone_number_question_type_multilingual_binding"
)

q = create_survey_element_from_dict(simple_phone_number_question)
self.s.add_child(q)
self.assertEqual(ctw(q.xml_control()), expected_phone_number_control_xml)

if TESTING_BINDINGS:
self.assertEqual(ctw(q.xml_bindings()), expected_phone_number_binding_xml)
# Inspect XML Control
observed = q.xml_control()
self.assertEqual("input", observed.nodeName)
self.assertEqual("/test/phone_number_q", observed.attributes["ref"].nodeValue)
observed_label = observed.childNodes[0]
self.assertEqual("label", observed_label.nodeName)
self.assertEqual(
"jr:itext('/test/phone_number_q:label')",
observed_label.attributes["ref"].nodeValue,
)
observed_hint = observed.childNodes[1]
self.assertEqual("hint", observed_hint.nodeName)
self.assertEqual("Enter numbers only.", observed_hint.childNodes[0].nodeValue)

# Inspect XML Binding
expected = {
"nodeset": "/test/phone_number_q",
"type": "string",
"constraint": r"regex(., '^\d*$')",
}
observed = {k: v for k, v in q.xml_bindings()[0].attributes.items()}
self.assertDictEqual(expected, observed)

def test_simple_select_all_question_multilingual(self):
"""
Expand Down
29 changes: 29 additions & 0 deletions tests/test_choices_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,32 @@ def test_choices_without_labels__for_dynamic_selects__allowed_by_pyxform(self):
""",
],
)

def test_choices_extra_columns_output_order_matches_xlsform(self):
"""Should find that element order matches column order."""
md = """
| survey | | | |
| | type | name | label |
| | select_one choices | a | A |
| choices | | | |
| | list_name | name | label | geometry |
| | choices | 1 | | 46.5841618 7.0801379 0 0 |
| | choices | 2 | | 35.8805082 76.515057 0 0 |
"""
self.assertPyxformXform(
md=md,
xml__xpath_contains=[
"""
/h:html/h:head/x:model/x:instance[@id='choices']/x:root/x:item[
./x:name[position() = 1 and text() = '1']
and ./x:geometry[position() = 2]
]
"""
"""
/h:html/h:head/x:model/x:instance[@id='choices']/x:root/x:item[
./x:name[position() = 1 and text() = '2']
and ./x:geometry[position() = 2]
]
"""
],
)
18 changes: 4 additions & 14 deletions tests/test_external_instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from pyxform.errors import PyXFormError
from tests.pyxform_test_case import PyxformTestCase, PyxformTestError
from tests.xpath_helpers.choices import xpc


class ExternalInstanceTests(PyxformTestCase):
Expand Down Expand Up @@ -229,21 +230,10 @@ def test_can__use_all_types_together_with_unique_ids(self):
'<instance id="city1" src="jr://file/city1.xml"/>',
'<instance id="cities" src="jr://file-csv/cities.csv"/>',
'<instance id="fruits" src="jr://file-csv/fruits.csv"/>',
"""
<instance id="states">
<root>
<item>
<label>Pass</label>
<name>1</name>
</item>
<item>
<label>Fail</label>
<name>2</name>
</item>
</root>
</instance>
""",
], # noqa
xml__xpath_match=[
xpc.model_instance_choices_label("states", (("1", "Pass"), ("2", "Fail")))
],
)

def test_cannot__use_different_src_same_id__select_then_internal(self):
Expand Down
19 changes: 13 additions & 6 deletions tests/test_whitespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,16 @@ def test_values_without_whitespaces_are_processed_successfully(self):
| | id_string | public_key | submission_url |
| | tutorial_encrypted | MIIB | https://odk.ona.io/random_person/submission |
"""

survey = self.md_to_pyxform_survey(md_raw=md)
expected = """<submission action="https://odk.ona.io/random_person/submission" base64RsaPublicKey="MIIB" method="post"/>"""
xml = survey._to_pretty_xml()
self.assertEqual(1, xml.count(expected))
self.assertPyxformXform(md=md, xml__contains=expected, run_odk_validate=True)
self.assertPyxformXform(
md=md,
run_odk_validate=True,
xml__xpath_contains=[
"""
/h:html/h:head/x:model/x:submission[
@action='https://odk.ona.io/random_person/submission'
and @method='post'
and @base64RsaPublicKey='MIIB'
]
"""
],
)
12 changes: 9 additions & 3 deletions tests/validators/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import posixpath
import threading
import time
from http.server import SimpleHTTPRequestHandler
from socketserver import ThreadingTCPServer
from urllib.parse import unquote
Expand Down Expand Up @@ -70,13 +71,18 @@ def __init__(self, port=8000):
self._server_address, self._handler, bind_and_activate=False
)

def _bind_and_activate(self):
def _bind_and_activate(self, tries: int = 0):
tries += 1
try:
self.httpd.server_bind()
self.httpd.server_activate()
except Exception as e:
except OSError as e:
self.httpd.server_close()
raise e
if 5 < tries:
raise e
else:
time.sleep(0.5)
self._bind_and_activate(tries=tries)

def start(self):
self._bind_and_activate()
Expand Down

0 comments on commit efd4768

Please sign in to comment.