Repository: XLSForm/pyxform
Pull request #308 (2019-05-23 08:47:51)

Add external instance if pulldata() is used in constraint, required, readonly, relevant expressions
Add external instance if pulldata() is used in constraint, required, readonly, relevant expressions
- required
- constraint
- readonly
- relevant

- functionality of the same for: calculate was retained.

codecov-io: # [Codecov](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=h1) Report
> Merging [#308](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=desc) into [master](https://codecov.io/gh/XLSForm/pyxform/commit/aa9c56ac3701ef7198c27af4d820d140f0a0ad6b?src=pr&el=desc) will **decrease** coverage by `0.92%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/XLSForm/pyxform/pull/308/graphs/tree.svg?width=650&token=DunISqIokF&height=150&src=pr)](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #308      +/-   ##
==========================================
- Coverage   82.18%   81.26%   -0.93%     
==========================================
  Files          23       23              
  Lines        3177     3117      -60     
  Branches      735      732       -3     
==========================================
- Hits         2611     2533      -78     
- Misses        432      450      +18     
  Partials      134      134
```


| [Impacted Files](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyxform/constants.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS9jb25zdGFudHMucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [pyxform/survey.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS9zdXJ2ZXkucHk=) | `89.87% <100%> (+0.23%)` | :arrow_up: |
| [pyxform/xls2xform.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS94bHMyeGZvcm0ucHk=) | `43.33% <0%> (-30.96%)` | :arrow_down: |
| [pyxform/validators/odk\_validate/\_\_init\_\_.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS92YWxpZGF0b3JzL29ka192YWxpZGF0ZS9fX2luaXRfXy5weQ==) | `61.53% <0%> (-12.98%)` | :arrow_down: |
| [pyxform/question\_type\_dictionary.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS9xdWVzdGlvbl90eXBlX2RpY3Rpb25hcnkucHk=) | `50% <0%> (-7.15%)` | :arrow_down: |
| [pyxform/validators/enketo\_validate/\_\_init\_\_.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS92YWxpZGF0b3JzL2Vua2V0b192YWxpZGF0ZS9fX2luaXRfXy5weQ==) | `32.35% <0%> (-1.94%)` | :arrow_down: |
| [pyxform/survey\_element.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS9zdXJ2ZXlfZWxlbWVudC5weQ==) | `88.44% <0%> (-1.66%)` | :arrow_down: |
| [pyxform/builder.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS9idWlsZGVyLnB5) | `74.73% <0%> (-0.67%)` | :arrow_down: |
| [pyxform/file\_utils.py](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree#diff-cHl4Zm9ybS9maWxlX3V0aWxzLnB5) | `88.88% <0%> (-0.59%)` | :arrow_down: |
| ... and [15 more](https://codecov.io/gh/XLSForm/pyxform/pull/308/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=footer). Last update [aa9c56a...19e0e8f](https://codecov.io/gh/XLSForm/pyxform/pull/308?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

p-netm: fixes #251 

Patch:
diff --git a/pyxform/constants.py b/pyxform/constants.py
index 73a4f9e..0741621 100644
--- a/pyxform/constants.py
+++ b/pyxform/constants.py
@@ -91,3 +91,6 @@ LOCATION_PRIORITY = "location-priority"
 LOCATION_MIN_INTERVAL = "location-min-interval"
 LOCATION_MAX_AGE = "location-max-age"
 TRACK_CHANGES = "track-changes"
+
+# supported bind keywords for which external instances will be created for pulldata function
+EXTERNAL_INSTANCES = ["calculate", "constraint", "readonly", "required", "relevant"]
diff --git a/pyxform/survey.py b/pyxform/survey.py
index 2ccc685..85774bb 100644
--- a/pyxform/survey.py
+++ b/pyxform/survey.py
@@ -286,30 +286,46 @@ class Survey(Section):
 
     @staticmethod
     def _generate_pulldata_instances(element):
-        if "calculate" in element["bind"]:
-            calculate = element["bind"]["calculate"]
-            if calculate.startswith("pulldata("):
-                pieces = (
-                    calculate.split('"') if '"' in calculate else calculate.split("'")
-                )
+        def get_pulldata_functions(element):
+            """
+            Returns a list of different pulldata(... function strings if
+            pulldata function is defined at least once for any of:
+            calculate, constraint, readonly, required, relevant
+
+            :param: element (pyxform.survey.Survey):
+            """
+            functions_present = []
+            for formula_name in constants.EXTERNAL_INSTANCES:
+                if unicode(element["bind"].get(formula_name)).startswith("pulldata("):
+                    functions_present.append(element["bind"][formula_name])
+            return functions_present
+
+        formulas = get_pulldata_functions(element)
+        if len(formulas) > 0:
+            formula_instances = []
+            for formula in formulas:
+                pieces = formula.split('"') if '"' in formula else formula.split("'")
                 if len(pieces) > 1 and pieces[1]:
                     file_id = pieces[1]
                     uri = "jr://file-csv/{}.csv".format(file_id)
-                    return InstanceInfo(
-                        type="pulldata",
-                        context="[type: {t}, name: {n}]".format(
-                            t=element["parent"]["type"], n=element["parent"]["name"]
-                        ),
-                        name=file_id,
-                        src=uri,
-                        instance=node(
-                            "instance",
-                            Survey._get_dummy_instance(),
-                            id=file_id,
+                    formula_instances.append(
+                        InstanceInfo(
+                            type=u"pulldata",
+                            context="[type: {t}, name: {n}]".format(
+                                t=element[u"parent"][u"type"],
+                                n=element[u"parent"][u"name"],
+                            ),
+                            name=file_id,
                             src=uri,
-                        ),
+                            instance=node(
+                                "instance",
+                                Survey._get_dummy_instance(),
+                                id=file_id,
+                                src=uri,
+                            ),
+                        )
                     )
-
+            return formula_instances
         return None
 
     @staticmethod
@@ -374,7 +390,9 @@ class Survey(Section):
             i_ext = self._generate_external_instances(element=i)
             i_pull = self._generate_pulldata_instances(element=i)
             i_file = self._generate_from_file_instances(element=i)
-            instances += [x for x in [i_ext, i_pull, i_file] if x is not None]
+            for x in [i_ext, i_pull, i_file]:
+                if x is not None:
+                    instances += x if isinstance(x, list) else [x]
 
         # Append last so the choice instance is excluded on a name clash.
         for name, value in self.choices.items():


Test patch:
diff --git a/pyxform/tests_v1/test_xmldata.py b/pyxform/tests_v1/test_xmldata.py
index e2f3e51..584d98b 100644
--- a/pyxform/tests_v1/test_xmldata.py
+++ b/pyxform/tests_v1/test_xmldata.py
@@ -366,3 +366,92 @@ class ExternalInstanceTests(PyxformTestCase):
         survey = self.md_to_pyxform_survey(md_raw=md)
         xml = survey._to_pretty_xml()
         self.assertEqual(1, xml.count(expected))
+
+    def test_external_instance_pulldata_constraint(self):
+        """
+        Checks if instance node for pulldata function is added
+        when pulldata occurs in column with constraint title
+        """
+        md = """
+        | survey |        |         |                |                                                         |
+        |        | type   | name    | label          | constraint                                              |
+        |        | text   | Part_ID | Participant ID | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) |
+        """
+        node = """<instance id="ID" src="jr://file-csv/ID.csv">"""
+        self.assertPyxformXform(md=md, xml__contains=[node])
+
+    def test_external_instance_pulldata_readonly(self):
+        """
+        Checks if instance node for pulldata function is added
+        when pulldata occurs in column with readonly title
+        """
+        md = """
+        | survey |        |         |                |                                                         |
+        |        | type   | name    | label          | readonly                                                |
+        |        | text   | Part_ID | Participant ID | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) |
+        """
+        node = """<instance id="ID" src="jr://file-csv/ID.csv">"""
+
+        self.assertPyxformXform(md=md, xml__contains=[node])
+
+    def test_external_instance_pulldata_required(self):
+        """
+        Checks if instance node for pulldata function is added
+        when pulldata occurs in column with required title
+        """
+        md = """
+        | survey |        |         |                |                                                         |
+        |        | type   | name    | label          | required                                                |
+        |        | text   | Part_ID | Participant ID | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) |
+        """
+        node = """<instance id="ID" src="jr://file-csv/ID.csv">"""
+        self.assertPyxformXform(md=md, xml__contains=[node], debug=True)
+
+    def test_external_instance_pulldata_relevant(self):
+        """
+        Checks if instance node for pulldata function is added
+        when pulldata occurs in column with relevant title
+        """
+        md = """
+        | survey |        |         |                |                                                         |
+        |        | type   | name    | label          | relevant                                                |
+        |        | text   | Part_ID | Participant ID | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) |
+        """
+        node = """<instance id="ID" src="jr://file-csv/ID.csv">"""
+        self.assertPyxformXform(md=md, xml__contains=[node], debug=True)
+
+    def test_external_instance_pulldata(self):
+        """
+        Checks that only one instance node for pulldata is created
+        if pulldata function is present in at least one columns with
+        the titles: constraint, relevant, required, readonly
+        """
+        md = """
+        | survey |        |         |                |                                                         |                                                         |                                                         |
+        |        | type   | name    | label          | relevant                                                | required                                                | constraint                                              |
+        |        | text   | Part_ID | Participant ID | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) | pulldata('ID', 'ParticipantID', 'ParticipantIDValue',.) |
+        """
+        node = """<instance id="ID" src="jr://file-csv/ID.csv">"""
+        survey = self.md_to_pyxform_survey(md_raw=md)
+        xml = survey._to_pretty_xml()
+        self.assertEqual(1, xml.count(node))
+
+    def test_external_instances_multiple_diff_pulldatas(self):
+        """
+        Checks that all instances for pulldata that needs creation
+        are created
+        The situation is if pulldata is present in 2 or more
+        columns but pulling data from different csv files
+        """
+        md = """
+        | survey |        |         |                |                                                        |                                                             |
+        |        | type   | name    | label          | relevant                                               | required                                                    |
+        |        | text   | Part_ID | Participant ID | pulldata('fruits', 'name', 'name_key', 'mango')        | pulldata('OtherID', 'ParticipantID', ParticipantIDValue',.) |
+        """
+        node1 = '<instance id="fruits" src="jr://file-csv/fruits.csv">'
+        node2 = '<instance id="OtherID" src="jr://file-csv/OtherID.csv">'
+
+        survey = self.md_to_pyxform_survey(md_raw=md)
+        xml = survey._to_pretty_xml()
+        self.assertEqual(1, xml.count(node1))
+        self.assertEqual(1, xml.count(node2))