diff --git a/pyxform/aliases.py b/pyxform/aliases.py index c428b44dc..c388ef19f 100644 --- a/pyxform/aliases.py +++ b/pyxform/aliases.py @@ -85,6 +85,7 @@ u"no_app_error_string" : u"bind::jr:noAppErrorString", u"requiredMsg" : u"bind::jr:requiredMsg", u"required_message" : u"bind::jr:requiredMsg", + u"body":u"control" } list_header = { u"caption": constants.LABEL, diff --git a/pyxform/question.py b/pyxform/question.py index 68eb09072..5fa941d87 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -17,11 +17,19 @@ def validate(self): ) def xml_instance(self): + survey = self.get_root() + attributes = {} + attributes.update(self.get(u'instance', {})) + for key, value in attributes.items(): + attributes[key] = survey.insert_xpaths(value) if self.get(u"default"): #survey = self.get_root() #return node(self.name, survey.insert_xpaths(unicode(self.get(u"default")))) - return node(self.name, unicode(self.get(u"default"))) - return node(self.name) + return node(self.name, + unicode(self.get(u"default")), + **attributes + ) + return node(self.name, **attributes) def xml_control(self): return None @@ -35,14 +43,12 @@ class InputQuestion(Question): def xml_control(self): control_dict = self.control label_and_hint = self.xml_label_and_hint() - control_dict['ref'] = self.get_xpath() - - # for SurveyCTO's support for external intents, resolve field references in appearance column: survey = self.get_root() - appearance=control_dict.get('appearance') - if appearance is not None: - control_dict['appearance'] = survey.insert_xpaths(appearance) - + # Resolve field references in attributes + for key, value in control_dict.items(): + control_dict[key] = survey.insert_xpaths(value) + control_dict['ref'] = self.get_xpath() + result = node(**control_dict) if label_and_hint: for element in self.xml_label_and_hint(): @@ -63,18 +69,16 @@ class TriggerQuestion(Question): def xml_control(self): control_dict = self.control - if u"appearance" in control_dict: - return node( - u"trigger", ref=self.get_xpath(), - appearance=control_dict[u"appearance"], - *self.xml_label_and_hint() - ) - else: - return node(u"trigger", - ref=self.get_xpath(), - *self.xml_label_and_hint() - ) - + 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['ref'] = self.get_xpath() + return node( + u"trigger", + *self.xml_label_and_hint(), + **control_dict + ) class UploadQuestion(Question): def _get_media_type(self): @@ -82,22 +86,13 @@ def _get_media_type(self): def xml_control(self): control_dict = self.control - if u"appearance" in control_dict: - return node( - u"upload", - ref=self.get_xpath(), - mediatype=self._get_media_type(), - appearance=control_dict[u"appearance"], - *self.xml_label_and_hint() - ) - else: - return node( - u"upload", - ref=self.get_xpath(), - mediatype=self._get_media_type(), - *self.xml_label_and_hint() - ) - + control_dict['ref'] = self.get_xpath() + control_dict['mediatype'] = self._get_media_type() + return node( + u"upload", + *self.xml_label_and_hint(), + **control_dict + ) class Option(SurveyElement): @@ -162,20 +157,16 @@ def validate(self): def xml_control(self): assert self.bind[u"type"] in [u"select", u"select1"] - survey = self.get_root() 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['ref'] = self.get_xpath() - # for SurveyCTO's dynamic-select support, resolve field references in appearance column: - appearance = control_dict.get('appearance') - if appearance is not None: - control_dict['appearance'] = survey.insert_xpaths(appearance) - result = node(**control_dict) for element in self.xml_label_and_hint(): result.appendChild(element) - survey = self.get_root() # itemset are only supposed to be strings, check to prevent the rare dicts that show up if self['itemset'] and isinstance( self['itemset'] , basestring): choice_filter = self.get('choice_filter') diff --git a/pyxform/section.py b/pyxform/section.py index fc8ece39d..7927c5eed 100644 --- a/pyxform/section.py +++ b/pyxform/section.py @@ -25,7 +25,14 @@ def xml_instance(self, **kwargs): """ Creates an xml representation of the section """ - result = node(self.name, **kwargs) + attributes = {} + attributes.update(kwargs) + attributes.update(self.get(u'instance', {})) + survey = self.get_root() + # Resolve field references in attributes + for key, value in attributes.items(): + attributes[key] = survey.insert_xpaths(value) + result = node(self.name, **attributes) for child in self.children: if child.get(u"flat"): for grandchild in child.xml_instance_array(): @@ -70,10 +77,10 @@ def xml_control(self): """ control_dict = self.control.copy() - jrcount = control_dict.get('jr:count') - if jrcount: - survey = self.get_root() - control_dict['jr:count'] = survey.insert_xpaths(jrcount) + survey = self.get_root() + # Resolve field references in attributes + for key, value in control_dict.items(): + control_dict[key] = survey.insert_xpaths(value) repeat_node = node(u"repeat", nodeset=self.get_xpath(), **control_dict) for n in Section.xml_control(self): @@ -85,7 +92,7 @@ def xml_control(self): u"group", self.xml_label(), repeat_node, ref=self.get_xpath() ) - return node(u"group", repeat_node, ref=self.get_xpath()) + return node(u"group", repeat_node, ref=self.get_xpath(), **self.control) #I'm anal about matching function signatures when overriding a function, but there's no reason for kwargs to be an argument def xml_instance(self, **kwargs): @@ -111,10 +118,17 @@ def xml_control(self): return None children = [] - attrs = {} + attributes = {} + attributes.update(self.control) + + survey = self.get_root() + + # Resolve field references in attributes + for key, value in attributes.items(): + attributes[key] = survey.insert_xpaths(value) if not self.get('flat'): - attrs['ref'] = self.get_xpath() + attributes['ref'] = self.get_xpath() if 'label' in self and len(self['label']) > 0: children.append(self.xml_label()) @@ -122,13 +136,13 @@ def xml_control(self): children.append(n) if u"appearance" in control_dict: - attrs['appearance'] = control_dict['appearance'] + attributes['appearance'] = control_dict['appearance'] if u"intent" in control_dict: survey = self.get_root() - attrs['intent'] = survey.insert_xpaths(control_dict['intent']) + attributes['intent'] = survey.insert_xpaths(control_dict['intent']) - return node(u"group", *children, **attrs) + return node(u"group", *children, **attributes) def to_json_dict(self): # This is quite hacky, might want to think about a smart way diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index a5425b908..c57fcea2d 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -34,6 +34,7 @@ class SurveyElement(dict): u"intent": unicode, u"jr:count" : unicode, u"bind": dict, + u"instance": dict, u"control": dict, u"media": dict, # this node will also have a parent and children, like a tree! diff --git a/pyxform/tests/attribute_columns_test.py b/pyxform/tests/attribute_columns_test.py new file mode 100644 index 000000000..aef9d3d04 --- /dev/null +++ b/pyxform/tests/attribute_columns_test.py @@ -0,0 +1,47 @@ +""" +Some tests for the new (v0.9) spec is properly implemented. +""" +import unittest2 as unittest +import codecs +import os +import sys +# Hack to make sure that pyxform is on the python import path +parentdir = os.path.dirname(os.path.dirname( + os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, parentdir) +import pyxform +from pyxform.errors import PyXFormError + +DIR = os.path.dirname(__file__) + + +class attribute_columns_test(unittest.TestCase): + + maxDiff = None + + def runTest(self): + filename = "attribute_columns_test.xlsx" + path_to_excel_file = os.path.join(DIR, "example_xls", filename) + # Get the xform output path: + root_filename, ext = os.path.splitext(filename) + output_path = os.path.join(DIR, "test_output", root_filename + ".xml") + expected_output_path = os.path.join(DIR, "test_expected_output", + root_filename + ".xml") + # Do the conversion: + warnings = [] + json_survey = pyxform.xls2json.parse_file_to_json( + path_to_excel_file, warnings=warnings) + survey = pyxform.create_survey_element_from_dict(json_survey) + survey.print_xform_to_file(output_path, warnings=warnings) + # print warnings + # Compare with the expected output: + with codecs.open(expected_output_path, 'rb', encoding="utf-8") \ + as expected_file: + with codecs.open(output_path, 'rb', encoding="utf-8") \ + as actual_file: + self.assertMultiLineEqual( + expected_file.read(), actual_file.read()) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyxform/tests/example_xls/attribute_columns_test.xlsx b/pyxform/tests/example_xls/attribute_columns_test.xlsx new file mode 100644 index 000000000..09b9d9f20 Binary files /dev/null and b/pyxform/tests/example_xls/attribute_columns_test.xlsx differ diff --git a/pyxform/tests/test_expected_output/attribute_columns_test.xml b/pyxform/tests/test_expected_output/attribute_columns_test.xml new file mode 100644 index 000000000..f477fd800 --- /dev/null +++ b/pyxform/tests/test_expected_output/attribute_columns_test.xml @@ -0,0 +1,959 @@ + + + + Spec test + + + + + + - + + + jr://images/a.jpg + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + Yes + + + - + + + - + + + - + + + Yes + + + - + + + jr://images/img_test.jpg + jr://audio/audio_test.wav + jr://video/test.mov + - + + + - + + + - + + + No + + + jr://images/a.jpg + + + - + + + - + + + - + + + The goal of this test is to try out all the different media types in many languages to see if there are any bugs inserting media. + + + - + + + - + + + - + + + jr://images/b.jpg + + + Yes + + + No + + + - + + + Yes + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + No + + + - + + + - + + + - + + + No + + + - + + + - + + + - + + + No + + + - + + + Yes + + + Yes + + + jr://images/b.jpg + + + Yes + + + No + + + - + + + a note + + + Yes + + + No + + + jr://images/img_test.jpg + + + No + + + - + + + - + + + - + + + - + + + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + jr://images/- + + + ni hao + + + + + + - + + + - + + + - + + + + + + - + + + jr://images/- + jr://audio/chinese_audio.wav + jr://video/- + 您好 + + + - + + + - + + + 没有 + + + jr://images/- + + + - + + + - + + + - + + + ni hao + + + - + + + - + + + - + + + jr://images/- + + + + + + 没有 + + + 没有 + + + + + + - + + + - + + + jr://images/- + + + - + + + - + + + 對不起 ,你可以不選擇“是”和“否”。 + + + - + + + - + + + 没有 + + + - + + + - + + + - + + + 没有 + + + - + + + - + + + - + + + 您好 + + + - + + + + + + + + + jr://images/- + + + + + + 没有 + + + - + + + - + + + + + + 没有 + + + - + + + 没有 + + + - + + + - + + + - + + + - + + + + + video_test + + + 1 + + + table list question + + + autocomplete_chars_test + + + constrained decimal + + + - + + + This launches a fictional application to get an integer result. + + + repeat_test + + + select multiple test + + + - + + + deviceid_test_output: + + numerical name test + + + compact-test + + + start test output: + + - + + + label-test + + + jr://images/img_test_2.jpg + jr://audio/- + jr://video/- + text_image_audio_video_test + + + deviceid_test_output: + + date_test + + + - + + + jr://images/- + + + autocomplete_test + + + phonenumber_test_output: + + - + + + - + + + image_test + + + datetime_test + + + jr://images/- + + + today_test_output: + + - + + + audio_test + + + jr://images/- + + + - + + + - + + + a integer + + + + + + jr://images/- + + + labeled select group test + + + geopoint_test + + + Sorry , you can't select yes and no. + + + Skip to end + + + - + + + Your name is + + time_test + + + - + + + simserial_test_output: + + note_test + + + Enter your name + + + Enter an address + + + end test output: + + acknowledge_test + + + - + + + - + + + - + + + jr://images/- + + + - + + + - + + + You entered an email address + + + - + + + - + + + - + + + required_text + + + list-nolabel-test + + + boolean name test + + + compact-2-test + + + barcode_test + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_1/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyxform/tests/test_output/attribute_columns_test.xml b/pyxform/tests/test_output/attribute_columns_test.xml new file mode 100644 index 000000000..f477fd800 --- /dev/null +++ b/pyxform/tests/test_output/attribute_columns_test.xml @@ -0,0 +1,959 @@ + + + + Spec test + + + + + + - + + + jr://images/a.jpg + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + Yes + + + - + + + - + + + - + + + Yes + + + - + + + jr://images/img_test.jpg + jr://audio/audio_test.wav + jr://video/test.mov + - + + + - + + + - + + + No + + + jr://images/a.jpg + + + - + + + - + + + - + + + The goal of this test is to try out all the different media types in many languages to see if there are any bugs inserting media. + + + - + + + - + + + - + + + jr://images/b.jpg + + + Yes + + + No + + + - + + + Yes + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + No + + + - + + + - + + + - + + + No + + + - + + + - + + + - + + + No + + + - + + + Yes + + + Yes + + + jr://images/b.jpg + + + Yes + + + No + + + - + + + a note + + + Yes + + + No + + + jr://images/img_test.jpg + + + No + + + - + + + - + + + - + + + - + + + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + - + + + jr://images/- + + + ni hao + + + + + + - + + + - + + + - + + + + + + - + + + jr://images/- + jr://audio/chinese_audio.wav + jr://video/- + 您好 + + + - + + + - + + + 没有 + + + jr://images/- + + + - + + + - + + + - + + + ni hao + + + - + + + - + + + - + + + jr://images/- + + + + + + 没有 + + + 没有 + + + + + + - + + + - + + + jr://images/- + + + - + + + - + + + 對不起 ,你可以不選擇“是”和“否”。 + + + - + + + - + + + 没有 + + + - + + + - + + + - + + + 没有 + + + - + + + - + + + - + + + 您好 + + + - + + + + + + + + + jr://images/- + + + + + + 没有 + + + - + + + - + + + + + + 没有 + + + - + + + 没有 + + + - + + + - + + + - + + + - + + + + + video_test + + + 1 + + + table list question + + + autocomplete_chars_test + + + constrained decimal + + + - + + + This launches a fictional application to get an integer result. + + + repeat_test + + + select multiple test + + + - + + + deviceid_test_output: + + numerical name test + + + compact-test + + + start test output: + + - + + + label-test + + + jr://images/img_test_2.jpg + jr://audio/- + jr://video/- + text_image_audio_video_test + + + deviceid_test_output: + + date_test + + + - + + + jr://images/- + + + autocomplete_test + + + phonenumber_test_output: + + - + + + - + + + image_test + + + datetime_test + + + jr://images/- + + + today_test_output: + + - + + + audio_test + + + jr://images/- + + + - + + + - + + + a integer + + + + + + jr://images/- + + + labeled select group test + + + geopoint_test + + + Sorry , you can't select yes and no. + + + Skip to end + + + - + + + Your name is + + time_test + + + - + + + simserial_test_output: + + note_test + + + Enter your name + + + Enter an address + + + end test output: + + acknowledge_test + + + - + + + - + + + - + + + jr://images/- + + + - + + + - + + + You entered an email address + + + - + + + - + + + - + + + required_text + + + list-nolabel-test + + + boolean name test + + + compact-2-test + + + barcode_test + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_1/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyxform/utils.py b/pyxform/utils.py index 327aa875d..e1fd0e8be 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -23,19 +23,24 @@ def is_valid_xml_tag(tag): """ return re.search(r"^" + XFORM_TAG_REGEXP + r"$", tag) -def node(tag, *args, **kwargs): +def node(*args, **kwargs): """ - tag -- a XML tag - args -- an array of children to append to the newly created node + args[0] -- a XML tag + args[1:] -- an array of children to append to the newly created node or if a unicode arg is supplied it will be used to make a text node + kwargs -- attributes returns a xml.dom.minidom.Element """ + blocked_attributes = ['tag'] + tag = args[0] if len(args) > 0 else kwargs['tag'] + args = args[1:] result = Element(tag) unicode_args = [u for u in args if type(u) == unicode] assert len(unicode_args) <= 1 parsedString = False #kwargs is an xml attribute dictionary, here we convert it to a xml.dom.minidom.Element for k, v in kwargs.iteritems(): + if k in blocked_attributes: continue if k == 'toParseString': if v == True and len(unicode_args) == 1: parsedString = True