From 0dadcca65990c24b8e7d2b311aef361395d12544 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Thu, 7 Mar 2024 15:33:12 +0100
Subject: [PATCH] WIP: Filling XLSX

---
 .../table_json_conversion/fill_xlsx.py        |  32 ++++-------
 unittests/table_json_conversion/__init__.py   |   0
 .../table_json_conversion/example_single.json |  32 +++++++++++
 .../example_single_data.xlsx                  | Bin 0 -> 8946 bytes
 .../table_json_conversion/test_fill_xlsx.py   |  34 ++++++++++--
 .../test_table_template_generator.py          |  37 +++++--------
 unittests/table_json_conversion/utils.py      |  52 ++++++++++++++++++
 7 files changed, 137 insertions(+), 50 deletions(-)
 create mode 100644 unittests/table_json_conversion/__init__.py
 create mode 100644 unittests/table_json_conversion/example_single.json
 create mode 100644 unittests/table_json_conversion/example_single_data.xlsx
 create mode 100644 unittests/table_json_conversion/utils.py

diff --git a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py
index b0f8d2df..8c9bba80 100644
--- a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py
+++ b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py
@@ -121,10 +121,16 @@ def _next_row_index(sheet: Worksheet) -> int:
 
 
 class TemplateFiller:
+    """Class to fill XLSX templates.  Has an index for all relevant columns."""
+
     def __init__(self, workbook: Workbook):
         self._workbook = workbook
         self._create_index()
-        self._context: Optional[dict] = None
+        self._context: Optional[dict[str, Any]] = None
+
+    @property
+    def workbook(self):
+        return self._workbook
 
     def fill_data(self, data: dict):
         """Fill the data into the workbook."""
@@ -194,6 +200,7 @@ out: union[dict, None]
         """
         if current_path is None:
             current_path = []
+        assert self._context is not None
         insertables: Dict[str, Any] = {}
         for name, content in data.items():
             path = current_path + [name]
@@ -248,7 +255,6 @@ out: union[dict, None]
                 insert_row = _next_row_index(sheet)
 
             sheet.cell(row=insert_row+1, column=col_index+1, value=value)
-            # self._handle_simple_data(data=content, current_path=path)
 
         # Insert foreign keys
         if insert_row is not None and sheet is not None and _is_exploded_sheet(sheet):
@@ -284,27 +290,9 @@ result: str
         data = json.load(data)
     else:
         raise ValueError(f"I don't know how to handle the datatype of `data`: {type(data)}")
+    assert isinstance(data, dict)
+
     result_wb = load_workbook(template)
     template_filler = TemplateFiller(result_wb)
-    # For each top level key in the json we iterate the values (if it is an array). Those are the
-    # root elements that belong to a particular sheet.
-    #       After treating a root element, the row index for the corresponding sheet needs to be
-    #       increased
-    #       When we finished treating an object that goes into a lower ranked sheet (see below), we
-    #       increase the row index of that sheet.
-    #
-    # We can generate a hierarchy of sheets in the beginning (using the paths). The lower sheets
-    # are for objects referenced by objects in higher ranked sheets.
-    # We can detect the sheet corresponding to a root element by looking at the first path element:
-    # The first path element must be the root element every where.
-    # Suggestion:
-    # row indices: Dict[str, int] string is the sheet name
-    # sheet_hirarchy: List[Tuple[str]]  elements are sheet names
     template_filler.fill_data(data=data)
-
-    # Question:
-    # We can create an internal representation where we assign as sheet_names the same names that
-    # are used in table generator. Or should we create another special row that contains this
-    # somehow?
-
     result_wb.save(result)
diff --git a/unittests/table_json_conversion/__init__.py b/unittests/table_json_conversion/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/unittests/table_json_conversion/example_single.json b/unittests/table_json_conversion/example_single.json
new file mode 100644
index 00000000..9997f17e
--- /dev/null
+++ b/unittests/table_json_conversion/example_single.json
@@ -0,0 +1,32 @@
+{
+  "Training": {
+    "date": "2023-01-01",
+    "url": "www.indiscale.com",
+    "coach": [
+      {
+        "family_name": "Sky",
+        "given_name": "Max",
+        "Organisation": "ECB"
+      },
+      {
+        "family_name": "Sky",
+        "given_name": "Min",
+        "Organisation": "ECB"
+      }
+    ],
+    "supervisor": {
+      "family_name": "Steve",
+      "given_name": "Stevie",
+            "Organisation": "IMF"
+    },
+    "duration": 1.0,
+    "participants": 1,
+    "subjects": ["Math", "Physics"],
+    "remote": false
+  },
+  "Person": {
+    "family_name": "Steve",
+    "given_name": "Stevie",
+    "Organisation": "IMF"
+  }
+}
diff --git a/unittests/table_json_conversion/example_single_data.xlsx b/unittests/table_json_conversion/example_single_data.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..662a636603944a91cbaeaea8cc0c3bbab12f2f50
GIT binary patch
literal 8946
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
Aq5uE@

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 3fa03aa6..35f75703 100644
--- a/unittests/table_json_conversion/test_fill_xlsx.py
+++ b/unittests/table_json_conversion/test_fill_xlsx.py
@@ -26,6 +26,8 @@ from caosadvancedtools.table_json_conversion.fill_xlsx import (
     _get_path_rows, _get_row_type_column_index, fill_template)
 from openpyxl import load_workbook
 
+from .utils import compare_workbooks
+
 
 def rfp(*pathcomponents):
     """
@@ -35,6 +37,31 @@ def rfp(*pathcomponents):
     return os.path.join(os.path.dirname(__file__), *pathcomponents)
 
 
+def fill_and_compare(json_file: str, template_file: str, known_good: str,
+                     custom_output: str = None):
+    """Fill the data into a template and compare to a known good.
+
+Parameters:
+-----------
+
+custom_output: str, optional
+  If given, write to this file and drop into an IPython shell.  For development only.
+    """
+    with tempfile.TemporaryDirectory() as tmpdir:
+        outfile = os.path.join(tmpdir, 'test.xlsx')
+        assert not os.path.exists(outfile)
+        if custom_output is not None:
+            outfile = custom_output
+        fill_template(data=json_file, template=template_file, result=outfile)
+        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)
+
+
 def test_detect():
     example = load_workbook(rfp("example_template.xlsx"))
     assert 0 == _get_row_type_column_index(example['Person'])
@@ -42,8 +69,5 @@ def test_detect():
 
 
 def test_fill_xlsx():
-    path = os.path.join(tempfile.mkdtemp(), 'test.xlsx')
-    assert not os.path.exists(path)
-    fill_template(data=rfp('example.json'), template=rfp('example_template.xlsx'), result=path)
-    assert os.path.exists(path)
-    generated = load_workbook(path)  # workbook can be read
+    fill_and_compare(json_file="example_single.json", template_file="example_template.xlsx",
+                     known_good="example_single_data.xlsx")
diff --git a/unittests/table_json_conversion/test_table_template_generator.py b/unittests/table_json_conversion/test_table_template_generator.py
index 670f7df1..5d05e213 100644
--- a/unittests/table_json_conversion/test_table_template_generator.py
+++ b/unittests/table_json_conversion/test_table_template_generator.py
@@ -29,6 +29,8 @@ from caosadvancedtools.table_json_conversion.table_generator import (
     ColumnType, XLSXTemplateGenerator)
 from openpyxl import load_workbook
 
+from .utils import compare_workbooks
+
 
 def rfp(*pathcomponents):
     """
@@ -52,30 +54,19 @@ out: tuple
         foreign_keys = {}
     with open(schema_file, encoding="utf-8") as schema_input:
         schema = json.load(schema_input)
-    if outfile is None:
-        outpath = os.path.join(tempfile.mkdtemp(), 'generated.xlsx')
-    else:
-        outpath = outfile
-    assert not os.path.exists(outpath)
-    generator.generate(schema=schema,
-                       foreign_keys=foreign_keys,
-                       filepath=outpath)
-    assert os.path.exists(outpath)
-    generated = load_workbook(outpath)
+    with tempfile.TemporaryDirectory() as tmpdir:
+        if outfile is None:
+            outpath = os.path.join(tmpdir, 'generated.xlsx')
+        else:
+            outpath = outfile
+        assert not os.path.exists(outpath)
+        generator.generate(schema=schema,
+                           foreign_keys=foreign_keys,
+                           filepath=outpath)
+        assert os.path.exists(outpath)
+        generated = load_workbook(outpath)
     good = load_workbook(known_good)
-    assert generated.sheetnames == good.sheetnames
-    for sheetname in good.sheetnames:
-        gen_sheet = generated[sheetname]
-        good_sheet = good[sheetname]
-        for irow, (erow, grow) in enumerate(zip(good_sheet.iter_rows(), gen_sheet.iter_rows())):
-            assert (good_sheet.row_dimensions[irow].hidden
-                    == gen_sheet.row_dimensions[irow].hidden), f"row: {sheetname}, {irow}"
-            for icol, (ecol, gcol) in enumerate(zip(erow, grow)):
-                assert (good_sheet.column_dimensions[ecol.column_letter].hidden
-                        == gen_sheet.column_dimensions[ecol.column_letter].hidden), (
-                            f"col: {sheetname}, {icol}")
-                cell = gen_sheet.cell(irow+1, icol+1)
-                assert ecol.value == gcol.value, f"Sheet: {sheetname}, cell: {cell.coordinate}"
+    compare_workbooks(generated, good)
     return generated, good
 
 
diff --git a/unittests/table_json_conversion/utils.py b/unittests/table_json_conversion/utils.py
new file mode 100644
index 00000000..0311e8bb
--- /dev/null
+++ b/unittests/table_json_conversion/utils.py
@@ -0,0 +1,52 @@
+# This file is a part of the LinkAhead Project.
+#
+# Copyright (C) 2024 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2024 Daniel Hornung <d.hornung@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# 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/>.
+
+"""Utilities for the tests.
+"""
+
+from openpyxl import Workbook
+
+
+def compare_workbooks(wb1: Workbook, wb2: Workbook, hidden: bool = True):
+    """Compare two workbooks for equal content.
+
+Raises an error if differences are found.
+
+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."
+    for sheetname in wb2.sheetnames:
+        sheet_1 = wb1[sheetname]
+        sheet_2 = wb2[sheetname]
+        for irow, (row1, row2) in enumerate(zip(sheet_1.iter_rows(), sheet_2.iter_rows())):
+            if hidden:
+                assert (sheet_1.row_dimensions[irow].hidden
+                        == sheet_2.row_dimensions[irow].hidden), f"hidden row: {sheetname}, {irow}"
+            for icol, (cell1, cell2) in enumerate(zip(row1, row2)):
+                if hidden:
+                    assert (sheet_1.column_dimensions[cell1.column_letter].hidden
+                            == sheet_2.column_dimensions[cell2.column_letter].hidden), (
+                                f"hidden col: {sheetname}, {icol}")
+                assert cell1.value == cell2.value, (
+                    f"Sheet: {sheetname}, cell: {cell1.coordinate}, Values: \n"
+                    f"{cell1.value}\n{cell2.value}"
+                )
-- 
GitLab