Skip to content

Commit

Permalink
Adding filter style cascading selects (see new_cascading_select.xls),…
Browse files Browse the repository at this point in the history
… all selects are now implemented using itemsets (for simplicity), reimplemented or_other completely in xls2json, updating Validate to the Collect 1.2 version.
  • Loading branch information
nathanathan committed Jul 26, 2012
1 parent e340488 commit b018098
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 57 deletions.
Binary file modified pyxform/odk_validate/java_lib/ODK Validate.jar
Binary file not shown.
111 changes: 67 additions & 44 deletions pyxform/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,50 +112,73 @@ def validate(self):

class MultipleChoiceQuestion(Question):

def __init__(self, *args, **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 = kwargs_copy.pop(u"choices", []) + \
kwargs_copy.pop(u"children", [])
Question.__init__(self, *args, **kwargs_copy)
for choice in choices:
self.add_choice(**choice)

def add_choice(self, **kwargs):
option = Option(**kwargs)
self.add_child(option)

def validate(self):
Question.validate(self)
descendants = self.iter_descendants()
descendants.next() # iter_descendants includes self; we need to pop it
for choice in descendants:
choice.validate()

def xml_control(self):
assert self.bind[u"type"] in [u"select", u"select1"] #Why select1? -- odk/jr use select1 for single-option-select

control_dict = self.control
if u"appearance" in control_dict:
result = node(
self.bind[u"type"],
ref=self.get_xpath(),
appearance=control_dict[u"appearance"]
)
else:
result = node(
self.bind[u"type"],
ref=self.get_xpath()
)
for n in self.xml_label_and_hint():
result.appendChild(n)
for n in [o.xml() for o in self.children]:
result.appendChild(n)
assert self.bind[u"type"] in [u"select", u"select1"]
control_dict = self.control.copy()
control_dict['ref'] = self.get_xpath()
nodeset = "instance('" + self['itemset'] + "')/root/item"
choice_filter = self.get('choice_filter')
if choice_filter:
survey = self.get_root()
choice_filter = survey.insert_xpaths(choice_filter)
nodeset += '[' + choice_filter + ']'
result = node(**control_dict)
for element in self.xml_label_and_hint():
result.appendChild(element)
itemset_label_ref = "jr:itext(itextId)"
itemset_children = [node('value', ref='name'), node('label', ref=itemset_label_ref)]
result.appendChild(node('itemset', *itemset_children, nodeset=nodeset))
return result



class SelectOneQuestion(MultipleChoiceQuestion):
def __init__(self, *args, **kwargs):
super(SelectOneQuestion, self).__init__(*args, **kwargs)
self._dict[self.TYPE] = u"select one"
pass

#class MultipleChoiceQuestion(Question):
#
# def __init__(self, *args, **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 = kwargs_copy.pop(u"choices", []) + \
# kwargs_copy.pop(u"children", [])
# Question.__init__(self, *args, **kwargs_copy)
# for choice in choices:
# self.add_choice(**choice)
#
# def add_choice(self, **kwargs):
# option = Option(**kwargs)
# self.add_child(option)
#
# def validate(self):
# Question.validate(self)
# descendants = self.iter_descendants()
# descendants.next() # iter_descendants includes self; we need to pop it
# for choice in descendants:
# choice.validate()
#
# def xml_control(self):
# assert self.bind[u"type"] in [u"select", u"select1"] #Why select1? -- odk/jr use select1 for single-option-select
#
# control_dict = self.control
# if u"appearance" in control_dict:
# result = node(
# self.bind[u"type"],
# ref=self.get_xpath(),
# appearance=control_dict[u"appearance"]
# )
# else:
# result = node(
# self.bind[u"type"],
# ref=self.get_xpath()
# )
# for n in self.xml_label_and_hint():
# result.appendChild(n)
# for n in [o.xml() for o in self.children]:
# result.appendChild(n)
# return result
#
#
#class SelectOneQuestion(MultipleChoiceQuestion):
# def __init__(self, *args, **kwargs):
# super(SelectOneQuestion, self).__init__(*args, **kwargs)
# self._dict[self.TYPE] = u"select one"
64 changes: 57 additions & 7 deletions pyxform/survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ class Survey(Section):
u"submission_url": unicode,
u"public_key": unicode,
u"version": unicode,
}
)
u"choices": dict,
}
)

def validate(self):
super(Survey, self).validate()
Expand Down Expand Up @@ -65,6 +66,36 @@ def xml(self):
node(u"h:body", *self.xml_control()),
**nsmap
)
def _generate_static_instance_itext(self):
"""
Generates <itext> elements for static data (e.g. choices for select type questions)
"""
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():
if isinstance(choicePropertyValue, dict):
itextId = '-'.join(['static_instance', list_name, str(idx)])
itextNode = node("text", id=itextId)
for key, value in choicePropertyValue.items():
itextNode.appendChild(node("value", value, form=key))
yield itextNode

def _generate_static_instances(self):
"""
Generates <instance> elements for static data (e.g. choices for select type questions)
"""
for list_name, choice_list in self.choices.items():
instance_element_list = []
for idx, choice in zip(range(len(choice_list)), choice_list):
choice_element_list = []
for choicePropertyName, choicePropertyValue in choice.items():
if isinstance(choicePropertyValue, basestring):
choice_element_list.append(node(choicePropertyName, unicode(choicePropertyValue)))
elif isinstance(choicePropertyValue, dict):
itextId = '-'.join(['static_instance', list_name, str(idx)])
choice_element_list.append(node("itextId", itextId))
instance_element_list.append(node("item", *choice_element_list))
yield node("instance", node("root", *instance_element_list), id=list_name)

def xml_model(self):
"""
Expand All @@ -73,9 +104,13 @@ def xml_model(self):
self._setup_translations()
self._setup_media()
self._add_empty_translations()
model_children = [node("instance", self.xml_instance())] + self.xml_bindings()
if self._translations:
model_children.insert(0, self.itext())

model_children = []
model_children.append(self.itext())#This will in many cases add an unnessairy itext node
model_children += [node("instance", self.xml_instance())]
model_children += list(self._generate_static_instances())
model_children += self.xml_bindings()

if self.submission_url or self.public_key:
submission_attrs = dict()
if self.submission_url:
Expand All @@ -101,7 +136,22 @@ def _setup_translations(self):
for element in self.iter_descendants():
for d in element.get_translations(self.default_language):
self._translations[d['lang']][d['path']] = {"long" : d['text']}


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():
if isinstance(choicePropertyValue, dict):
itextId = '-'.join(['static_instance', list_name, str(idx)])
for mediatypeorlanguage, value in choicePropertyValue.items():
if isinstance(value, dict):
for langauge, value in value.items():
self._translations[langauge][itextId] = {mediatypeorlanguage : value}
else:
if choicePropertyName is 'media':
self._translations['default'][itextId] = {mediatypeorlanguage : value}
else:
self._translations[mediatypeorlanguage][itextId] = {'long' : value}

def _add_empty_translations(self):
"""
Adds translations so that every itext element has the same elements accross every language.
Expand Down Expand Up @@ -181,7 +231,7 @@ def itext(self):
result = []
for lang, translation in self._translations.items():
if lang == self.default_language:
result.append(node("translation", lang=lang,default=u"true()"))
result.append(node("translation", lang=lang, default=u"true()"))
#result.append(node("translation", lang=lang))
else:
result.append(node("translation", lang=lang))
Expand Down
2 changes: 2 additions & 0 deletions pyxform/survey_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class SurveyElement(dict):
# this node will also have a parent and children, like a tree!
u"parent": lambda: None,
u"children": list,
u"itemset": dict,
u"choice_filter": unicode,
}

def _default(self):
Expand Down
Binary file not shown.
35 changes: 29 additions & 6 deletions pyxform/xls2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def workbook_to_json(workbook_dict, form_name=None, default_language=u"default",


choices = combined_lists

json_dict['choices'] = choices
########### Cascading Select sheet ###########
cascading_choices = workbook_dict.get(constants.CASCADING_CHOICES, {})

Expand Down Expand Up @@ -508,14 +508,34 @@ def workbook_to_json(workbook_dict, form_name=None, default_language=u"default",
if select_type == constants.SELECT_ALL_THAT_APPLY:
for choice in choices[list_name]:
if ' ' in choice[constants.NAME]:
raise PyXFormError("Choice names with spaces cannot be added to multiple choice selects. See [" + choice[constants.NAME] + "] in [" + list_name + "]")

raise PyXFormError("Choice names with spaces cannot be added to multiple choice selects. See [" + choice[constants.NAME] + "] in [" + list_name + "]")

specify_other_question = None
if parse_dict.get("specify_other") is not None:
select_type += u" or specify other"
#With this code we no longer need to handle or_other questions in survey builder.
choices[list_name].append(
{
'name': 'other',
'label': 'Other',
'or_other': 'true',
})
if 'choice_filter' in row:
row['choice_filter'] += ' and or_other="true"'
else:
row['choice_filter'] = 'or_other="true"'

specify_other_question = \
{
'type':'text',
'name': row['name'] + '_specify_other',
'label':'Specify Other for:\n"' + row['label'] + '"',
'bind' : {'relevant': "selected(../%s, 'other')" % row['name']},
}

new_json_dict = row.copy()
new_json_dict[constants.TYPE] = select_type
new_json_dict[constants.CHOICES] = choices[list_name]
#new_json_dict[constants.CHOICES] = choices[list_name]
new_json_dict['itemset'] = list_name

#Code to deal with table_list appearance flags (for groups of selects)
if table_list or begin_table_list:
Expand All @@ -525,7 +545,8 @@ def workbook_to_json(workbook_dict, form_name=None, default_language=u"default",
constants.TYPE : select_type,
constants.NAME : "reserved_name_for_field_list_labels_" + str(row_number), #Adding row number for uniqueness
constants.CONTROL : { u"appearance" : u"label" },
constants.CHOICES : choices[list_name]
#constants.CHOICES : choices[list_name]
'itemset' : list_name,
}
parent_children_array.append(table_list_header)
begin_table_list = False
Expand All @@ -539,6 +560,8 @@ def workbook_to_json(workbook_dict, form_name=None, default_language=u"default",
control[u"appearance"] = "list-nolabel"

parent_children_array.append(new_json_dict)
if specify_other_question:
parent_children_array.append(specify_other_question)
continue

#TODO: Consider adding some question_type validation here.
Expand Down

0 comments on commit b018098

Please sign in to comment.