From f7eeda08c3c00ce350127eee060f436bcf776e72 Mon Sep 17 00:00:00 2001 From: ignisor Date: Wed, 27 Jun 2018 15:15:52 +0300 Subject: [PATCH] tbl: #86 add/remove row and column support --- .gitignore | 1 + docs/api/table.rst | 17 ++++++++++ features/steps/table.py | 47 ++++++++++++++++++++++++++ features/tbl-table-props.feature | 28 +++++++++++++++ pptx/shapes/table.py | 54 +++++++++++++++++++++++++++++ tests/shapes/test_table.py | 58 ++++++++++++++++++++++++++++++++ 6 files changed, 205 insertions(+) diff --git a/.gitignore b/.gitignore index 49f59feb9..793d30aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ _scratch/ /spec/gen_spec/spec*.db tags /tests/debug.py +.idea diff --git a/docs/api/table.rst b/docs/api/table.rst index f6cad330e..fce2d3460 100644 --- a/docs/api/table.rst +++ b/docs/api/table.rst @@ -21,6 +21,14 @@ A |Table| object is added to a slide using the :undoc-members: +|_ColumnCollection| objects +--------------------------- +.. autoclass:: _ColumnCollection() + :members: + :member-order: bysource + :undoc-members: + + |_Column| objects ----------------- @@ -30,6 +38,15 @@ A |Table| object is added to a slide using the :undoc-members: +|_RowCollection| objects +--------------------------- +.. autoclass:: _RowCollection() + :members: + :member-order: bysource + :undoc-members: + + + |_Row| objects -------------- diff --git a/features/steps/table.py b/features/steps/table.py index a6d8934d9..f0bf4e347 100644 --- a/features/steps/table.py +++ b/features/steps/table.py @@ -108,6 +108,25 @@ def when_set_table_column_widths(context): context.table_.columns[1].width = Inches(3.00) +@when("I add a row to a table") +def when_add_row_to_table(context): + context.table_.rows.add_row() + + +@when("I remove a row from a table") +def when_add_row_to_table(context): + context.table_.rows.remove(context.table_.rows[1]) + + +@when("I add a column to a table") +def when_add_row_to_table(context): + context.table_.columns.add_column() + + +@when("I remove a column from a table") +def when_add_row_to_table(context): + context.table_.columns.remove(context.table_.columns[1]) + # then ==================================================== @@ -192,3 +211,31 @@ def then_text_appears_in_first_cell_of_table(context): table = prs.slides[0].shapes[3].table text = table.cell(0, 0).text_frame.paragraphs[0].runs[0].text assert text == 'test text' + + +@then('the table now has 3 rows') +def table_has_three_rows(context): + prs = Presentation(saved_pptx_path) + table = prs.slides[0].shapes[3].table + assert len(table.rows) == 3 + + +@then('the table now has 1 row') +def table_has_three_rows(context): + prs = Presentation(saved_pptx_path) + table = prs.slides[0].shapes[3].table + assert len(table.rows) == 1 + + +@then('the table now has 3 columns') +def table_has_three_rows(context): + prs = Presentation(saved_pptx_path) + table = prs.slides[0].shapes[3].table + assert len(table.columns) == 3 + + +@then('the table now has 1 column') +def table_has_three_rows(context): + prs = Presentation(saved_pptx_path) + table = prs.slides[0].shapes[3].table + assert len(table.columns) == 1 diff --git a/features/tbl-table-props.feature b/features/tbl-table-props.feature index fb598e70b..951e28b0e 100644 --- a/features/tbl-table-props.feature +++ b/features/tbl-table-props.feature @@ -51,3 +51,31 @@ Feature: Change properties of table When I set the vert_banding property to True And I save the presentation Then the columns of the table have alternating shading + + + Scenario: Append table row + Given a 2x2 table + When I add a row to a table + And I save the presentation + Then the table now has 3 rows + + + Scenario: Remove table row + Given a 2x2 table + When I remove a row from a table + And I save the presentation + Then the table now has 1 row + + + Scenario: Append table column + Given a 2x2 table + When I add a column to a table + And I save the presentation + Then the table now has 3 columns + + + Scenario: Append table column + Given a 2x2 table + When I remove a column from a table + And I save the presentation + Then the table now has 1 column diff --git a/pptx/shapes/table.py b/pptx/shapes/table.py index 1b73335ae..674442fd5 100644 --- a/pptx/shapes/table.py +++ b/pptx/shapes/table.py @@ -6,6 +6,8 @@ from __future__ import absolute_import, print_function +import copy + from . import Subshape from ..compat import is_integer, to_unicode from ..dml.fill import FillFormat @@ -370,6 +372,36 @@ def notify_width_changed(self): """ self._parent.notify_width_changed() + def add_column(self): + """ + Duplicates last column to keep formatting and resets it's cells text_frames + (e.g. ``column = table.columns.add_column()``). + Returns new |_Column| instance. + """ + new_col = copy.deepcopy(self._tbl.tblGrid.gridCol_lst[-1]) + self._tbl.tblGrid.append(new_col) # copies last grid element + + for tr in self._tbl.tr_lst: + # duplicate last cell of each row + new_tc = copy.deepcopy(tr.tc_lst[-1]) + tr.append(new_tc) + + cell = _Cell(new_tc, tr.tc_lst) + cell.text = '' + + return _Column(new_col, self) + + def remove(self, column): + """ + Removes specified *column* (e.g. ``table.columns.remove(table.columns[0])``). + """ + col_idx = self._tbl.tblGrid.index(column._gridCol) + + for tr in self._tbl.tr_lst: + tr.remove(tr.tc_lst[col_idx]) + + self._tbl.tblGrid.remove(column._gridCol) + class _RowCollection(Subshape): """ @@ -399,3 +431,25 @@ def notify_height_changed(self): Called by a row when its height changes. Pass along to parent. """ self._parent.notify_height_changed() + + def add_row(self): + """ + Duplicates last row to keep formatting and resets it's cells text_frames + (e.g. ``row = table.rows.add_row()``). + Returns new |_Row| instance. + """ + new_row = copy.deepcopy(self._tbl.tr_lst[-1]) # copies last row element + + for tc in new_row.tc_lst: + cell = _Cell(tc, new_row.tc_lst) + cell.text = '' + + self._tbl.append(new_row) + + return _Row(new_row, self) + + def remove(self, row): + """ + Removes specified *row* (e.g. ``table.rows.remove(table.rows[0])``). + """ + self._tbl.remove(row._tr) diff --git a/tests/shapes/test_table.py b/tests/shapes/test_table.py index 9945ae42c..bf61d61ef 100644 --- a/tests/shapes/test_table.py +++ b/tests/shapes/test_table.py @@ -425,6 +425,14 @@ def it_raises_on_indexed_access_out_of_range(self): with pytest.raises(IndexError): columns[9] + def it_supports_appending_column(self, append_fixture): + columns, expected_count = append_fixture + assert len(columns) == expected_count + + def it_supports_removing_column(self, remove_fixture): + columns, expected_count = remove_fixture + assert len(columns) == expected_count + # fixtures ------------------------------------------------------- @pytest.fixture(params=[ @@ -462,6 +470,28 @@ def len_fixture(self, request): columns = _ColumnCollection(element(tbl_cxml), None) return columns, expected_len + @pytest.fixture(params=[ + ('a:tbl/a:tblGrid/a:gridCol', 2), + ('a:tbl/a:tblGrid/(a:gridCol, a:gridCol)', 3), + ]) + def append_fixture(self, request): + tbl_cxml, expected_len = request.param + columns = _ColumnCollection(element(tbl_cxml), None) + columns.add_column() + + return columns, expected_len + + @pytest.fixture(params=[ + ('a:tbl/a:tblGrid/a:gridCol', 0), + ('a:tbl/a:tblGrid/(a:gridCol, a:gridCol)', 1), + ]) + def remove_fixture(self, request): + tbl_cxml, expected_len = request.param + columns = _ColumnCollection(element(tbl_cxml), None) + columns.remove(columns[0]) + + return columns, expected_len + class Describe_Row(object): @@ -555,6 +585,14 @@ def it_raises_on_indexed_access_out_of_range(self): with pytest.raises(IndexError): rows[9] + def it_supports_appending_row(self, append_fixture): + rows, expected_count = append_fixture + assert len(rows) == expected_count + + def it_supports_removing_row(self, remove_fixture): + rows, expected_count = remove_fixture + assert len(rows) == expected_count + # fixtures ------------------------------------------------------- @pytest.fixture(params=[ @@ -584,3 +622,23 @@ def len_fixture(self, request): tbl_cxml, expected_len = request.param rows = _RowCollection(element(tbl_cxml), None) return rows, expected_len + + @pytest.fixture(params=[ + ('a:tbl/a:tr', 2), ('a:tbl/(a:tr, a:tr)', 3), + ]) + def append_fixture(self, request): + tbl_cxml, expected_len = request.param + rows = _RowCollection(element(tbl_cxml), None) + rows.add_row() + + return rows, expected_len + + @pytest.fixture(params=[ + ('a:tbl/a:tr', 0), ('a:tbl/(a:tr, a:tr)', 1), + ]) + def remove_fixture(self, request): + tbl_cxml, expected_len = request.param + rows = _RowCollection(element(tbl_cxml), None) + rows.remove(rows[0]) + + return rows, expected_len