Repository: XLSForm/pyxform (python)

Issue:
Add default value support for annotate question
If an annotate question has a default value (a filename), the XForm should prepend this value with `jr://images/`.

So if the default column for question annotate question `a` contains `body.png`, the output in the XForm instance should be: `<a>jr://images/body.png</a>`. 

Note that this value transformation is the same as what is used for values in the `media::image::` column.

(as agreed [here](https://forum.opendatakit.org/t/proposal-specify-a-fixed-image-to-be-annotated-or-signed/14954/18) - XForms spec PR [here](https://github.com/opendatakit/xforms-spec/pull/257) 

Pull request:
quick handling of default image values
Closes #405 

#### Why is this the best possible solution? Were any other approaches considered?
This is the simplest and fastest way to get default support for images. I am creating another pull request now which creates broader support for different binary defaults.

#### What are the regression risks?
image questions now have another processing step. Nothing in this step appears unsafe, but if there is a regression, the new step would be the first place to look. Given this is a small change, in the unlikely event of a regression, I believe the fix will be easy.

#### Does this change require updates to documentation? If so, please file an issue [here](https://github.com/XLSForm/xlsform.github.io) and include the link below.
I do not believe so.

#### Before submitting this PR, please make sure you have:
- [x] included test cases for core behavior and edge cases in `tests_v1`
- [x] run `nosetests` and verified all tests pass
- [x] run `black pyxform` to format code
- [x] verified that any code or assets from external sources are properly credited in comments

Patch:
diff --git a/pyxform/aliases.py b/pyxform/aliases.py
index 3c58f9e..7e96f73 100644
--- a/pyxform/aliases.py
+++ b/pyxform/aliases.py
@@ -115,7 +115,7 @@ list_header = {
     "video": "media::video",
 }
 # Note that most of the type aliasing happens in all.xls
-_type = {
+_type_alias_map = {
     "imei": "deviceid",
     "image": "photo",
     "add image prompt": "photo",
diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py
index 4b82a2f..b19e741 100644
--- a/pyxform/xls2json.py
+++ b/pyxform/xls2json.py
@@ -156,8 +156,8 @@ def dealias_types(dict_array):
     """
     for row in dict_array:
         found_type = row.get(constants.TYPE)
-        if found_type in aliases._type.keys():
-            row[constants.TYPE] = aliases._type[found_type]
+        if found_type in aliases._type_alias_map.keys():
+            row[constants.TYPE] = aliases._type_alias_map[found_type]
     return dict_array
 
 
@@ -304,6 +304,14 @@ def process_range_question_type(row):
     return new_dict
 
 
+def process_image_default(default_value):
+    # prepend image files with the correct prefix, if they don't have it.
+    image_jr_prefix = "jr://images/"
+    if image_jr_prefix not in default_value:
+        return "{}{}".format("jr://images/", default_value)
+    return default_value
+
+
 def find_sheet_misspellings(key: str, keys: "KeysView") -> "Optional[str]":
     """
     Find possible sheet name misspellings to warn the user about.
@@ -357,7 +365,6 @@ def workbook_to_json(
     returns a nested dictionary equivalent to the format specified in the
     json form spec.
     """
-    # ensure required headers are present
     if warnings is None:
         warnings = []
     is_valid = False
@@ -370,11 +377,14 @@ def workbook_to_json(
         if similar is not None:
             msg += similar
         raise PyXFormError(msg)
+
+    # ensure required headers are present
     for row in workbook_dict.get(constants.SURVEY, []):
         is_valid = "type" in [z.lower() for z in row]
         if is_valid:
             break
     if not is_valid:
+        # TODO - could we state what headers are missing?
         raise PyXFormError(
             "The survey sheet is either empty or missing important column headers."
         )
@@ -566,7 +576,6 @@ def workbook_to_json(
     # #################################
 
     # Parse the survey sheet while generating a survey in our json format:
-
     row_number = 1  # We start at 1 because the column header row is not
     #                 included in the survey sheet (presumably).
     # A stack is used to keep track of begin/end expressions
@@ -580,6 +589,7 @@ def workbook_to_json(
     # If a group has a table-list appearance flag
     # this will be set to the name of the list
     table_list = None
+
     # For efficiency we compile all the regular expressions
     # that will be used to parse types:
     end_control_regex = re.compile(
@@ -1204,8 +1214,11 @@ def workbook_to_json(
 
         if question_type == "photo":
             new_dict = row.copy()
-            parameters = get_parameters(row.get("parameters", ""), ["max-pixels"])
 
+            if row.get("default"):
+                new_dict["default"] = process_image_default(row["default"])
+            # Validate max-pixels
+            parameters = get_parameters(row.get("parameters", ""), ["max-pixels"])
             if "max-pixels" in parameters.keys():
                 try:
                     int(parameters["max-pixels"])


Test patch:
diff --git a/tests/test_static_defaults.py b/tests/test_static_defaults.py
new file mode 100644
index 0000000..60e5f0e
--- /dev/null
+++ b/tests/test_static_defaults.py
@@ -0,0 +1,42 @@
+from tests.pyxform_test_case import PyxformTestCase
+
+class StaticDefaultTests(PyxformTestCase):
+    def test_static_defaults(self):
+        self.assertPyxformXform(
+            name="static",
+            md="""
+            | survey |              |          |       |                   |
+            |        | type         | name     | label | default           |
+            |        | integer      | numba    | Foo   | foo               |
+            |        | begin repeat | repeat   |       |                   |
+            |        | integer      | bar      | Bar   | 12                |
+            |        | end repeat   | repeat   |       |                   |
+            """,
+            model__contains=["<numba>foo</numba>", "<bar>12</bar>"],
+            model__excludes=["setvalue", "<numba />"],
+        )
+
+    def test_static_image_defaults(self):
+        self.assertPyxformXform(
+            name="static_image",
+            md="""
+            | survey |        |          |       |                |                        |
+            |        | type   | name     | label | parameters     | default                |
+            |        | image  | my_image | Image | max-pixels=640 | my_default_image.jpg   |
+            |        | text   | my_descr | descr |                | no description provied |
+            """,
+            model__contains=[
+                # image needed NS and question typing still exist!
+                'xmlns:orx="http://openrosa.org/xforms"',
+                '<bind nodeset="/static_image/my_image" type="binary" orx:max-pixels="640"/>',
+                # image default appears
+                "<my_image>jr://images/my_default_image.jpg</my_image>",
+                # other defaults appear
+                "<my_descr>no description provied</my_descr>",
+            ],
+            model__excludes=[
+                "setvalue",
+                "<my_image></my_image>",
+                "<my_descr></my_descr>",
+            ],
+        )