diff --git a/packages/dsw-templating/dsw/templating/steps/excel.py b/packages/dsw-templating/dsw/templating/steps/excel.py index c4dfd663..8aa450ea 100644 --- a/packages/dsw-templating/dsw/templating/steps/excel.py +++ b/packages/dsw-templating/dsw/templating/steps/excel.py @@ -666,30 +666,22 @@ def _setup_worksheet_panes(worksheet: Worksheet, data: dict): left_col=data['split_panes'].get('left_col', 0), ) if 'freeze_panes' in data: - worksheet.split_panes( - data['split_panes'].get('x', 0), - data['split_panes'].get('y', 0), - top_row=data['split_panes'].get('top_row', 0), - left_col=data['split_panes'].get('left_col', 0), - ) + if isinstance(data['freeze_panes'], str): + worksheet.freeze_panes(data['freeze_panes']) + else: + worksheet.freeze_panes( + data['freeze_panes'].get('row', 0), + data['freeze_panes'].get('col', 0), + top_row=data['freeze_panes'].get('top_row'), + left_col=data['freeze_panes'].get('left_col'), + ) @staticmethod def _setup_worksheet_filters(worksheet: Worksheet, data: dict): - if 'filter_column_lists' in data: - for f in data['filter_column_list']: - worksheet.filter_column_list( - f.get('col', 0), - filters=f.get('filters', _EMPTY_LIST), - ) - if 'filter_columns' in data: - for f in data['filter_column_list']: - worksheet.filter_column( - f.get('col', 0), - criteria=f.get('criteria', _EMPTY_LIST), - ) + # XlsxWriter ignores column filters until the autofilter is set if 'autofilter' in data: if isinstance(data['autofilter'], str): - worksheet.set_selection(data['autofilter']) + worksheet.autofilter(data['autofilter']) else: worksheet.autofilter( data['autofilter'].get('first_row', 0), @@ -697,6 +689,18 @@ def _setup_worksheet_filters(worksheet: Worksheet, data: dict): data['autofilter'].get('last_row', 0), data['autofilter'].get('last_col', 0), ) + if 'filter_column_lists' in data: + for f in data['filter_column_lists']: + worksheet.filter_column_list( + f.get('col', 0), + filters=f.get('filters', _EMPTY_LIST), + ) + if 'filter_columns' in data: + for f in data['filter_columns']: + worksheet.filter_column( + f.get('col', 0), + criteria=f.get('criteria', ''), + ) def _setup_worksheet_merge_ranges(self, worksheet: Worksheet, data: dict): if 'merge_ranges' in data: @@ -725,7 +729,7 @@ def _setup_worksheet_data_validations(worksheet: Worksheet, data: dict): if 'range' in item: worksheet.data_validation( item['range'], - options=item.get('options', ''), + options=item.get('options', {}), ) else: worksheet.data_validation( @@ -733,18 +737,21 @@ def _setup_worksheet_data_validations(worksheet: Worksheet, data: dict): item.get('first_col', 0), item.get('last_row', 0), item.get('last_col', 0), - options=item.get('options', ''), + options=item.get('options', {}), ) def _setup_worksheet_conditional_formats(self, worksheet: Worksheet, data: dict): if 'conditional_formats' in data: for item in data['conditional_formats']: - if 'format' in item: - item['format'] = self.formats.get(item['format']) + options = item.get('options', {}) + format_name = options.get('format', item.get('format')) + if format_name is not None: + options['format'] = self.formats.get(format_name) + item['options'] = options if 'range' in item: worksheet.conditional_format( item['range'], - options=item.get('options', ''), + options=item.get('options', {}), ) else: worksheet.conditional_format( @@ -752,7 +759,7 @@ def _setup_worksheet_conditional_formats(self, worksheet: Worksheet, data: dict) item.get('first_col', 0), item.get('last_row', 0), item.get('last_col', 0), - options=item.get('options', ''), + options=item.get('options', {}), ) def _setup_worksheet_tables(self, worksheet: Worksheet, data: dict): @@ -765,7 +772,7 @@ def _setup_worksheet_tables(self, worksheet: Worksheet, data: dict): if 'range' in item: worksheet.add_table( item['range'], - options=item.get('options', ''), + options=item.get('options', {}), ) else: worksheet.add_table( @@ -773,7 +780,7 @@ def _setup_worksheet_tables(self, worksheet: Worksheet, data: dict): item.get('first_col', 0), item.get('last_row', 0), item.get('last_col', 0), - options=item.get('options', ''), + options=item.get('options', {}), ) @staticmethod @@ -783,13 +790,13 @@ def _setup_worksheet_sparklines(worksheet: Worksheet, data: dict): if 'cell' in item: worksheet.add_sparkline( item['cell'], - options=item.get('options', ''), + options=item.get('options', {}), ) else: worksheet.add_sparkline( item.get('row', 0), item.get('col', item.get('column', 0)), - options=item.get('options', ''), + options=item.get('options', {}), ) def _setup_worksheet_row_sizing(self, worksheet: Worksheet, data: dict): @@ -844,16 +851,10 @@ def _setup_worksheet_col_sizing(self, worksheet: Worksheet, data: dict): def _setup_worksheet_background(worksheet: Worksheet, data: dict): if 'background' in data: if 'filename' in data['background']: - worksheet.set_background( - source=data['background']['filename'], - is_byte_stream=False, - ) + worksheet.set_background(data['background']['filename']) elif 'b64bytes' in data['background']: bytes_io = _b64img2io(data['background']['b64bytes']) - worksheet.set_background( - bytes_io, - is_byte_stream=True, - ) + worksheet.set_background(bytes_io) def _replace_nested_formats(self, data: dict, keys: frozenset[str]): if not isinstance(data, dict): @@ -870,7 +871,6 @@ def _replace_nested_formats(self, data: dict, keys: frozenset[str]): def build(self, data: dict): self._add_workbook_properties(data) - self._add_workbook_options(data) self._add_workbook_definitions(data) for vba_project in data.get('vba_projects', _EMPTY_LIST): @@ -891,6 +891,8 @@ def build(self, data: dict): else: self._add_worksheet(sheet_data) + self._add_workbook_options(data) + def cleanup(self): for stream in self.byte_streams: stream.close() diff --git a/packages/dsw-templating/support/steps/excel.md b/packages/dsw-templating/support/steps/excel.md index 73d94683..79ea3f6a 100644 --- a/packages/dsw-templating/support/steps/excel.md +++ b/packages/dsw-templating/support/steps/excel.md @@ -29,7 +29,13 @@ It allows to set both [basic](https://xlsxwriter.readthedocs.io/workbook.html#se }, "custom": [ { - "projectUuid": "...", + "name": "projectUuid", + "value": "..." + }, + { + "name": "exportedAt", + "value": "2018-01-01", + "type": "date" } ] } @@ -143,7 +149,7 @@ A chartsheet simply refers to a `chart` (by its `name`) that should be placed in The possible `options` are: * Basic: [`first_sheet`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-set-first-sheet), [`protect`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-protect), [`zoom`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-set-zoom), [`tab_color`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-set-tab-color), [`page_view`](https://xlsxwriter.readthedocs.io/page_setup.html#set-page-view), [`select`](https://xlsxwriter.readthedocs.io/worksheet.html#select), [`hide`](https://xlsxwriter.readthedocs.io/worksheet.html#hide) -* Print: `orientation` ([`landspace`](https://xlsxwriter.readthedocs.io/page_setup.html#set-landscape) or [`portrait`](https://xlsxwriter.readthedocs.io/page_setup.html#set-portrait)), [`paper`](https://xlsxwriter.readthedocs.io/page-setup.html#set-paper), [`margins`](https://xlsxwriter.readthedocs.io/page_setup.html#set-margins), [`header`](https://xlsxwriter.readthedocs.io/page_setup.html#set-header), [`footer`](https://xlsxwriter.readthedocs.io/page_setup.html#set-footer), [`center_horizontally`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-horizontally), [`center_vertically`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-vertically) +* Print: `orientation` ([`landscape`](https://xlsxwriter.readthedocs.io/page_setup.html#set-landscape) or [`portrait`](https://xlsxwriter.readthedocs.io/page_setup.html#set-portrait)), [`paper`](https://xlsxwriter.readthedocs.io/page-setup.html#set-paper), [`margins`](https://xlsxwriter.readthedocs.io/page_setup.html#set-margins), [`header`](https://xlsxwriter.readthedocs.io/page_setup.html#set-header), [`footer`](https://xlsxwriter.readthedocs.io/page_setup.html#set-footer), [`center_horizontally`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-horizontally), [`center_vertically`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-vertically) ```json { @@ -163,17 +169,17 @@ Traditional worksheet with many options and data placed into cells. There are mo The possible `options` are: * Basic (common): [`first_sheet`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-set-first-sheet), [`protect`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-protect), [`zoom`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-set-zoom), [`tab_color`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-set-tab-color), [`page_view`](https://xlsxwriter.readthedocs.io/page_setup.html#set-page-view), [`select`](https://xlsxwriter.readthedocs.io/worksheet.html#select), [`hide`](https://xlsxwriter.readthedocs.io/worksheet.html#hide) -* Print (common): `orientation` ([`landspace`](https://xlsxwriter.readthedocs.io/page_setup.html#set-landscape) or [`portrait`](https://xlsxwriter.readthedocs.io/page_setup.html#set-portrait)), [`paper`](https://xlsxwriter.readthedocs.io/page-setup.html#set-paper), [`margins`](https://xlsxwriter.readthedocs.io/page_setup.html#set-margins), [`header`](https://xlsxwriter.readthedocs.io/page_setup.html#set-header), [`footer`](https://xlsxwriter.readthedocs.io/page_setup.html#set-footer), [`center_horizontally`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-horizontally), [`center_vertically`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-vertically) +* Print (common): `orientation` ([`landscape`](https://xlsxwriter.readthedocs.io/page_setup.html#set-landscape) or [`portrait`](https://xlsxwriter.readthedocs.io/page_setup.html#set-portrait)), [`paper`](https://xlsxwriter.readthedocs.io/page-setup.html#set-paper), [`margins`](https://xlsxwriter.readthedocs.io/page_setup.html#set-margins), [`header`](https://xlsxwriter.readthedocs.io/page_setup.html#set-header), [`footer`](https://xlsxwriter.readthedocs.io/page_setup.html#set-footer), [`center_horizontally`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-horizontally), [`center_vertically`](https://xlsxwriter.readthedocs.io/page_setup.html#worksheet-center-vertically) * Basic: [`comments_author`](https://xlsxwriter.readthedocs.io/worksheet.html#set_comments_author), [`hide_zero`](https://xlsxwriter.readthedocs.io/worksheet.html#hide_zero), [`hide_row_col_headers`](https://xlsxwriter.readthedocs.io/page_setup.html#hide_row_col_headers), [`right_to_left`](https://xlsxwriter.readthedocs.io/worksheet.html#right_to_left), [`hide_gridlines`](), [`ignore_errors`](https://xlsxwriter.readthedocs.io/worksheet.html#ignore_errors), [`vba_name`](https://xlsxwriter.readthedocs.io/workbook.html#set_vba_name) * Print (advanced): [`print_row_col_headers`](https://xlsxwriter.readthedocs.io/page_setup.html#print_row_col_headers), [`print_area`](), [`print_across`](https://xlsxwriter.readthedocs.io/page_setup.html#print_across), [`fit_to_pages`](https://xlsxwriter.readthedocs.io/page_setup.html#fit_to_pages), [`start_page`](https://xlsxwriter.readthedocs.io/page_setup.html#set_start_page), [`print_scale`](https://xlsxwriter.readthedocs.io/page_setup.html#set_print_scale), [`print_black_and_white`](https://xlsxwriter.readthedocs.io/page_setup.html#print_black_and_white) * Special ranges: [`unprotect_ranges`](https://xlsxwriter.readthedocs.io/worksheet.html#unprotect_range), [`top_left_cell`](https://xlsxwriter.readthedocs.io/worksheet.html#set_top_left_cell), [`selection`](https://xlsxwriter.readthedocs.io/worksheet.html#set_selection) * Repeats: [`repeat_rows`](https://xlsxwriter.readthedocs.io/page_setup.html#repeat_rows), [`repeat_columns`](https://xlsxwriter.readthedocs.io/page_setup.html#repeat_columns), [`default_row`](https://xlsxwriter.readthedocs.io/worksheet.html#set_default_row) * Paging: [`h_pagebreaks`](https://xlsxwriter.readthedocs.io/page_setup.html#set_h_pagebreaks), [`v_pagebreaks`](https://xlsxwriter.readthedocs.io/page_setup.html#set_v_pagebreaks), [`outline_settings`](https://xlsxwriter.readthedocs.io/worksheet.html#outline_settings) -* Panes: [`split_panes`](https://xlsxwriter.readthedocs.io/worksheet.html#split_panes), [`freeze_panes`](https://xlsxwriter.readthedocs.io/worksheet.html#freeze_panes) +* Panes: [`split_panes`](https://xlsxwriter.readthedocs.io/worksheet.html#split_panes) with `x`, `y`, `top_row`, `left_col` attributes, [`freeze_panes`](https://xlsxwriter.readthedocs.io/worksheet.html#freeze_panes) with a cell directly or `row`, `col`, `top_row`, `left_col` attributes * Filters: [`filter_column_lists`](https://xlsxwriter.readthedocs.io/worksheet.html#filter_column_list) each with `col` and `filters`, [`filter_columns`](https://xlsxwriter.readthedocs.io/worksheet.html#filter_column) each with `col` and `criteria`, [`autofilter`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-autofilter) with range directly or `first_row`, `first_col`, `last_row`, `last_col` attributes (see [docs](https://xlsxwriter.readthedocs.io/working_with_autofilters.html)) * Merge ranges: [`merge_ranges`](https://xlsxwriter.readthedocs.io/worksheet.html#merge_range) list can be used to merge cells (`range` or combination of `first_row`, `first_col`, `last_row`, `last_col` attributes) together and apply a format (via `format` reference attribute), also contents can be set via `data` attribute * Data validations: [`data_validations`](https://xlsxwriter.readthedocs.io/worksheet.html#data_validation) list can be used to validate data in cell ranges (see [docs](https://xlsxwriter.readthedocs.io/working_with_data_validation.html)) -* Conditional formats: [`conditional_formats`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-conditional-format) list can be used to define conditional formats on cell ranges (see [docs](https://xlsxwriter.readthedocs.io/example_conditional_format.html#ex-cond-format)) +* Conditional formats: [`conditional_formats`](https://xlsxwriter.readthedocs.io/worksheet.html#worksheet-conditional-format) list can be used to define conditional formats on cell ranges, with `format` in `options` referring to a defined format by its name (see [docs](https://xlsxwriter.readthedocs.io/example_conditional_format.html#ex-cond-format)) * Tables: [`tables`](https://xlsxwriter.readthedocs.io/worksheet.html#add_table) list can be used to define formatted tables (see [docs](https://xlsxwriter.readthedocs.io/working_with_tables.html)) * Sparklines: [`sparklines`](https://xlsxwriter.readthedocs.io/worksheet.html#add_sparkline) list (see [docs](https://xlsxwriter.readthedocs.io/working_with_sparklines.html)) * Row/col sizing: [`columns`](https://xlsxwriter.readthedocs.io/worksheet.html?highlight=column_pixels#set_column), [`column_pixels`](https://xlsxwriter.readthedocs.io/worksheet.html#set_column_pixels), [`rows`](https://xlsxwriter.readthedocs.io/worksheet.html?highlight=column_pixels#set_row), and [`row_pixels`](https://xlsxwriter.readthedocs.io/worksheet.html#set_row_pixels) lists can be used to adjust cell sizing (unfortunately automatic sizing is not possible) diff --git a/packages/dsw-templating/tests/test_steps_excel.py b/packages/dsw-templating/tests/test_steps_excel.py index bcf08b20..e74f7e70 100644 --- a/packages/dsw-templating/tests/test_steps_excel.py +++ b/packages/dsw-templating/tests/test_steps_excel.py @@ -1,4 +1,4 @@ -# cspell:ignore sqref +# cspell:ignore sqref dxf import io import pathlib import zipfile @@ -71,3 +71,54 @@ def test_sparkline(tmp_path, key): {'row': 0, key: 3, 'options': {'range': 'Sheet1!A1:C1'}}, ]}}) assert 'D1' in sheet_xml(workbook) + + +def test_active_sheet(tmp_path): + content = WorkbookBuilder.build_to_bytes( + tmp_file=tmp_path / 'result.xlsx', + input_data={'sheets': [{'name': 'A'}, {'name': 'B'}], 'options': {'active_sheet': 1}}, + ) + workbook = zipfile.ZipFile(io.BytesIO(content)) + assert 'activeTab="1"' in workbook.read('xl/workbook.xml').decode('utf-8') + + +@pytest.mark.parametrize('freeze', ['B3', {'row': 2, 'col': 1}]) +def test_freeze_panes(tmp_path, freeze): + workbook = build(tmp_path, {'options': {'freeze_panes': freeze}}) + assert 'topLeftCell="B3" activePane="bottomRight" state="frozen"' in sheet_xml(workbook) + + +@pytest.mark.parametrize('autofilter', ['A1:C6', {'last_row': 5, 'last_col': 2}]) +def test_filters(tmp_path, autofilter): + workbook = build(tmp_path, {'options': { + 'autofilter': autofilter, + 'filter_column_lists': [{'col': 0, 'filters': ['x']}], + 'filter_columns': [{'col': 1, 'criteria': 'x > 5'}], + }}) + xml = sheet_xml(workbook) + assert '' in xml + assert '' in xml + assert '' in xml + + +@pytest.mark.parametrize('item', [ + {'range': 'A1:A2', 'options': {'type': 'no_blanks', 'format': 'bold'}}, + {'range': 'A1:A2', 'format': 'bold', 'options': {'type': 'no_blanks'}}, +]) +def test_conditional_format(tmp_path, item): + workbook = build( + tmp_path, + {'options': {'conditional_formats': [item]}}, + formats={'bold': {'bold': True}}, + ) + assert '