diff --git a/pyxform/aliases.py b/pyxform/aliases.py index 2d24fd53..7af0d388 100644 --- a/pyxform/aliases.py +++ b/pyxform/aliases.py @@ -90,6 +90,7 @@ "tag": constants.NAME, "value": constants.NAME, "image": "media::image", + "big-image": "media::big-image", "audio": "media::audio", "video": "media::video", "count": "control::jr:count", @@ -114,7 +115,7 @@ constants.HINT: constants.HINT, "guidance_hint": "guidance_hint", "image": survey_header["image"], - # Per ODK Spec, could include "big-image" once pyxform supports it. + "big-image": survey_header["big-image"], "audio": survey_header["audio"], "video": survey_header["video"], "jr:constraintMsg": "constraint_message", @@ -123,6 +124,7 @@ TRANSLATABLE_CHOICES_COLUMNS = { "label": constants.LABEL, "image": "media::image", + "big-image": "media::big-image", "audio": "media::audio", "video": "media::video", } @@ -131,6 +133,7 @@ "list_name": constants.LIST_NAME, "value": constants.NAME, "image": "media::image", + "big-image": "media::big-image", "audio": "media::audio", "video": "media::video", } diff --git a/pyxform/json_form_schema.json b/pyxform/json_form_schema.json index e1dd1f90..b2321b1e 100644 --- a/pyxform/json_form_schema.json +++ b/pyxform/json_form_schema.json @@ -144,6 +144,15 @@ "description" : "A key value pair where the key is a language, and the value is the content uri in that language." } }, + "big-image" : + { + "type" : ["object", "string"], + "additionalProperties": + { + "type" : "string", + "description" : "A key value pair where the key is a language, and the value is the content uri in that language." + } + }, "audio" : { "type" : ["object", "string"], diff --git a/pyxform/survey.py b/pyxform/survey.py index bbbf26a6..dcdc3471 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -827,7 +827,7 @@ def itext(self): itext_nodes.append( node("value", value, toParseString=output_inserted) ) - elif media_type == "image": + elif media_type == "image" or media_type == "big-image": if value != "-": itext_nodes.append( node( diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index ade87aba..332c8421 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -152,7 +152,7 @@ def add_children(self, children): ) # Supported media types for attaching to questions - SUPPORTED_MEDIA = ("image", "audio", "video") + SUPPORTED_MEDIA = ("image", "big-image", "audio", "video") def validate(self): if not is_valid_xml_tag(self.name): @@ -428,10 +428,17 @@ def xml_label_and_hint(self) -> "List[DetachableElement]": msg = "The survey element named '%s' " "has no label or hint." % self.name if len(result) == 0: raise PyXFormError(msg) + # Guidance hint alone is not OK since they may be hidden by default. if not any((self.label, self.media, self.hint)) and self.guidance_hint: raise PyXFormError(msg) + # big-image must combine with image + if "image" not in self.media and "big-image" in self.media: + raise PyXFormError( + "To use big-image, you must also specify an image for the survey element named {self.name}." + ) + return result def xml_bindings(self): diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index 106d5c04..cb5ca943 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -29,7 +29,7 @@ def test_missing_name_but_has_alias_of_name(self): errored=False, ) - def test_missing_label(self): + def test_label_or_hint__must_be_provided(self): self.assertPyxformXform( name="invalidcols", ss_structure={"survey": [{"type": "text", "name": "q1"}]}, @@ -63,6 +63,18 @@ def test_label_node_added_when_hint_given_but_no_label_value(self): xml__contains=prep_for_xml_contains(expected), ) + def test_big_image_invalid_if_no_image(self): + self.assertPyxformXform( + name="data", + md=""" + | survey | | | | + | | type | name | media::big-image | + | | text | c | m.png | + """, + errored=True, + error__contains=["must also specify an image"], + ) + def test_column_case(self): """ Ensure that column name is case insensitive diff --git a/tests/test_translations.py b/tests/test_translations.py index 40ed1873..9243e2f7 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -95,6 +95,7 @@ def question_itext_form(self, lang, form, fname): prefix = { "audio": ("label", "jr://audio/"), "image": ("label", "jr://images/"), + "big-image": ("label", "jr://images/"), "video": ("label", "jr://video/"), "guidance": ("hint", ""), } @@ -129,6 +130,7 @@ def question_no_itext_form(self, lang, form, fname): prefix = { "audio": ("label", "jr://audio/"), "image": ("label", "jr://images/"), + "big-image": ("label", "jr://images/"), "video": ("label", "jr://video/"), "guidance": ("hint", ""), } @@ -165,6 +167,7 @@ def choice_itext_form(self, lang, cname, form, fname): prefix = { "audio": "jr://audio/", "image": "jr://images/", + "big-image": "jr://images/", "video": "jr://video/", } return f""" @@ -188,6 +191,7 @@ def choice_no_itext_form(self, lang, cname, form, fname): prefix = { "audio": "jr://audio/", "image": "jr://images/", + "big-image": "jr://images/", "video": "jr://video/", } return f""" @@ -228,6 +232,7 @@ def choice_instance_itext_form(lang, lname, form, fname, index): prefix = { "audio": "jr://audio/", "image": "jr://images/", + "big-image": "jr://images/", "video": "jr://video/", } return f""" @@ -601,6 +606,25 @@ def test_no_default__no_translation__label_and_hint_all_cols(self): warnings_count=0, ) + def test_no_default__no_translation__image_with_big_image(self): + """Should find default language translations for image and big-image.""" + md = """ + | survey | | | | | + | | type | name | media::image | media::big-image | + | | note | n1 | greeting.jpg | greeting.jpg | + """ + self.assertPyxformXform( + name="test", + debug=True, + md=md, + xml__xpath_match=[ + self.xp.question_itext_form(DEFAULT_LANG, "image", "greeting.jpg"), + self.xp.question_itext_form(DEFAULT_LANG, "big-image", "greeting.jpg"), + self.xp.language_is_default(DEFAULT_LANG), + ], + warnings_count=0, + ) + def test_no_default__one_translation__label_and_hint(self): """Should find language translations for label and hint.""" md = """ @@ -670,9 +694,9 @@ def test_no_default__one_translation__label_and_hint_with_guidance(self): def test_no_default__one_translation__label_and_hint_all_cols(self): """Should find language translation for label, hint, and all translatables.""" md = """ - | survey | | | | | | | | | | | - | | type | name | label::eng | hint::eng | guidance_hint::eng | media::image::eng | media::video::eng | media::audio::eng | constraint_message::eng | required_message::eng | - | | note | n1 | hello | salutation | greeting | greeting.jpg | greeting.mkv | greeting.mp3 | check me | mandatory | + | survey | | | | | | | | | | | | + | | type | name | label::eng | hint::eng | guidance_hint::eng | media::image::eng | media::big-image::eng | media::video::eng | media::audio::eng | constraint_message::eng | required_message::eng | + | | note | n1 | hello | salutation | greeting | greeting.jpg | greeting.jpg | greeting.mkv | greeting.mp3 | check me | mandatory | """ self.assertPyxformXform( name="test", @@ -684,6 +708,7 @@ def test_no_default__one_translation__label_and_hint_all_cols(self): self.xp.question_itext_hint("eng", "salutation"), self.xp.question_itext_form("eng", "guidance", "greeting"), self.xp.question_itext_form("eng", "image", "greeting.jpg"), + self.xp.question_itext_form("eng", "big-image", "greeting.jpg"), self.xp.question_itext_form("eng", "video", "greeting.mkv"), self.xp.question_itext_form("eng", "audio", "greeting.mp3"), self.xp.constraint_msg_references_itext(), @@ -747,9 +772,9 @@ def test_missing_translation__one_lang_simple__warn__default(self): def test_missing_translation__one_lang_all_cols__warn__no_default(self): """Should warn if there's multiple missing translations and no default_language.""" md = """ - | survey | | | | | | | | | | | | - | | type | name | label | label::eng | hint::eng | guidance_hint::eng | media::image::eng | media::video::eng | media::audio::eng | constraint_message::eng | required_message::eng | - | | note | n1 | hello | hi there | salutation | greeting | greeting.jpg | greeting.mkv | greeting.mp3 | check me | mandatory | + | survey | | | | | | | | | | | | | + | | type | name | label | label::eng | hint::eng | guidance_hint::eng | media::image::eng | media::big-image::eng | media::video::eng | media::audio::eng | constraint_message::eng | required_message::eng | + | | note | n1 | hello | hi there | salutation | greeting | greeting.jpg | greeting.jpg | greeting.mkv | greeting.mp3 | check me | mandatory | """ cols = { SURVEY: { @@ -757,6 +782,7 @@ def test_missing_translation__one_lang_all_cols__warn__no_default(self): "hint", "guidance_hint", "media::image", + "media::big-image", "media::video", "media::audio", "constraint_message", @@ -780,6 +806,8 @@ def test_missing_translation__one_lang_all_cols__warn__no_default(self): self.xp.question_itext_form(DEFAULT_LANG, "guidance", "-"), self.xp.question_itext_form("eng", "image", "greeting.jpg"), self.xp.question_no_itext_form(DEFAULT_LANG, "image", "greeting.jpg"), + self.xp.question_itext_form("eng", "big-image", "greeting.jpg"), + self.xp.question_no_itext_form(DEFAULT_LANG, "big-image", "greeting.jpg"), self.xp.question_itext_form("eng", "video", "greeting.mkv"), self.xp.question_no_itext_form(DEFAULT_LANG, "video", "greeting.mkv"), self.xp.question_itext_form("eng", "audio", "greeting.mp3"), @@ -801,9 +829,9 @@ def test_missing_translation__one_lang_all_cols__warn__default(self): | settings | | | | default_language | | | eng | - | survey | | | | | | | | | | | | - | | type | name | label | label::eng | hint::eng | guidance_hint::eng | media::image::eng | media::video::eng | media::audio::eng | constraint_message::eng | required_message::eng | - | | note | n1 | hello | hi there | salutation | greeting | greeting.jpg | greeting.mkv | greeting.mp3 | check me | mandatory | + | survey | | | | | | | | | | | | | + | | type | name | label | label::eng | hint::eng | guidance_hint::eng | media::image::eng | media::big-image::eng | media::video::eng | media::audio::eng | constraint_message::eng | required_message::eng | + | | note | n1 | hello | hi there | salutation | greeting | greeting.jpg | greeting.jpg | greeting.mkv | greeting.mp3 | check me | mandatory | """ # cols = { # SURVEY: { @@ -1024,10 +1052,10 @@ def test_select1__non_dynamic_choices__no_lang__all_columns(self): | survey | | | | | | type | name | label | | | select_one c1 | q1 | Question 1 | - | choices | | | | | | | - | | list name | name | label | media::audio | media::image | media::video | - | | c1 | na | la | a.mp3 | a.jpg | a.mkv | - | | c1 | nb | lb | b.mp3 | b.jpg | b.mkv | + | choices | | | | | | | | + | | list name | name | label | media::audio | media::image | media::big-image | media::video | + | | c1 | na | la | a.mp3 | a.jpg | a.jpg | a.mkv | + | | c1 | nb | lb | b.mp3 | b.jpg | b.jpg | b.mkv | """ xpath_match = [ self.xp.question_label_in_body("Question 1"), @@ -1038,10 +1066,12 @@ def test_select1__non_dynamic_choices__no_lang__all_columns(self): self.xp.choice_itext_label(DEFAULT_LANG, "na", "la"), self.xp.choice_itext_form(DEFAULT_LANG, "na", "audio", "a.mp3"), self.xp.choice_itext_form(DEFAULT_LANG, "na", "image", "a.jpg"), + self.xp.choice_itext_form(DEFAULT_LANG, "na", "big-image", "a.jpg"), self.xp.choice_itext_form(DEFAULT_LANG, "na", "video", "a.mkv"), self.xp.choice_itext_label(DEFAULT_LANG, "nb", "lb"), self.xp.choice_itext_form(DEFAULT_LANG, "nb", "audio", "b.mp3"), self.xp.choice_itext_form(DEFAULT_LANG, "nb", "image", "b.jpg"), + self.xp.choice_itext_form(DEFAULT_LANG, "nb", "big-image", "b.jpg"), self.xp.choice_itext_form(DEFAULT_LANG, "nb", "video", "b.mkv"), ] self.assertPyxformXform( @@ -1056,10 +1086,10 @@ def test_select1__non_dynamic_choices__one_lang__all_columns(self): | survey | | | | | | type | name | label | | | select_one c1 | q1 | Question 1 | - | choices | | | | | | | - | | list name | name | label::Eng (en) | media::audio::Eng (en) | media::image::Eng (en) | media::video::Eng (en) | - | | c1 | na | la | a.mp3 | a.jpg | a.mkv | - | | c1 | nb | lb | b.mp3 | b.jpg | b.mkv | + | choices | | | | | | | | + | | list name | name | label::Eng (en) | media::audio::Eng (en) | media::image::Eng (en) | media::big-image::Eng (en) | media::video::Eng (en) | + | | c1 | na | la | a.mp3 | a.jpg | a.jpg | a.mkv | + | | c1 | nb | lb | b.mp3 | b.jpg | b.jpg | b.mkv | """ xpath_match = [ self.xp.question_label_in_body("Question 1"), @@ -1070,10 +1100,12 @@ def test_select1__non_dynamic_choices__one_lang__all_columns(self): self.xp.choice_itext_label("Eng (en)", "na", "la"), self.xp.choice_itext_form("Eng (en)", "na", "audio", "a.mp3"), self.xp.choice_itext_form("Eng (en)", "na", "image", "a.jpg"), + self.xp.choice_itext_form("Eng (en)", "na", "big-image", "a.jpg"), self.xp.choice_itext_form("Eng (en)", "na", "video", "a.mkv"), self.xp.choice_itext_label("Eng (en)", "nb", "lb"), self.xp.choice_itext_form("Eng (en)", "nb", "audio", "b.mp3"), self.xp.choice_itext_form("Eng (en)", "nb", "image", "b.jpg"), + self.xp.choice_itext_form("Eng (en)", "nb", "big-image", "b.jpg"), self.xp.choice_itext_form("Eng (en)", "nb", "video", "b.mkv"), ] self.assertPyxformXform( @@ -1088,10 +1120,10 @@ def test_select1__dynamic_choices__no_lang__all_columns(self): | survey | | | | | | | type | name | label | choice_filter | | | select_one c1 | q1 | Question 1 | q1 != '' | - | choices | | | | | | | - | | list name | name | label | media::audio | media::image | media::video | - | | c1 | na | la | a.mp3 | a.jpg | a.mkv | - | | c1 | nb | lb | b.mp3 | b.jpg | b.mkv | + | choices | | | | | | | + | | list name | name | label | media::audio | media::image | media::big-image | media::video | + | | c1 | na | la | a.mp3 | a.jpg | a.jpg | a.mkv | + | | c1 | nb | lb | b.mp3 | b.jpg | b.jpg | b.mkv | """ xpath_match = [ self.xp.question_label_in_body("Question 1"), @@ -1101,10 +1133,16 @@ def test_select1__dynamic_choices__no_lang__all_columns(self): self.xp.choice_instance_itext_label(DEFAULT_LANG, "c1", "la", 0), self.xp.choice_instance_itext_form(DEFAULT_LANG, "c1", "audio", "a.mp3", 0), self.xp.choice_instance_itext_form(DEFAULT_LANG, "c1", "image", "a.jpg", 0), + self.xp.choice_instance_itext_form( + DEFAULT_LANG, "c1", "big-image", "a.jpg", 0 + ), self.xp.choice_instance_itext_form(DEFAULT_LANG, "c1", "video", "a.mkv", 0), self.xp.choice_instance_itext_label(DEFAULT_LANG, "c1", "lb", 1), self.xp.choice_instance_itext_form(DEFAULT_LANG, "c1", "audio", "b.mp3", 1), self.xp.choice_instance_itext_form(DEFAULT_LANG, "c1", "image", "b.jpg", 1), + self.xp.choice_instance_itext_form( + DEFAULT_LANG, "c1", "big-image", "b.jpg", 1 + ), self.xp.choice_instance_itext_form(DEFAULT_LANG, "c1", "video", "b.mkv", 1), ] self.assertPyxformXform( @@ -1119,10 +1157,10 @@ def test_select1__dynamic_choices__one_lang__all_columns(self): | survey | | | | | | | type | name | label | choice_filter | | | select_one c1 | q1 | Question 1 | q1 != '' | - | choices | | | | | | | - | | list name | name | label::Eng (en) | media::audio::Eng (en) | media::image::Eng (en) | media::video::Eng (en) | - | | c1 | na | la | a.mp3 | a.jpg | a.mkv | - | | c1 | nb | lb | b.mp3 | b.jpg | b.mkv | + | choices | | | | | | | | + | | list name | name | label::Eng (en) | media::audio::Eng (en) | media::image::Eng (en) | media::big-image::Eng (en) | media::video::Eng (en) | + | | c1 | na | la | a.mp3 | a.jpg | a.jpg | a.mkv | + | | c1 | nb | lb | b.mp3 | b.jpg | b.jpg | b.mkv | """ xpath_match = [ self.xp.question_label_in_body("Question 1"), @@ -1132,10 +1170,12 @@ def test_select1__dynamic_choices__one_lang__all_columns(self): self.xp.choice_instance_itext_label("Eng (en)", "c1", "la", 0), self.xp.choice_instance_itext_form("Eng (en)", "c1", "audio", "a.mp3", 0), self.xp.choice_instance_itext_form("Eng (en)", "c1", "image", "a.jpg", 0), + self.xp.choice_instance_itext_form("Eng (en)", "c1", "big-image", "a.jpg", 0), self.xp.choice_instance_itext_form("Eng (en)", "c1", "video", "a.mkv", 0), self.xp.choice_instance_itext_label("Eng (en)", "c1", "lb", 1), self.xp.choice_instance_itext_form("Eng (en)", "c1", "audio", "b.mp3", 1), self.xp.choice_instance_itext_form("Eng (en)", "c1", "image", "b.jpg", 1), + self.xp.choice_instance_itext_form("Eng (en)", "c1", "big-image", "b.jpg", 1), self.xp.choice_instance_itext_form("Eng (en)", "c1", "video", "b.mkv", 1), ] self.assertPyxformXform( @@ -1224,14 +1264,15 @@ def test_missing_translation__one_lang_all_cols__warn__no_default(self): | | type | name | label | | | select_one c1 | q1 | Question 1 | | choices | | | | - | | list name | name | label | label::eng | media::audio::eng | media::image::eng | media::video::eng | - | | c1 | na | la-d | la-e | la-d.mp3 | la-d.jpg | la-d.mkv | - | | c1 | nb | lb-d | lb-e | lb-d.mp3 | lb-d.jpg | lb-d.mkv | + | | list name | name | label | label::eng | media::audio::eng | media::image::eng | media::big-image::eng | media::video::eng | + | | c1 | na | la-d | la-e | la-d.mp3 | la-d.jpg | la-d.jpg | la-d.mkv | + | | c1 | nb | lb-d | lb-e | lb-d.mp3 | lb-d.jpg | lb-d.jpg | lb-d.mkv | """ cols = { CHOICES: { DEFAULT_LANG: ( "media::image", + "media::big-image", "media::video", "media::audio", ) @@ -1252,17 +1293,21 @@ def test_missing_translation__one_lang_all_cols__warn__no_default(self): self.xp.choice_itext_label("eng", "na", "la-e"), self.xp.choice_itext_form("eng", "na", "audio", "la-d.mp3"), self.xp.choice_itext_form("eng", "na", "image", "la-d.jpg"), + self.xp.choice_itext_form("eng", "na", "big-image", "la-d.jpg"), self.xp.choice_itext_form("eng", "na", "video", "la-d.mkv"), self.xp.choice_no_itext_form(DEFAULT_LANG, "na", "audio", "la-d.mp3"), self.xp.choice_no_itext_form(DEFAULT_LANG, "na", "image", "la-d.jpg"), + self.xp.choice_no_itext_form(DEFAULT_LANG, "na", "big-image", "la-d.jpg"), self.xp.choice_no_itext_form(DEFAULT_LANG, "na", "video", "la-d.mkv"), self.xp.choice_itext_label(DEFAULT_LANG, "nb", "lb-d"), self.xp.choice_itext_label("eng", "nb", "lb-e"), self.xp.choice_itext_form("eng", "nb", "audio", "lb-d.mp3"), self.xp.choice_itext_form("eng", "nb", "image", "lb-d.jpg"), + self.xp.choice_itext_form("eng", "nb", "big-image", "lb-d.jpg"), self.xp.choice_itext_form("eng", "nb", "video", "lb-d.mkv"), self.xp.choice_no_itext_form(DEFAULT_LANG, "nb", "audio", "lb-d.mp3"), self.xp.choice_no_itext_form(DEFAULT_LANG, "nb", "image", "lb-d.jpg"), + self.xp.choice_no_itext_form(DEFAULT_LANG, "nb", "big-image", "lb-d.jpg"), self.xp.choice_no_itext_form(DEFAULT_LANG, "nb", "video", "lb-d.mkv"), self.xp.language_is_default(DEFAULT_LANG), self.xp.language_is_not_default("eng"), @@ -1279,9 +1324,9 @@ def test_missing_translation__one_lang_all_cols__warn__default(self): | | type | name | label | | | select_one c1 | q1 | Question 1 | | choices | | | | - | | list name | name | label | label::eng | media::audio::eng | media::image::eng | media::video::eng | - | | c1 | na | la-d | la-e | la-d.mp3 | la-d.jpg | la-d.mkv | - | | c1 | nb | lb-d | lb-e | lb-d.mp3 | lb-d.jpg | lb-d.mkv | + | | list name | name | label | label::eng | media::audio::eng | media::image::eng | media::big-image::eng | media::video::eng | + | | c1 | na | la-d | la-e | la-d.mp3 | la-d.jpg | la-d.jpg | la-d.mkv | + | | c1 | nb | lb-d | lb-e | lb-d.mp3 | lb-d.jpg | lb-d.jpg | lb-d.mkv | """ # cols = { # CHOICES: { @@ -1307,10 +1352,12 @@ def test_missing_translation__one_lang_all_cols__warn__default(self): self.xp.choice_itext_label("eng", "na", "la-e"), self.xp.choice_itext_form("eng", "na", "audio", "la-d.mp3"), self.xp.choice_itext_form("eng", "na", "image", "la-d.jpg"), + self.xp.choice_itext_form("eng", "na", "big-image", "la-d.jpg"), self.xp.choice_itext_form("eng", "na", "video", "la-d.mkv"), self.xp.choice_itext_label("eng", "nb", "lb-e"), self.xp.choice_itext_form("eng", "nb", "audio", "lb-d.mp3"), self.xp.choice_itext_form("eng", "nb", "image", "lb-d.jpg"), + self.xp.choice_itext_form("eng", "nb", "big-image", "lb-d.jpg"), self.xp.choice_itext_form("eng", "nb", "video", "lb-d.mkv"), self.xp.language_is_default("eng"), self.xp.language_no_itext(DEFAULT_LANG),