diff --git a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py index b0f8d2dfe33f420df5e22d45c48e4aa6fea58d21..8c9bba804b292f1cb986f60921145303ebb23bed 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/unittests/table_json_conversion/example_single.json b/unittests/table_json_conversion/example_single.json new file mode 100644 index 0000000000000000000000000000000000000000..9997f17e76a46d5e97d842fdee40626047e7a347 --- /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 Binary files /dev/null and b/unittests/table_json_conversion/example_single_data.xlsx differ diff --git a/unittests/table_json_conversion/test_fill_xlsx.py b/unittests/table_json_conversion/test_fill_xlsx.py index 3fa03aa6d67828771369e3d20342541164055733..35f75703aa73eb2e6c2dc037f83cd7e174a9a244 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 670f7df1dc59c4928ec64d4bf3bdd7b7d5f45cd0..5d05e2135eae2125c51bafb17b6d99e40f46e4c3 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 0000000000000000000000000000000000000000..0311e8bb4eaf4e2cf6c6f7686b4b9144112111e6 --- /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}" + )