diff --git a/pyxform/aliases.py b/pyxform/aliases.py index 17cfa5c6..86496a1a 100644 --- a/pyxform/aliases.py +++ b/pyxform/aliases.py @@ -96,6 +96,7 @@ "big-image": "media::big-image", "audio": "media::audio", "video": "media::video", + "image-description": "media::image-description", "count": "control::jr:count", "repeat_count": "control::jr:count", "jr:count": "control::jr:count", @@ -142,6 +143,7 @@ "big-image": "media::big-image", "audio": "media::audio", "video": "media::video", + "image-description": "media::image-description", } # Note that most of the type aliasing happens in all.xls _type_alias_map = { diff --git a/pyxform/constants.py b/pyxform/constants.py index 793d1e05..8f485937 100644 --- a/pyxform/constants.py +++ b/pyxform/constants.py @@ -166,5 +166,5 @@ class EntityColumns(StrEnum): "xmlns:orx": "http://openrosa.org/xforms", "xmlns:odk": "http://www.opendatakit.org/xforms", } -SUPPORTED_MEDIA_TYPES = {"image", "big-image", "audio", "video"} +SUPPORTED_MEDIA_TYPES = {"image", "big-image", "audio", "video", "image-description"} OR_OTHER_CHOICE = {NAME: "other", LABEL: "Other"} diff --git a/pyxform/json_form_schema.json b/pyxform/json_form_schema.json index b2321b1e..9b6f49a8 100644 --- a/pyxform/json_form_schema.json +++ b/pyxform/json_form_schema.json @@ -170,6 +170,16 @@ "type" : "string", "description" : "A key value pair where the key is a language, and the value is the content uri in that language." } + }, + "image-description" : + { + "type" : ["object", "string"], + "description" : "Optional string description of the image; eg to use for accessibility." + "additionalProperties": + { + "type" : "string", + "description" : "A key value pair where the key is a language, and the value is the content in that language." + } } } }, diff --git a/pyxform/survey.py b/pyxform/survey.py index 50ecafe9..4cb75996 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -1096,6 +1096,15 @@ def itext(self) -> DetachableElement: toParseString=output_inserted, ) ) + elif media_type == "image-description": + itext_nodes.append( + node( + "value", + value, + form=media_type, + toParseString=output_inserted, + ) + ) elif value != "-": itext_nodes.append( node( diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index 3312e229..094e7440 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -530,6 +530,16 @@ def xml_label_and_hint(self, survey: "Survey") -> list["DetachableElement"]: "To use big-image, you must also specify an image for the survey element named {self.name}." ) + # image-description must be associated with an image + if ( + self.media is not None + and "image" not in self.media + and "image-description" in self.media + ): + raise PyXFormError( + "To use image-description, you must also specify an image for the survey element named {self.name}." + ) + return result def xml_bindings( diff --git a/pyxform/validators/pyxform/choices.py b/pyxform/validators/pyxform/choices.py index 3b347c35..b218514e 100644 --- a/pyxform/validators/pyxform/choices.py +++ b/pyxform/validators/pyxform/choices.py @@ -22,6 +22,11 @@ "If this is intentional, use the setting 'allow_choice_duplicates'. " "Learn more: https://xlsform.org/#choice-names." ) +MISSING_IMAGE = ( + "[row : {row}] On the 'choices' sheet, an 'image' has not been specified. " + "Choices with 'big-image' or 'image-description' require a corresponding image. " + "Learn more: https://xlsform.org/en/#the-choices-worksheet" +) def validate_headers( @@ -55,6 +60,14 @@ def validate_choice_list( else: seen_options.add(name) + # Check the option's media, if specified, is mutually consistent + if "media" in option: + media = option["media"] + if "image" not in media and ( + "big-image" in media or "image-description" in media + ): + duplicate_errors.append(MISSING_IMAGE.format(row=option["__row"])) + if 0 < len(duplicate_errors): raise PyXFormError("\n".join(duplicate_errors)) diff --git a/tests/bug_example_xls/big-image_choice_missing_image.xlsx b/tests/bug_example_xls/big-image_choice_missing_image.xlsx new file mode 100644 index 00000000..963f7313 Binary files /dev/null and b/tests/bug_example_xls/big-image_choice_missing_image.xlsx differ diff --git a/tests/bug_example_xls/big-image_survey_missing_image.xlsx b/tests/bug_example_xls/big-image_survey_missing_image.xlsx new file mode 100644 index 00000000..a0b94555 Binary files /dev/null and b/tests/bug_example_xls/big-image_survey_missing_image.xlsx differ diff --git a/tests/bug_example_xls/image-description_choice_missing_image.xlsx b/tests/bug_example_xls/image-description_choice_missing_image.xlsx new file mode 100644 index 00000000..b0bad98f Binary files /dev/null and b/tests/bug_example_xls/image-description_choice_missing_image.xlsx differ diff --git a/tests/bug_example_xls/image-description_survey_missing_image.xlsx b/tests/bug_example_xls/image-description_survey_missing_image.xlsx new file mode 100644 index 00000000..6fd8af2c Binary files /dev/null and b/tests/bug_example_xls/image-description_survey_missing_image.xlsx differ diff --git a/tests/example_xls/image-description.xlsx b/tests/example_xls/image-description.xlsx new file mode 100644 index 00000000..c2e93cbf Binary files /dev/null and b/tests/example_xls/image-description.xlsx differ diff --git a/tests/example_xls/image-description_translated.xlsx b/tests/example_xls/image-description_translated.xlsx new file mode 100644 index 00000000..4e00f89f Binary files /dev/null and b/tests/example_xls/image-description_translated.xlsx differ diff --git a/tests/example_xls/media-image-description.xlsx b/tests/example_xls/media-image-description.xlsx new file mode 100644 index 00000000..e8e10940 Binary files /dev/null and b/tests/example_xls/media-image-description.xlsx differ diff --git a/tests/example_xls/media-image-description_translated.xlsx b/tests/example_xls/media-image-description_translated.xlsx new file mode 100644 index 00000000..ed88208a Binary files /dev/null and b/tests/example_xls/media-image-description_translated.xlsx differ diff --git a/tests/test_expected_output/image-description.xml b/tests/test_expected_output/image-description.xml new file mode 100644 index 00000000..e6e883ab --- /dev/null +++ b/tests/test_expected_output/image-description.xml @@ -0,0 +1,89 @@ + + + + image-description + + + + + whale + jr://images/a.jpg + whale silhouette + + + frog + jr://images/b.jpg + frog silhouette + + + crocodile + jr://images/c.jpg + crocodile silhouette + + + eagle + jr://images/d.jpg + eagle silhouette + + + What is your name + jr://images/small.jpg + small car + + + + + + + + + + + + + + + + animals-0 + a + + + animals-1 + b + + + animals-2 + c + + + animals-3 + d + + + + + + + + + + + + + diff --git a/tests/test_expected_output/image-description_translated.xml b/tests/test_expected_output/image-description_translated.xml new file mode 100644 index 00000000..f7517179 --- /dev/null +++ b/tests/test_expected_output/image-description_translated.xml @@ -0,0 +1,121 @@ + + + + image-description_translated + + + + + whale + jr://images/a.jpg + whale silhouette + + + frog + jr://images/b.jpg + frog silhouette + + + crocodile + jr://images/c.jpg + crocodile silhouette + + + eagle + jr://images/d.jpg + eagle silhouette + + + What is your name + jr://images/small.jpg + small car + + + Pick your favorite animal + + + + + baleine + jr://images/a.jpg + silhouette de baleine + + + grenouille + jr://images/b.jpg + silhouette de grenouille + + + crocodile + jr://images/c.jpg + silhouette de crocodile + + + aigle + jr://images/d.jpg + silhouette d'aigle + + + Quel est ton nom + jr://images/small.jpg + petite voiture + + + Choisissez votre animal préféré + + + + + + + + + + + + + + + + animals-0 + a + + + animals-1 + b + + + animals-2 + c + + + animals-3 + d + + + + + + + + + + + + diff --git a/tests/test_expected_output/media-image-description.xml b/tests/test_expected_output/media-image-description.xml new file mode 100644 index 00000000..49b7b23a --- /dev/null +++ b/tests/test_expected_output/media-image-description.xml @@ -0,0 +1,89 @@ + + + + media-image-description + + + + + whale + jr://images/a.jpg + whale silhouette + + + frog + jr://images/b.jpg + frog silhouette + + + crocodile + jr://images/c.jpg + crocodile silhouette + + + eagle + jr://images/d.jpg + eagle silhouette + + + What is your name + jr://images/small.jpg + small car + + + + + + + + + + + + + + + + animals-0 + a + + + animals-1 + b + + + animals-2 + c + + + animals-3 + d + + + + + + + + + + + + + diff --git a/tests/test_expected_output/media-image-description_translated.xml b/tests/test_expected_output/media-image-description_translated.xml new file mode 100644 index 00000000..52f64a26 --- /dev/null +++ b/tests/test_expected_output/media-image-description_translated.xml @@ -0,0 +1,122 @@ + + + + media-image-description_translated + + + + + whale + jr://images/a.jpg + whale silhouette + + + frog + jr://images/b.jpg + frog silhouette + + + crocodile + jr://images/c.jpg + crocodile silhouette + + + eagle + jr://images/d.jpg + eagle silhouette + + + What is your name + jr://images/small.jpg + small car + + + Pick your favorite animal + + + + + baleine + jr://images/a.jpg + silhouette de baleine + + + grenouille + jr://images/b.jpg + silhouette de grenouille + + + crocodile + jr://images/c.jpg + silhouette de crocodile + + + aigle + jr://images/d.jpg + silhouette d'aigle + + + Quel est ton nom + jr://images/small.jpg + petite voiture + + + Choisissez votre animal préféré + + + + + + + + + + + + + + + + animals-0 + a + + + animals-1 + b + + + animals-2 + c + + + animals-3 + d + + + + + + + + + + + + + diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index c5db20d1..a628f700 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -31,6 +31,22 @@ def test_conversion_raises(self): ), ("duplicate_columns.xlsx", "Duplicate column header: label"), ("calculate_without_calculation.xls", "[row : 34] Missing calculation."), + ( + "big-image_survey_missing_image.xlsx", + "To use big-image, you must also specify an image for the survey element", + ), + ( + "big-image_choice_missing_image.xlsx", + "On the 'choices' sheet, an 'image' has not been specified.", + ), + ( + "image-description_survey_missing_image.xlsx", + "To use image-description, you must also specify an image for the survey element", + ), + ( + "image-description_choice_missing_image.xlsx", + "On the 'choices' sheet, an 'image' has not been specified.", + ), ) for i, (case, err_msg) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"): diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py index 8e67451e..2784dfe5 100644 --- a/tests/xform_test_case/test_xform_conversion.py +++ b/tests/xform_test_case/test_xform_conversion.py @@ -26,6 +26,10 @@ def test_conversion_vs_expected(self): ("xlsform_spec_test.xlsx", True), ("xml_escaping.xls", True), ("default_time_demo.xls", True), + ("image-description.xlsx", False), + ("media-image-description.xlsx", False), + ("image-description_translated.xlsx", False), + ("media-image-description_translated.xlsx", False), ) for i, (case, set_name) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"):