Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Case insensitivity of sheet, column, and setting names; plus more performance improvements #746

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 43 additions & 63 deletions pyxform/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,77 +44,57 @@
}
settings_header = {
"form_title": constants.TITLE,
"set form title": constants.TITLE,
"set_form_title": constants.TITLE,
"form_id": constants.ID_STRING,
"sms_keyword": constants.SMS_KEYWORD,
"sms_separator": constants.SMS_SEPARATOR,
"sms_allow_media": constants.SMS_ALLOW_MEDIA,
"sms_date_format": constants.SMS_DATE_FORMAT,
"sms_datetime_format": constants.SMS_DATETIME_FORMAT,
"set_form_id": constants.ID_STRING,
"prefix": constants.COMPACT_PREFIX,
"delimiter": constants.COMPACT_DELIMITER,
"set form id": constants.ID_STRING,
"public_key": constants.PUBLIC_KEY,
"submission_url": constants.SUBMISSION_URL,
"auto_send": constants.AUTO_SEND,
"auto_delete": constants.AUTO_DELETE,
"allow_choice_duplicates": constants.ALLOW_CHOICE_DUPLICATES,
}
# TODO: Check on bind prefix approach in json.
# Conversion dictionary from user friendly column names to meaningful values
survey_header = {
"Label": "label",
"Name": "name",
"SMS Field": constants.SMS_FIELD,
"SMS Option": constants.SMS_OPTION,
"SMS Separator": constants.SMS_SEPARATOR,
"SMS Allow Media": constants.SMS_ALLOW_MEDIA,
"SMS Date Format": constants.SMS_DATE_FORMAT,
"SMS DateTime Format": constants.SMS_DATETIME_FORMAT,
"SMS Response": constants.SMS_RESPONSE,
"compact_tag": "instance::odk:tag", # used for compact representation
"Type": "type",
"List_name": constants.LIST_NAME_U,
# u"repeat_count": u"jr:count", duplicate key
"read_only": "bind::readonly",
"readonly": "bind::readonly",
"relevant": "bind::relevant",
"sms_field": constants.SMS_FIELD,
"sms_option": constants.SMS_OPTION,
"sms_separator": constants.SMS_SEPARATOR,
"sms_allow_media": constants.SMS_ALLOW_MEDIA,
"sms_date_format": constants.SMS_DATE_FORMAT,
"sms_datetime_format": constants.SMS_DATETIME_FORMAT,
"sms_response": constants.SMS_RESPONSE,
"compact_tag": ("instance", "odk:tag"), # used for compact representation
"read_only": ("bind", "readonly"),
"readonly": ("bind", "readonly"),
"relevant": ("bind", "relevant"),
"caption": constants.LABEL,
"appearance": "control::appearance", # TODO: this is also an issue
"relevance": "bind::relevant",
"required": "bind::required",
"constraint": "bind::constraint",
"constraining message": "bind::jr:constraintMsg",
"constraint message": "bind::jr:constraintMsg",
"constraint_message": "bind::jr:constraintMsg",
"calculation": "bind::calculate",
"calculate": "bind::calculate",
"appearance": ("control", "appearance"),
"relevance": ("bind", "relevant"),
"required": ("bind", "required"),
"constraint": ("bind", "constraint"),
"constraining_message": ("bind", "jr:constraintMsg"),
"constraint_message": ("bind", "jr:constraintMsg"),
"calculation": ("bind", "calculate"),
"calculate": ("bind", "calculate"),
"command": constants.TYPE,
"tag": constants.NAME,
"value": constants.NAME,
"image": "media::image",
"big-image": "media::big-image",
"audio": "media::audio",
"video": "media::video",
"count": "control::jr:count",
"repeat_count": "control::jr:count",
"jr:count": "control::jr:count",
"autoplay": "control::autoplay",
"rows": "control::rows",
"image": ("media", "image"),
"big-image": ("media", "big-image"),
"audio": ("media", "audio"),
"video": ("media", "video"),
"count": ("control", "jr:count"),
"repeat_count": ("control", "jr:count"),
"jr:count": ("control", "jr:count"),
"autoplay": ("control", "autoplay"),
"rows": ("control", "rows"),
# New elements that have to go into itext elements:
"noAppErrorString": "bind::jr:noAppErrorString",
"no_app_error_string": "bind::jr:noAppErrorString",
"requiredMsg": "bind::jr:requiredMsg",
"required_message": "bind::jr:requiredMsg",
"required message": "bind::jr:requiredMsg",
"noapperrorstring": ("bind", "jr:noAppErrorString"),
"no_app_error_string": ("bind", "jr:noAppErrorString"),
"requiredmsg": ("bind", "jr:requiredMsg"),
"required_message": ("bind", "jr:requiredMsg"),
"body": "control",
"parameters": "parameters",
constants.ENTITIES_SAVETO: "bind::entities:saveto",
constants.ENTITIES_SAVETO: ("bind", "entities:saveto"),
}

entities_header = {constants.LIST_NAME_U: "dataset"}

# Key is the pyxform internal name, Value is the name used in error/warning messages.
TRANSLATABLE_SURVEY_COLUMNS = {
constants.LABEL: constants.LABEL,
# Per ODK Spec, could include "short" once pyxform supports it.
Expand All @@ -129,19 +109,19 @@
}
TRANSLATABLE_CHOICES_COLUMNS = {
"label": constants.LABEL,
"image": "media::image",
"big-image": "media::big-image",
"audio": "media::audio",
"video": "media::video",
"image": survey_header["image"],
"big-image": survey_header["big-image"],
"audio": survey_header["audio"],
"video": survey_header["video"],
}
list_header = {
"caption": constants.LABEL,
constants.LIST_NAME_U: constants.LIST_NAME_S,
"value": constants.NAME,
"image": "media::image",
"big-image": "media::big-image",
"audio": "media::audio",
"video": "media::video",
"image": survey_header["image"],
"big-image": survey_header["big-image"],
"audio": survey_header["audio"],
"video": survey_header["video"],
}
# Note that most of the type aliasing happens in all.xls
_type_alias_map = {
Expand Down
30 changes: 17 additions & 13 deletions pyxform/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,20 @@ def _create_question_from_dict(
)

if question_class:
if const.CHOICES in d and choices:
return question_class(
question_type_dictionary=question_type_dictionary,
choices=choices.get(d[const.ITEMSET], d[const.CHOICES]),
**{k: v for k, v in d.items() if k != const.CHOICES},
)
else:
return question_class(
question_type_dictionary=question_type_dictionary, **d
)
if choices:
d_choices = d.get(const.CHOICES, d.get(const.CHILDREN))
if d_choices:
return question_class(
question_type_dictionary=question_type_dictionary,
**{
k: v
for k, v in d.items()
if k not in {const.CHOICES, const.CHILDREN}
},
choices=choices.get(d[const.ITEMSET], d_choices),
)

return question_class(question_type_dictionary=question_type_dictionary, **d)

return ()

Expand Down Expand Up @@ -259,16 +263,16 @@ def _name_and_label_substitutions(question_template, column_headers):
const.NAME: column_headers[const.NAME],
const.LABEL: column_headers[const.LABEL][lang],
}
for lang in column_headers[const.LABEL].keys()
for lang in column_headers[const.LABEL]
}

result = question_template.copy()
for key in result.keys():
for key in result:
if isinstance(result[key], str):
result[key] %= column_headers
elif isinstance(result[key], dict):
result[key] = result[key].copy()
for key2 in result[key].keys():
for key2 in result[key]:
if info_by_lang and isinstance(column_headers[const.LABEL], dict):
result[key][key2] %= info_by_lang.get(key2, column_headers)
else:
Expand Down
22 changes: 10 additions & 12 deletions pyxform/entities/entities_parsing.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
from collections.abc import Sequence
from typing import Any

from pyxform import constants as const
from pyxform.errors import PyXFormError
from pyxform.parsing.expression import is_xml_tag
from pyxform.validators.pyxform.sheet_misspellings import find_sheet_misspellings

EC = const.EntityColumns


def get_entity_declaration(
entities_sheet: list[dict], workbook_dict: dict[str, list[dict]], warnings: list[str]
entities_sheet: Sequence[dict],
) -> dict[str, Any]:
if len(entities_sheet) == 0:
similar = find_sheet_misspellings(key=const.ENTITIES, keys=workbook_dict.keys())
if similar is not None:
warnings.append(similar + const._MSG_SUPPRESS_SPELLING)
return {}
elif len(entities_sheet) > 1:
if len(entities_sheet) > 1:
raise PyXFormError(
"Currently, you can only declare a single entity per form. Please make sure your entities sheet only declares one entity."
)
Expand Down Expand Up @@ -83,13 +78,16 @@ def get_validated_dataset_name(entity):


def validate_entity_saveto(
row: dict, row_number: int, entity_declaration: dict[str, Any], in_repeat: bool
row: dict,
row_number: int,
in_repeat: bool,
entity_declaration: dict[str, Any] | None = None,
):
save_to = row.get(const.BIND, {}).get("entities:saveto", "")
if not save_to:
return

if len(entity_declaration) == 0:
if not entity_declaration:
raise PyXFormError(
"To save entity properties using the save_to column, you must add an entities sheet and declare an entity."
)
Expand Down Expand Up @@ -126,9 +124,9 @@ def validate_entity_saveto(


def validate_entities_columns(row: dict):
extra = {k: None for k in row.keys() if k not in EC.value_list()}
extra = {k: None for k in row if k not in EC.value_list()}
if 0 < len(extra):
fmt_extra = ", ".join(f"'{k}'" for k in extra.keys())
fmt_extra = ", ".join(f"'{k}'" for k in extra)
msg = (
f"The entities sheet included the following unexpected column(s): {fmt_extra}. "
f"These columns are not supported by this version of pyxform. Please either: "
Expand Down
Loading
Loading