From d27e09238f5386692379344965657735944afcb9 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Fri, 8 Mar 2024 10:11:03 +0100
Subject: [PATCH] WIP: Filling XLSX: Seems to be working.

We should still have a few more tests.
---
 .../table_json_conversion/fill_xlsx.py        |  73 +++++++++++++++---
 .../data/multiple_refs_data.json              |  48 ++++++++++++
 .../data/multiple_refs_data.xlsx              | Bin 0 -> 12870 bytes
 .../table_json_conversion/test_fill_xlsx.py   |   8 +-
 unittests/table_json_conversion/utils.py      |   4 +-
 5 files changed, 117 insertions(+), 16 deletions(-)
 create mode 100644 unittests/table_json_conversion/data/multiple_refs_data.json
 create mode 100644 unittests/table_json_conversion/data/multiple_refs_data.xlsx

diff --git a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py
index ec381ec7..5a620d67 100644
--- a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py
+++ b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py
@@ -20,6 +20,8 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program. If not, see <https://www.gnu.org/licenses/>.
 
+from __future__ import annotations
+
 import json
 from collections import OrderedDict
 from types import SimpleNamespace
@@ -126,7 +128,6 @@ class TemplateFiller:
     def __init__(self, workbook: Workbook):
         self._workbook = workbook
         self._create_index()
-        self._context: Optional[dict[str, Any]] = None
 
     @property
     def workbook(self):
@@ -134,9 +135,49 @@ class TemplateFiller:
 
     def fill_data(self, data: dict):
         """Fill the data into the workbook."""
-        self._context = data
         self._handle_data(data=data)
-        self._context = None
+
+    class Context:
+        """Context for an entry: simple properties of all ancestors, organized in a dict.
+
+        This is similar to a dictionary with all scalar element properties at the tree nodes up to
+        the root.  Siblings in lists and dicts are ignored.  Additionally the context knows where
+        its current position is.
+
+        Lookup of elements can easily be achieved by giving the path (as ``list[str]`` or
+        stringified path).
+
+        """
+
+        def __init__(self, current_path: List[str] = None, props: Dict[str, Any] = None):
+            self._current_path = current_path if current_path is not None else []
+            self._props = props if props is not None else {}  # this is flat
+
+        def copy(self) -> TemplateFiller.Context:
+            """Deep copy."""
+            result = TemplateFiller.Context(current_path=self._current_path.copy(),
+                                            props=self._props.copy())
+            return result
+
+        def next_level(self, next_level: str) -> TemplateFiller.Context:
+            result = self.copy()
+            result._current_path.append(next_level)
+            return result
+
+        def __getitem__(self, path: Union[List[str], str], owner=None) -> Any:
+            if isinstance(path, list):
+                path = p2s(path)
+            return self._props[path]
+
+        def __setitem__(self, propname: str, value):
+            fullpath = p2s(self._current_path + [propname])
+            self._props[fullpath] = value
+
+        def fill_from_data(self, data: Dict[str, Any]):
+            """Fill current level with all scalar elements of ``data``."""
+            for name, value in data.items():
+                if not isinstance(value, (dict, list)):
+                    self[name] = value
 
     def _create_index(self):
         """Create a sheet index for the workbook.
@@ -175,6 +216,7 @@ class TemplateFiller:
                     col_type=col[coltype_idx].value)
 
     def _handle_data(self, data: dict, current_path: List[str] = None,
+                     context: TemplateFiller.Context = None,
                      only_collect_insertables: bool = False,
                      ) -> Optional[Dict[str, Any]]:
         """Handle the data and write it into ``workbook``.
@@ -186,7 +228,13 @@ data: dict
 
 current_path: list[str], optional
   If this is None or empty, we are at the top level.  This means that all children shall be entered
-  into their respective sheets and not into a sheet at this level.
+  into their respective sheets and not into a sheet at this level.  ``current_path`` and ``context``
+  must either both be given, or none of them.
+
+context: TemplateFiller.Context, optional
+  Directopry of scalar element properties at the tree nodes up to the root.  Siblings in lists
+  and dicts are ignored.  ``context`` and ``current_path`` must either both be given, or none of
+  them.
 
 only_collect_insertables: bool, optional
   If True, do not insert anything on this level, but return a dict with entries to be inserted.
@@ -194,16 +242,21 @@ only_collect_insertables: bool, optional
 
 Returns
 -------
-
 out: union[dict, None]
   If ``only_collect_insertables`` is True, return a dict (path string -> value)
         """
+        assert (current_path is None) is (context is None), (
+            "`current_path` and `context` must either both be given, or none of them.")
         if current_path is None:
             current_path = []
-        assert self._context is not None
+        if context is None:
+            context = TemplateFiller.Context()
+        context.fill_from_data(data)
+
         insertables: Dict[str, Any] = {}
         for name, content in data.items():
             path = current_path + [name]
+            next_context = context.next_level(name)
             # preprocessing
             if isinstance(content, list):
                 if not content:
@@ -213,13 +266,13 @@ out: union[dict, None]
                 if isinstance(content[0], dict):
                     # An array of objects: must go into exploded sheet
                     for entry in content:
-                        self._handle_data(data=entry, current_path=path)
+                        self._handle_data(data=entry, current_path=path, context=next_context)
                     continue
             elif isinstance(content, dict):
                 if not current_path:  # Special handling for top level
-                    self._handle_data(content, current_path=path)
+                    self._handle_data(content, current_path=path, context=next_context)
                     continue
-                insert = self._handle_data(content, current_path=path,
+                insert = self._handle_data(content, current_path=path, context=next_context.copy(),
                                            only_collect_insertables=True)
                 assert isinstance(insert, dict)
                 assert not any(key in insertables for key in insert)
@@ -260,7 +313,7 @@ out: union[dict, None]
         if insert_row is not None and sheet is not None and _is_exploded_sheet(sheet):
             foreigns = _get_foreign_key_columns(sheet)
             for index, path in ((f.index, f.path) for f in foreigns.values()):
-                value = _get_deep_value(self._context, path)
+                value = context[path]
                 sheet.cell(row=insert_row+1, column=index+1, value=value)
 
         return None
diff --git a/unittests/table_json_conversion/data/multiple_refs_data.json b/unittests/table_json_conversion/data/multiple_refs_data.json
new file mode 100644
index 00000000..5b8ce913
--- /dev/null
+++ b/unittests/table_json_conversion/data/multiple_refs_data.json
@@ -0,0 +1,48 @@
+{
+  "Training": {
+    "trainer": [],
+    "participant": [
+      {
+        "full_name": "Petra Participant",
+        "email": "petra@indiscale.com"
+      },
+      {
+        "full_name": "Peter",
+        "email": "peter@getlinkahead.com"
+      }
+    ],
+    "Organisation": [
+      {
+        "Person": [
+          {
+            "full_name": "Henry Henderson",
+            "email": "henry@organization.org"
+          },
+          {
+            "full_name": "Harry Hamburg",
+            "email": "harry@organization.org"
+          }
+        ],
+        "name": "World Training Organization",
+        "Country": "US"
+      },
+      {
+        "Person": [
+          {
+            "full_name": "Hermione Harvard",
+            "email": "hermione@organisation.org.uk"
+          },
+          {
+            "full_name": "Hazel Harper",
+            "email": "hazel@organisation.org.uk"
+          }
+        ],
+        "name": "European Training Organisation",
+        "Country": "UK"
+      }
+    ],
+    "date": "2024-03-21T14:12:00.000Z",
+    "url": "www.indiscale.com",
+    "name": "Example training with multiple organizations."
+  }
+}
diff --git a/unittests/table_json_conversion/data/multiple_refs_data.xlsx b/unittests/table_json_conversion/data/multiple_refs_data.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..21622ede9515b0cfa9f965f8c2ee89f782c4bf0c
GIT binary patch
literal 12870
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
b4+8RUcve9M66)<J$gkVlYfn;SeEar4J;J{d

literal 0
HcmV?d00001

diff --git a/unittests/table_json_conversion/test_fill_xlsx.py b/unittests/table_json_conversion/test_fill_xlsx.py
index 5b45939a..18ab4f06 100644
--- a/unittests/table_json_conversion/test_fill_xlsx.py
+++ b/unittests/table_json_conversion/test_fill_xlsx.py
@@ -56,9 +56,6 @@ custom_output: str, optional
         assert os.path.exists(outfile)
         generated = load_workbook(outfile)  # workbook can be read
     known_good_wb = load_workbook(known_good)
-    # if custom_output is not None:
-    #     from IPython import embed
-    #     embed()
     compare_workbooks(generated, known_good_wb)
 
 
@@ -71,5 +68,6 @@ def test_detect():
 def test_fill_xlsx():
     fill_and_compare(json_file="data/simple_data.json", template_file="data/simple_template.xlsx",
                      known_good="data/simple_data.xlsx")
-    # fill_and_compare(json_file="data/example.json", template_file="data/example_template.xlsx",
-    #                  known_good="data/example_single_data.xlsx")
+    fill_and_compare(json_file="data/multiple_refs_data.json",
+                     template_file="data/multiple_refs_template.xlsx",
+                     known_good="data/multiple_refs_data.xlsx")
diff --git a/unittests/table_json_conversion/utils.py b/unittests/table_json_conversion/utils.py
index 0311e8bb..6c32117c 100644
--- a/unittests/table_json_conversion/utils.py
+++ b/unittests/table_json_conversion/utils.py
@@ -33,7 +33,9 @@ Parameters
 hidden: bool, optional
   Test if the "hidden" status of rows and columns is the same.
     """
-    assert wb1.sheetnames == wb2.sheetnames, "Sheet names are different."
+    assert wb1.sheetnames == wb2.sheetnames, (
+        f"Sheet names are different: \n{wb1.sheetnames}\n   !=\n{wb2.sheetnames}"
+    )
     for sheetname in wb2.sheetnames:
         sheet_1 = wb1[sheetname]
         sheet_2 = wb2[sheetname]
-- 
GitLab