diff --git a/pyxform/__init__.py b/pyxform/__init__.py old mode 100755 new mode 100644 diff --git a/pyxform/question.py b/pyxform/question.py index aa10464f5..4f8efd67b 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -7,28 +7,23 @@ class Question(SurveyElement): - def validate(self): SurveyElement.validate(self) # make sure that the type of this question exists in the # question type dictionary. if self.type not in QUESTION_TYPE_DICT: - raise PyXFormError( - "Unknown question type '%s'." % self.type - ) + raise PyXFormError("Unknown question type '%s'." % self.type) - def xml_instance(self): + def xml_instance(self, **kwargs): survey = self.get_root() attributes = {} - attributes.update(self.get(u'instance', {})) + 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 - ) + return node(self.name, unicode(self.get(u"default")), **attributes) return node(self.name, **attributes) def xml_control(self): @@ -40,14 +35,15 @@ class InputQuestion(Question): This control string is the same for: strings, integers, decimals, dates, geopoints, barcodes ... """ + def xml_control(self): control_dict = self.control label_and_hint = 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() + control_dict[key] = survey.insert_xpaths(value, self) + control_dict["ref"] = self.get_xpath() result = node(**control_dict) if label_and_hint: @@ -55,30 +51,25 @@ def xml_control(self): result.appendChild(element) # Input types are used for selects with external choices sheets. - if self['query']: - choice_filter = self.get('choice_filter') - query = "instance('" + self['query'] + "')/root/item" - choice_filter = survey.insert_xpaths(choice_filter) + if self["query"]: + choice_filter = self.get("choice_filter") + query = "instance('" + self["query"] + "')/root/item" + choice_filter = survey.insert_xpaths(choice_filter, self, True) if choice_filter: - query += '[' + choice_filter + ']' - result.setAttribute('query', query) + query += "[" + choice_filter + "]" + result.setAttribute("query", query) return result class TriggerQuestion(Question): - def xml_control(self): control_dict = self.control 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 - ) + control_dict[key] = survey.insert_xpaths(value, self) + control_dict["ref"] = self.get_xpath() + return node(u"trigger", *self.xml_label_and_hint(), **control_dict) class UploadQuestion(Question): @@ -87,17 +78,12 @@ def _get_media_type(self): def xml_control(self): control_dict = self.control - control_dict['ref'] = self.get_xpath() - control_dict['mediatype'] = self._get_media_type() - return node( - u"upload", - *self.xml_label_and_hint(), - **control_dict - ) + 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): - def xml_value(self): return node(u"value", self.name) @@ -114,15 +100,15 @@ def validate(self): class MultipleChoiceQuestion(Question): - def __init__(self, **kwargs): kwargs_copy = kwargs.copy() # Notice that choices can be specified under choices or children. # I'm going to try to stick to just choices. # Aliases in the json format will make it more difficult # to use going forward. - choices = list(kwargs_copy.pop(u"choices", [])) + \ - list(kwargs_copy.pop(u"children", [])) + choices = list(kwargs_copy.pop(u"choices", [])) + list( + kwargs_copy.pop(u"children", []) + ) Question.__init__(self, **kwargs_copy) for choice in choices: self.add_choice(**choice) @@ -145,47 +131,55 @@ 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['ref'] = self.get_xpath() + control_dict[key] = survey.insert_xpaths(value, self) + control_dict["ref"] = self.get_xpath() result = node(**control_dict) for element in self.xml_label_and_hint(): result.appendChild(element) # 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') - itemset, file_extension = os.path.splitext(self['itemset']) - if file_extension in ['.csv', '.xml']: + if self["itemset"] and isinstance(self["itemset"], basestring): + choice_filter = self.get("choice_filter") + itemset, file_extension = os.path.splitext(self["itemset"]) + if file_extension in [".csv", ".xml"]: itemset = itemset itemset_label_ref = "label" else: - itemset = self['itemset'] + 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, True) if choice_filter: - nodeset += '[' + choice_filter + ']' - - if self['parameters']: - params = self['parameters'] - - if 'randomize' in params and params['randomize'] == 'true': - nodeset = 'randomize(' + nodeset - - if 'seed' in params: - if params['seed'].startswith('${'): - nodeset = nodeset + ', ' + survey.insert_xpaths(params['seed']).strip() + nodeset += "[" + choice_filter + "]" + + if self["parameters"]: + params = self["parameters"] + + if "randomize" in params and params["randomize"] == "true": + nodeset = "randomize(" + nodeset + + if "seed" in params: + if params["seed"].startswith("${"): + nodeset = ( + nodeset + + ", " + + survey.insert_xpaths( + params["seed"], self + ).strip() + ) else: - nodeset = nodeset + ', ' + params['seed'] + nodeset = nodeset + ", " + params["seed"] - nodeset += ')' + nodeset += ")" - itemset_children = [node('value', ref='name'), - node('label', ref=itemset_label_ref)] - result.appendChild(node('itemset', *itemset_children, - nodeset=nodeset)) + itemset_children = [ + node("value", ref="name"), + node("label", ref=itemset_label_ref), + ] + result.appendChild( + node("itemset", *itemset_children, nodeset=nodeset) + ) else: for n in [o.xml() for o in self.children]: result.appendChild(n) @@ -201,8 +195,9 @@ def __init__(self, **kwargs): class Tag(SurveyElement): def __init__(self, **kwargs): kwargs_copy = kwargs.copy() - choices = kwargs_copy.pop(u"choices", []) + \ - kwargs_copy.pop(u"children", []) + choices = kwargs_copy.pop(u"choices", []) + kwargs_copy.pop( + u"children", [] + ) super(Tag, self).__init__(**kwargs_copy) @@ -229,8 +224,7 @@ def validate(self): class OsmUploadQuestion(UploadQuestion): def __init__(self, **kwargs): kwargs_copy = kwargs.copy() - tags = kwargs_copy.pop(u"tags", []) + \ - kwargs_copy.pop(u"children", []) + tags = kwargs_copy.pop(u"tags", []) + kwargs_copy.pop(u"children", []) super(OsmUploadQuestion, self).__init__(**kwargs_copy) @@ -246,13 +240,9 @@ def add_tag(self, **kwargs): def xml_control(self): control_dict = self.control - control_dict['ref'] = self.get_xpath() - control_dict['mediatype'] = self._get_media_type() - result = node( - u"upload", - *self.xml_label_and_hint(), - **control_dict - ) + control_dict["ref"] = self.get_xpath() + control_dict["mediatype"] = self._get_media_type() + result = node(u"upload", *self.xml_label_and_hint(), **control_dict) for osm_tag in self.children: result.appendChild(osm_tag.xml()) @@ -265,15 +255,16 @@ class RangeQuestion(Question): This control string is the same for: strings, integers, decimals, dates, geopoints, barcodes ... """ + def xml_control(self): control_dict = self.control label_and_hint = 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() - params = self.get('parameters', {}) + control_dict[key] = survey.insert_xpaths(value, self) + control_dict["ref"] = self.get_xpath() + params = self.get("parameters", {}) control_dict.update(params) result = node(**control_dict) if label_and_hint: 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 cc3d93a3f..931659eca 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Survey module with XForm Survey objects and utility functions. +""" import codecs import os import re @@ -7,20 +11,20 @@ from datetime import datetime from pyxform import constants -from pyxform.errors import PyXFormError -from pyxform.errors import ValidationError +from pyxform.errors import PyXFormError, ValidationError from pyxform.external_instance import ExternalInstance from pyxform.instance import SurveyInstance from pyxform.instance_info import InstanceInfo from pyxform.question import Question from pyxform.section import Section from pyxform.survey_element import SurveyElement -from pyxform.utils import PatchedText, basestring, node, unicode, NSMAP, get_languages_with_bad_tags -from pyxform.validators import odk_validate -from pyxform.validators import enketo_validate +from pyxform.utils import (NSMAP, PatchedText, basestring, + get_languages_with_bad_tags, node, unicode) +from pyxform.validators import enketo_validate, odk_validate def register_nsmap(): + """Function to register NSMAP namespaces with ETree""" for prefix, uri in NSMAP.items(): prefix_no_xmlns = prefix.replace("xmlns", "").replace(":", "") ETree.register_namespace(prefix_no_xmlns, uri) @@ -29,7 +33,65 @@ 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: + context_parts = context_xpath[len(xpath_parent) + 1:].split('/') + parts = [] + steps = 1 + remainder_xpath = xpath[len(xpath_parent):] + 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:]) + parts = xpath_parts[index:] + break + else: + parts = remainder_xpath.split('/')[index + 2:] + except IndexError: + steps = len(context_parts[index - 1:]) + parts = xpath_parts[index - 1:] + break + + return (steps, "/" + "/".join(parts) if parts else remainder_xpath) + + return (None, None) + + class Survey(Section): + """ + Survey class - represents the full XForm XML. + """ FIELDS = Section.FIELDS.copy() FIELDS.update( @@ -62,7 +124,7 @@ class Survey(Section): u"attribute": dict, u"namespaces": unicode, } - ) + ) # yapf: disable def validate(self): if self.id_string in [None, 'None']: @@ -72,12 +134,13 @@ def validate(self): def _validate_uniqueness_of_section_names(self): section_names = [] - for e in self.iter_descendants(): - if isinstance(e, Section): - if e.name in section_names: + for element in self.iter_descendants(): + if isinstance(element, Section): + if element.name in section_names: raise PyXFormError( - "There are two sections with the name %s." % e.name) - section_names.append(e.name) + "There are two sections with the name %s." % + element.name) + section_names.append(element.name) def get_nsmap(self): """Add additional namespaces""" @@ -95,8 +158,8 @@ def get_nsmap(self): for k, v in nslist if xmlns + k not in nsmap ])) return nsmap - else: - return NSMAP + + return NSMAP def xml(self): """ @@ -111,14 +174,11 @@ def xml(self): self, constants.STYLE) nsmap = self.get_nsmap() - return node(u"h:html", - node(u"h:head", - node(u"h:title", self.title), - self.xml_model() - ), - node(u"h:body", *self.xml_control(), **body_kwargs), - **nsmap - ) + return node( + u"h:html", + node(u"h:head", node(u"h:title", self.title), self.xml_model()), + node(u"h:body", *self.xml_control(), **body_kwargs), + **nsmap) @staticmethod def _generate_static_instances(list_name, choice_list): @@ -139,14 +199,12 @@ def _generate_static_instances(list_name, choice_list): itext_id = '-'.join(['static_instance', list_name, str(idx)]) choice_element_list.append(node("itextId", itext_id)) - for choicePropertyName, choicePropertyValue in choice.items(): - if isinstance(choicePropertyValue, basestring) \ - and choicePropertyName != 'label': - choice_element_list.append( - node(choicePropertyName, - unicode(choicePropertyValue)) - ) + for name, value in choice.items(): + if isinstance(value, basestring) and name != 'label': + choice_element_list.append(node(name, unicode(value))) + instance_element_list.append(node("item", *choice_element_list)) + return InstanceInfo( type=u"choice", context=u"survey", @@ -185,6 +243,8 @@ def _generate_external_instances(element): ) ) + return None + @staticmethod def _validate_external_instances(instances): """ @@ -202,14 +262,14 @@ def _validate_external_instances(instances): seen[element].append(i) errors = [] for element, copies in seen.items(): - if 1 < len(copies): + if len(copies) > 1: contexts = ", ".join(x.context for x in copies) errors.append( "Instance names must be unique within a form. " "The name '{i}' was found {c} time(s), " "under these contexts: {contexts}".format( i=element, c=len(copies), contexts=contexts)) - if 0 < len(errors): + if errors: raise ValidationError("\n".join(errors)) @staticmethod @@ -238,6 +298,8 @@ def _generate_pulldata_instances(element): ) ) + return None + @staticmethod def _generate_from_file_instances(element): itemset = element.get('itemset') @@ -261,6 +323,8 @@ def _generate_from_file_instances(element): ) ) + return None + def _generate_instances(self): """ Get instances from all the different ways that they may be generated. @@ -303,12 +367,13 @@ def _generate_instances(self): instances += [x for x in [i_ext, i_pull, i_file] if x is not None] # Append last so the choice instance is excluded on a name clash. - for k, v in self.choices.items(): + for name, value in self.choices.items(): instances += [ - self._generate_static_instances(list_name=k, choice_list=v)] + self._generate_static_instances(list_name=name, + choice_list=value)] # Check that external instances have unique names. - if 0 < len(instances): + if instances: ext_only = [x for x in instances if x.type == "external"] self._validate_external_instances(instances=ext_only) @@ -362,10 +427,10 @@ def xml_model(self): **submission_attrs) model_children.insert(0, submission_node) - return node("model", *model_children) + return node("model", *model_children) - def xml_instance(self): - result = Section.xml_instance(self) + def xml_instance(self, **kwargs): + result = Section.xml_instance(self, **kwargs) # set these first to prevent overwriting id and version for key, value in self.attribute.items(): @@ -401,7 +466,26 @@ def _setup_translations(self): set up the self._translations dict which will be referenced in the setup media and itext functions """ - self._translations = defaultdict(dict) + def _setup_choice_translations(name, choice_value, itext_id): + for media_type_or_language, value in choice_value.items(): # noqa + if isinstance(value, dict): + for language, val in value.items(): + self._add_to_nested_dict( + self._translations, + [language, itext_id, media_type_or_language], val) + else: + if name == 'media': + self._add_to_nested_dict( + self._translations, + [self.default_language, itext_id, + media_type_or_language], + value) + else: + self._add_to_nested_dict( + self._translations, + [media_type_or_language, itext_id, 'long'], value) + + self._translations = defaultdict(dict) # pylint: disable=attribute-defined-outside-init for element in self.iter_descendants(): for d in element.get_translations(self.default_language): if 'guidance_hint' in d['path']: @@ -415,35 +499,16 @@ def _setup_translations(self): # This code sets up translations for choices in filtered selects. for list_name, choice_list in self.choices.items(): for idx, choice in zip(range(len(choice_list)), choice_list): - for choicePropertyName, choicePropertyValue in choice.items(): - itext_id = '-'.join( - ['static_instance', list_name, str(idx)]) - if isinstance(choicePropertyValue, dict): - for mediatypeorlanguage, value in choicePropertyValue.items(): # noqa - if isinstance(value, dict): - for language, val in value.items(): - self._add_to_nested_dict( - self._translations, - [language, itext_id, - mediatypeorlanguage], - val) - else: - if choicePropertyName == 'media': - self._add_to_nested_dict( - self._translations, - [self.default_language, itext_id, - mediatypeorlanguage], - value) - else: - self._add_to_nested_dict( - self._translations, - [mediatypeorlanguage, itext_id, - 'long'], value) - elif choicePropertyName == 'label': + for name, choice_value in choice.items(): + itext_id = '-'.join(['static_instance', list_name, + str(idx)]) + if isinstance(choice_value, dict): + _setup_choice_translations(name, choice_value, itext_id) + elif name == 'label': self._add_to_nested_dict( self._translations, [self.default_language, itext_id, 'long'], - choicePropertyValue) + choice_value) def _add_empty_translations(self): """ @@ -473,7 +538,7 @@ def _setup_media(self): It matches the xform nesting order. """ if not self._translations: - self._translations = defaultdict(dict) + self._translations = defaultdict(dict) # pylint: disable=attribute-defined-outside-init for survey_element in self.iter_descendants(): @@ -496,7 +561,7 @@ def _setup_media(self): raise PyXFormError( "Media type: " + media_type + " not supported") - if type(possibly_localized_media) is dict: + if isinstance(possibly_localized_media, dict): # media is localized localized_media = possibly_localized_media else: @@ -523,7 +588,7 @@ def _setup_media(self): translations_language[translation_key] if media_type not in translations_trans_key: - translations_trans_key[media_type] = {} + translations_trans_key[media_type] = {} translations_trans_key[media_type] = media @@ -546,7 +611,7 @@ def itext(self): itext_nodes = [] label_type = label_name.partition(":")[-1] - if type(content) is not dict: + if not isinstance(content, dict): raise Exception() for media_type, media_value in content.items(): @@ -599,6 +664,7 @@ def itext(self): return node("itext", *result) def date_stamp(self): + """Returns a date string with the format of %Y_%m_%d.""" return self._created.strftime("%Y_%m_%d") def _to_ugly_xml(self): @@ -614,10 +680,11 @@ def _to_pretty_xml(self): # TODO: check out pyxml # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ xml_with_linebreaks = self.xml().toprettyxml(indent=' ') - text_re = re.compile('(>)\n\s*(\s[^<>\s].*?)\n\s*(\s)\n(\s\s)*') - pretty_xml = text_re.sub(lambda m: ''.join(m.group(1, 2, 3)), xml_with_linebreaks) - inline_output = output_re.sub('\g<1>', pretty_xml) + text_re = re.compile(r'(>)\n\s*(\s[^<>\s].*?)\n\s*(\s)\n(\s\s)*') + pretty_xml = text_re.sub(lambda m: ''.join(m.group(1, 2, 3)), + xml_with_linebreaks) + inline_output = output_re.sub(r'\g<1>', pretty_xml) return '\n' + inline_output def __repr__(self): @@ -627,15 +694,15 @@ def __unicode__(self): return "" % hex(id(self)) def _setup_xpath_dictionary(self): - self._xpath = {} + self._xpath = {} # pylint: disable=attribute-defined-outside-init for element in self.iter_descendants(): - if isinstance(element, Question) or isinstance(element, Section): + if isinstance(element, (Question, Section)): if element.name in self._xpath: self._xpath[element.name] = None else: self._xpath[element.name] = element.get_xpath() - def _var_repl_function(self, matchobj): + def _var_repl_function(self, matchobj, context, use_current=False): """ Given a dictionary of xpaths, return a function we can use to replace ${varname} with the xpath to varname. @@ -649,33 +716,44 @@ def _var_repl_function(self, matchobj): if self._xpath[name] is None: raise PyXFormError(intro + " There are multiple survey elements" " with this name.") + if context: + # 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 + prefix = " current()/" if use_current else " " + return prefix + "/".join([".."] * steps) + ref_path + " " return " " + self._xpath[name] + " " - def insert_xpaths(self, text): + def insert_xpaths(self, text, context, use_current=False): """ 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, use_current) - 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. """ -# 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=None): """ 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. @@ -691,11 +769,12 @@ 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 + # pylint: disable=too-many-arguments def print_xform_to_file(self, path=None, validate=True, pretty_print=True, warnings=None, enketo=False): """ @@ -707,29 +786,42 @@ def print_xform_to_file(self, path=None, validate=True, pretty_print=True, if not path: path = self._print_name + ".xml" try: - with codecs.open(path, mode="w", encoding="utf-8") as fp: + with codecs.open(path, mode="w", encoding="utf-8") as file_obj: if pretty_print: - fp.write(self._to_pretty_xml()) + file_obj.write(self._to_pretty_xml()) else: - fp.write(self._to_ugly_xml()) - except Exception as e: + file_obj.write(self._to_ugly_xml()) + except Exception as error: if os.path.exists(path): os.unlink(path) - raise e + raise error if validate: warnings.extend(odk_validate.check_xform(path)) if enketo: warnings.extend(enketo_validate.check_xform(path)) # Warn if one or more translation is missing a valid IANA subtag - if len(self._translations.keys()) > 0: - bad_languages = get_languages_with_bad_tags(self._translations.keys()) - if len(bad_languages) > 0: - warnings.append("\tThe following language declarations do not contain valid machine-readable codes: " + - ", ".join(bad_languages) + ". Learn more: http://xlsform.org#multiple-language-support") + translations = self._translations.keys() + if translations: + bad_languages = get_languages_with_bad_tags(translations) + if bad_languages: + warnings.append( + "\tThe following language declarations do not contain " + "valid machine-readable codes: " + + ", ".join(bad_languages) + ". " + + "Learn more: http://xlsform.org#multiple-language-support") def to_xml(self, validate=True, pretty_print=True, warnings=None, enketo=False): + """ + Generates the XForm XML. + validate is True by default - pass the XForm XML through ODK Validator. + pretty_print is True by default - formats the XML for readability. + warnings - if a list is passed it stores all warnings generated + enketo - pass the XForm XML though Enketo Validator. + + Return XForm XML string. + """ # On Windows, NamedTemporaryFile must be opened exclusively. # So it must be explicitly created, opened, closed, and removed. tmp = tempfile.NamedTemporaryFile(delete=False) @@ -744,8 +836,8 @@ def to_xml(self, validate=True, pretty_print=True, warnings=None, os.remove(tmp.name) if pretty_print: return self._to_pretty_xml() - else: - return self._to_ugly_xml() + + return self._to_ugly_xml() def instantiate(self): """ diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index af432523f..720fbed22 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -302,7 +302,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): @@ -311,7 +312,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): @@ -356,7 +357,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/test_output/yes_or_no_question.json b/pyxform/tests/test_output/yes_or_no_question.json index 2fdedd886..e95631377 100644 --- a/pyxform/tests/test_output/yes_or_no_question.json +++ b/pyxform/tests/test_output/yes_or_no_question.json @@ -22,7 +22,8 @@ } ], "type": "select one", - "name": "good_day", + "name": "good_day", + "parameters": {}, "label": { "english": "have you had a good day today?" } diff --git a/pyxform/tests_v1/test_for_loop.py b/pyxform/tests_v1/test_for_loop.py index 7a976ea84..e0fb831df 100644 --- a/pyxform/tests_v1/test_for_loop.py +++ b/pyxform/tests_v1/test_for_loop.py @@ -19,7 +19,7 @@ def test_loop(self): ], model__contains=[ """""" ], xml__contains=[ diff --git a/pyxform/tests_v1/test_repeat.py b/pyxform/tests_v1/test_repeat.py new file mode 100644 index 000000000..f07caf165 --- /dev/null +++ b/pyxform/tests_v1/test_repeat.py @@ -0,0 +1,157 @@ +# -*- 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( + 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 | | | | + | | | | | | + """, # noqa pylint: disable=line-too-long + instance__contains=[ + '
', + '', + '', + '
', + ], + model__contains=[ + """""", + """""", + """""", + """""", + """""", + """""" + ], + xml__contains=[ + '', + '', + '', + """""", + """""", + """""" + ], + ) + + def test_calculate_relative_path(self): + """Test relative paths in calculate column.""" + self.assertPyxformXform( + name="data", + title="Paths in a calculate within a repeat are relative.", + md=""" + | survey | | | | | + | | type | name | label | calculate | + | | begin repeat | rep | | | + | | select_one crop_list | crop | Select | | + | | text | a | Verify | name = ${crop} | + | | begin group | group | | | + | | text | b | Verify | name = ${crop} | + | | end group | | | | + | | end repeat | | | | + | | | | | | + | choices | | | | | + | | list name | name | label | | + | | crop_list | maize | Maize | | + | | crop_list | beans | Beans | | + | | crop_list | kale | Kale | | + """, # noqa pylint: disable=line-too-long + model__contains=[ + """""", + """""", + ], + ) + + def test_choice_filter_relative_path(self): # pylint: disable=invalid-name + """Test relative paths in choice_filter column.""" + self.assertPyxformXform( + name="data", + title="Choice filter uses relative path", + md=""" + | survey | | | | | + | | type | name | label | choice_filter | + | | begin repeat | rep | | | + | | select_one crop_list | crop | Select | | + | | select_one crop_list | a | Verify | name = ${crop} | + | | begin group | group | | | + | | select_one crop_list | b | Verify | name = ${crop} | + | | end group | | | | + | | end repeat | | | | + | | | | | | + | choices | | | | | + | | list name | name | label | | + | | crop_list | maize | Maize | | + | | crop_list | beans | Beans | | + | | crop_list | kale | Kale | | + """, # noqa pylint: disable=line-too-long + xml__contains=[ + """""", # noqa pylint: disable=line-too-long + """""", # noqa pylint: disable=line-too-long + ], + )