From d4ea5b376186f7cdfef5bdea44663a01d657b103 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Thu, 30 Sep 2021 14:14:52 +0200 Subject: [PATCH] (core) Add RenameChoices user action Summary: ["RenameChoices", table_id, col_id, renames] Updates the data in a Choice/ChoiceList column to reflect the new choice names. `renames` should be a dict of `{old_choice_name: new_choice_name}`. This doesn't touch the choices configuration in widgetOptions, that must be done separately. Frontend to be done in another diff. Test Plan: Added two Python unit tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3050 --- sandbox/grist/column.py | 17 ++++++ sandbox/grist/test_engine.py | 6 +- sandbox/grist/test_summary_choicelist.py | 75 ++++++++++++++++++++++++ sandbox/grist/test_useractions.py | 67 +++++++++++++++++++++ sandbox/grist/useractions.py | 21 ++++++- 5 files changed, 184 insertions(+), 2 deletions(-) diff --git a/sandbox/grist/column.py b/sandbox/grist/column.py index 7cf8f79d..0c214d43 100644 --- a/sandbox/grist/column.py +++ b/sandbox/grist/column.py @@ -225,6 +225,20 @@ class BaseColumn(object): # pylint: disable=no-self-use, unused-argument return values, [] + def rename_choices(self, renames): + row_ids = [] + values = [] + for row_id, value in enumerate(self._data): + if value is not None and self.type_obj.is_right_type(value): + value = self._rename_cell_choice(renames, value) + if value is not None: + row_ids.append(row_id) + values.append(value) + return row_ids, values + + def _rename_cell_choice(self, renames, value): + return renames.get(value, value) + class DataColumn(BaseColumn): """ @@ -361,6 +375,9 @@ class ChoiceListColumn(BaseColumn): def _make_rich_value(self, typed_value): return () if typed_value is None else typed_value + def _rename_cell_choice(self, renames, value): + return tuple(renames.get(choice, choice) for choice in value) + class BaseReferenceColumn(BaseColumn): """ diff --git a/sandbox/grist/test_engine.py b/sandbox/grist/test_engine.py index 40b675aa..763da686 100644 --- a/sandbox/grist/test_engine.py +++ b/sandbox/grist/test_engine.py @@ -3,6 +3,7 @@ import functools import json import unittest from collections import namedtuple +from pprint import pprint import six @@ -203,7 +204,10 @@ class EngineTestCase(unittest.TestCase): """ Prints out_actions in human-readable format, for help in writing / debugging tets. """ - print("\n".join(self._formatActionGroup(out_actions.__dict__))) + pprint({ + k: [get_comparable_repr(action) for action in getattr(out_actions, k)] + for k in self.action_group_action_fields + }) def assertTableData(self, table_name, data=[], cols="all", rows="all", sort=None): """ diff --git a/sandbox/grist/test_summary_choicelist.py b/sandbox/grist/test_summary_choicelist.py index 6825e93f..ba113475 100644 --- a/sandbox/grist/test_summary_choicelist.py +++ b/sandbox/grist/test_summary_choicelist.py @@ -352,3 +352,78 @@ class TestSummaryChoiceList(EngineTestCase): starting_table.columns[1] = starting_table.columns[1]._replace(type="ChoiceList") self.assertTables([starting_table, summary_table]) self.assertTableData('GristSummary_6_Source', data=data) + + def test_rename_choices(self): + self.load_sample(self.sample) + + # Create a summary section, grouped by both choicelist columns. + self.apply_user_action(["CreateViewSection", 1, 0, "record", [11, 12]]) + + summary_table = Table( + 2, "GristSummary_6_Source", primaryViewId=0, summarySourceTable=1, + columns=[ + Column(13, "choices1", "Choice", isFormula=False, formula="", summarySourceCol=11), + Column(14, "choices2", "Choice", isFormula=False, formula="", summarySourceCol=12), + Column(15, "group", "RefList:Source", isFormula=True, summarySourceCol=0, + formula="table.getSummarySourceGroup(rec)"), + Column(16, "count", "Int", isFormula=True, summarySourceCol=0, + formula="len($group)"), + ], + ) + + self.assertTables([self.starting_table, summary_table]) + + # Rename all the choices + out_actions = self.apply_user_action( + ["RenameChoices", "Source", "choices1", {"a": "aa", "b": "bb"}]) + self.apply_user_action( + ["RenameChoices", "Source", "choices2", {"c": "cc", "d": "dd"}]) + + # Actions from renaming choices1 only + self.assertPartialOutActions(out_actions, {'stored': [ + ['UpdateRecord', 'Source', 21, {'choices1': ['L', u'aa', u'bb']}], + ['BulkAddRecord', + 'GristSummary_6_Source', + [5, 6, 7, 8], + {'choices1': [u'aa', u'aa', u'bb', u'bb'], + 'choices2': [u'c', u'd', u'c', u'd']}], + ['BulkUpdateRecord', + 'GristSummary_6_Source', + [1, 2, 3, 4, 5, 6, 7, 8], + {'count': [0, 0, 0, 0, 1, 1, 1, 1]}], + ['BulkUpdateRecord', + 'GristSummary_6_Source', + [1, 2, 3, 4, 5, 6, 7, 8], + {'group': [['L'], + ['L'], + ['L'], + ['L'], + ['L', 21], + ['L', 21], + ['L', 21], + ['L', 21]]}] + ]}) + + # Final Source table is essentially the same as before, just with each letter doubled + self.assertTableData('Source', data=[ + ["id", "choices1", "choices2", "other"], + [21, ["aa", "bb"], ["cc", "dd"], "foo"], + ]) + + # Final summary table is very similar to before, but with two empty chunks of 4 rows + # left over from each rename + self.assertTableData('GristSummary_6_Source', data=[ + ["id", "choices1", "choices2", "group", "count"], + [1, "a", "c", [], 0], + [2, "a", "d", [], 0], + [3, "b", "c", [], 0], + [4, "b", "d", [], 0], + [5, "aa", "c", [], 0], + [6, "aa", "d", [], 0], + [7, "bb", "c", [], 0], + [8, "bb", "d", [], 0], + [9, "aa", "cc", [21], 1], + [10, "aa", "dd", [21], 1], + [11, "bb", "cc", [21], 1], + [12, "bb", "dd", [21], 1], + ]) diff --git a/sandbox/grist/test_useractions.py b/sandbox/grist/test_useractions.py index 6d8c466e..e393af1d 100644 --- a/sandbox/grist/test_useractions.py +++ b/sandbox/grist/test_useractions.py @@ -770,3 +770,70 @@ class TestUserActions(test_engine.EngineTestCase): ['id', 'indentation'], [ 3, 0], ]) + + #---------------------------------------------------------------------- + + def test_rename_choices(self): + sample = testutil.parse_test_sample({ + "SCHEMA": [ + [1, "ChoiceTable", [ + [1, "ChoiceColumn", "Choice", False, "", "ChoiceColumn", ""], + ]], + [2, "ChoiceListTable", [ + [2, "ChoiceListColumn", "ChoiceList", False, "", "ChoiceListColumn", ""], + ]], + ], + "DATA": { + "ChoiceTable": [ + ["id", "ChoiceColumn"], + [1, "a"], + [2, "b"], + [3, "c"], + [4, "d"], + [5, None], + ], + "ChoiceListTable": [ + ["id", "ChoiceListColumn"], + [1, ["a"]], + [2, ["b"]], + [3, ["c"]], + [4, ["d"]], + [5, None], + [7, ["a", "b"]], + [8, ["b", "c"]], + [9, ["a", "c"]], + [10, ["a", "b", "c"]], + ], + } + }) + self.load_sample(sample) + + # Renames go in a loop to make sure that works correctly + # a -> b -> c -> a -> b -> ... + renames = {"a": "b", "b": "c", "c": "a"} + self.apply_user_action( + ["RenameChoices", "ChoiceTable", "ChoiceColumn", renames]) + self.apply_user_action( + ["RenameChoices", "ChoiceListTable", "ChoiceListColumn", renames]) + + self.assertTableData('ChoiceTable', data=[ + ["id", "ChoiceColumn"], + [1, "b"], + [2, "c"], + [3, "a"], + [4, "d"], + [5, None], + ]) + + self.assertTableData('ChoiceListTable', data=[ + ["id", "ChoiceListColumn"], + [1, ["b"]], + [2, ["c"]], + [3, ["a"]], + [4, ["d"]], + [5, None], + [7, ["b", "c"]], + [8, ["c", "a"]], + [9, ["b", "a"]], + [10, ["b", "c", "a"]], + ]) diff --git a/sandbox/grist/useractions.py b/sandbox/grist/useractions.py index c390bf9f..afc0171b 100644 --- a/sandbox/grist/useractions.py +++ b/sandbox/grist/useractions.py @@ -12,7 +12,7 @@ from acl_formula import parse_acl_formula_json import actions import column import identifiers -from objtypes import strict_equal +from objtypes import strict_equal, encode_object import schema from schema import RecalcWhen import summary @@ -1247,6 +1247,25 @@ class UserActions(object): self.SetDisplayFormula(dst_col.parentId.tableId, None, dst_col.id, re.sub((r'\$%s\b' % src_col.colId), '$' + dst_col.colId, src_col.displayCol.formula)) + @useraction + def RenameChoices(self, table_id, col_id, renames): + """ + Updates the data in a Choice/ChoiceList column to reflect the new choice names. + `renames` should be a dict of {old_choice_name: new_choice_name}. + This doesn't touch the choices configuration in widgetOptions, that must be done separately. + """ + + table = self._engine.tables[table_id] + col = table.get_column(col_id) + + if col.is_formula(): + # We don't set the values of formula columns, they should just recalculate themselves + return None + + row_ids, values = col.rename_choices(renames) + values = [encode_object(v) for v in values] + return self.BulkUpdateRecord(table_id, row_ids, {col_id: values}) + #---------------------------------------- # User actions on tables. #----------------------------------------