Skip to content

Commit

Permalink
Merge pull request #614 from lindsay-stevens/pyxform-603
Browse files Browse the repository at this point in the history
603: Always generate secondary instance for selects
  • Loading branch information
lognaturel authored Jul 31, 2023
2 parents 66d5e89 + 751e4ea commit 129abe9
Show file tree
Hide file tree
Showing 49 changed files with 1,646 additions and 1,820 deletions.
2 changes: 1 addition & 1 deletion pyxform/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"select one": constants.SELECT_ONE,
"select_multiple": constants.SELECT_ALL_THAT_APPLY,
"select all that apply": constants.SELECT_ALL_THAT_APPLY,
"select_one_external": "select one external",
"select_one_external": constants.SELECT_ONE_EXTERNAL,
"select_one_from_file": constants.SELECT_ONE,
"select_multiple_from_file": constants.SELECT_ALL_THAT_APPLY,
"select one from file": constants.SELECT_ONE,
Expand Down
64 changes: 38 additions & 26 deletions pyxform/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import copy
import os
import re
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from pyxform import file_utils, utils
from pyxform import constants, file_utils, utils
from pyxform.entities.entity_declaration import EntityDeclaration
from pyxform.errors import PyXFormError
from pyxform.external_instance import ExternalInstance
Expand All @@ -24,6 +25,14 @@
from pyxform.survey import Survey
from pyxform.xls2json import SurveyReader

if TYPE_CHECKING:
from pyxform.survey_element import SurveyElement

OR_OTHER_CHOICE = {
"name": "other",
"label": "Other",
}


def copy_json_dict(json_dict):
"""
Expand Down Expand Up @@ -87,7 +96,9 @@ def set_sections(self, sections):
assert type(sections) == dict
self._sections = sections

def create_survey_element_from_dict(self, d):
def create_survey_element_from_dict(
self, d: Dict[str, Any]
) -> Union["SurveyElement", List["SurveyElement"]]:
"""
Convert from a nested python dictionary/array structure (a json dict I
call it because it corresponds directly with a json object)
Expand Down Expand Up @@ -142,7 +153,11 @@ def _save_trigger_as_setvalue_and_remove_calculate(self, d):
self.setvalues_by_triggering_ref[triggering_ref] = [(d["name"], value)]

@staticmethod
def _create_question_from_dict(d, question_type_dictionary, add_none_option=False):
def _create_question_from_dict(
d: Dict[str, Any],
question_type_dictionary: Dict[str, Any],
add_none_option: bool = False,
) -> Union[Question, List[Question]]:
question_type_str = d["type"]
d_copy = d.copy()

Expand All @@ -151,11 +166,9 @@ def _create_question_from_dict(d, question_type_dictionary, add_none_option=Fals
SurveyElementBuilder._add_none_option_to_select_all_that_apply(d_copy)

# Handle or_other on select type questions
or_other_str = " or specify other"
if question_type_str.endswith(or_other_str):
question_type_str = question_type_str[
: len(question_type_str) - len(or_other_str)
]
or_other_len = len(constants.SELECT_OR_OTHER_SUFFIX)
if question_type_str.endswith(constants.SELECT_OR_OTHER_SUFFIX):
question_type_str = question_type_str[: len(question_type_str) - or_other_len]
d_copy["type"] = question_type_str
SurveyElementBuilder._add_other_option_to_multiple_choice_question(d_copy)
return [
Expand All @@ -172,20 +185,18 @@ def _create_question_from_dict(d, question_type_dictionary, add_none_option=Fals
# todo: clean up this spaghetti code
d_copy["question_type_dictionary"] = question_type_dictionary
if question_class:

return question_class(**d_copy)

return []

@staticmethod
def _add_other_option_to_multiple_choice_question(d):
def _add_other_option_to_multiple_choice_question(d: Dict[str, Any]) -> None:
# ideally, we'd just be pulling from children
choice_list = d.get("choices", d.get("children", []))
if len(choice_list) <= 0:
raise PyXFormError("There should be choices for this question.")
other_choice = {"name": "other", "label": "Other"}
if other_choice not in choice_list:
choice_list.append(other_choice)
if OR_OTHER_CHOICE not in choice_list:
choice_list.append(OR_OTHER_CHOICE)

@staticmethod
def _add_none_option_to_select_all_that_apply(d_copy):
Expand Down Expand Up @@ -219,7 +230,7 @@ def _get_question_class(question_type_str, question_type_dictionary):
return SurveyElementBuilder.QUESTION_CLASSES[control_tag]

@staticmethod
def _create_specify_other_question_from_dict(d):
def _create_specify_other_question_from_dict(d: Dict[str, Any]) -> InputQuestion:
kwargs = {
"type": "text",
"name": "%s_other" % d["name"],
Expand All @@ -241,6 +252,11 @@ def _create_section_from_dict(self, d):
# And I hope it doesn't break something else.
# I think the good solution would be to rewrite this class.
survey_element = self.create_survey_element_from_dict(copy.deepcopy(child))
if child["type"].endswith(" or specify other"):
select_question = survey_element[0]
itemset_choices = result["choices"][select_question["itemset"]]
if OR_OTHER_CHOICE not in itemset_choices:
itemset_choices.append(OR_OTHER_CHOICE)
if survey_element:
result.add_children(survey_element)

Expand Down Expand Up @@ -338,13 +354,12 @@ def create_survey_from_xls(path_or_file, default_name=None):


def create_survey(
name_of_main_section=None,
sections=None,
main_section=None,
id_string=None,
title=None,
default_language=None,
):
name_of_main_section: str = None,
sections: Dict[str, Dict] = None,
main_section: Dict[str, Any] = None,
id_string: Optional[str] = None,
title: Optional[str] = None,
) -> Survey:
"""
name_of_main_section -- a string key used to find the main section in the
sections dict if it is not supplied in the
Expand Down Expand Up @@ -380,11 +395,10 @@ def create_survey(

if title is not None:
survey.title = title
survey.def_lang = default_language
return survey


def create_survey_from_path(path, include_directory=False):
def create_survey_from_path(path: str, include_directory: bool = False) -> Survey:
"""
include_directory -- Switch to indicate that all the survey forms in the
same directory as the specified file should be read
Expand All @@ -398,6 +412,4 @@ def create_survey_from_path(path, include_directory=False):
else:
main_section_name, section = file_utils.load_file_to_dict(path)
sections = {main_section_name: section}
pkg = {"name_of_main_section": main_section_name, "sections": sections}

return create_survey(**pkg)
return create_survey(name_of_main_section=main_section_name, sections=sections)
7 changes: 7 additions & 0 deletions pyxform/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
MEDIA = "media"
CONTROL = "control"
APPEARANCE = "appearance"
ITEMSET = "itemset"
RANDOMIZE = "randomize"
CHOICE_FILTER = "choice_filter"
PARAMETERS = "parameters"

LOOP = "loop"
COLUMNS = "columns"
Expand All @@ -56,7 +60,9 @@
CHILDREN = "children"

SELECT_ONE = "select one"
SELECT_ONE_EXTERNAL = "select one external"
SELECT_ALL_THAT_APPLY = "select all that apply"
SELECT_OR_OTHER_SUFFIX = " or specify other"
RANK = "rank"
CHOICES = "choices"

Expand All @@ -65,6 +71,7 @@
CASCADING_SELECT = "cascading_select"
TABLE_LIST = "table-list" # hyphenated because it goes in appearance, and convention for appearance column is dashes # noqa
FIELD_LIST = "field-list"
LIST_NOLABEL = "list-nolabel"

SURVEY = "survey"
SETTINGS = "settings"
Expand Down
18 changes: 15 additions & 3 deletions pyxform/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
XForm Survey element classes for different question types.
"""
import os.path
import re

from pyxform.constants import (
EXTERNAL_CHOICES_ITEMSET_REF_LABEL,
Expand All @@ -15,7 +14,12 @@
from pyxform.errors import PyXFormError
from pyxform.question_type_dictionary import QUESTION_TYPE_DICT
from pyxform.survey_element import SurveyElement
from pyxform.utils import default_is_dynamic, has_dynamic_label, node
from pyxform.utils import (
PYXFORM_REFERENCE_REGEX,
default_is_dynamic,
has_dynamic_label,
node,
)


class Question(SurveyElement):
Expand Down Expand Up @@ -155,6 +159,9 @@ def xml(self):
def validate(self):
pass

def xml_control(self):
raise NotImplementedError()


class MultipleChoiceQuestion(Question):
def __init__(self, **kwargs):
Expand Down Expand Up @@ -222,7 +229,9 @@ def build_xml(self):

has_media = False
has_dyn_label = False
is_previous_question = bool(re.match(r"^\${.*}$", self.get("itemset")))
is_previous_question = bool(
PYXFORM_REFERENCE_REGEX.search(self.get("itemset"))
)

if choices.get(itemset):
has_media = bool(choices[itemset][0].get("media"))
Expand Down Expand Up @@ -326,6 +335,9 @@ def xml(self):
def validate(self):
pass

def xml_control(self):
raise NotImplementedError()


class OsmUploadQuestion(UploadQuestion):
def __init__(self, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion pyxform/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""
from pyxform.errors import PyXFormError
from pyxform.external_instance import ExternalInstance
from pyxform.question import SurveyElement
from pyxform.survey_element import SurveyElement
from pyxform.utils import node


Expand Down
Loading

0 comments on commit 129abe9

Please sign in to comment.