(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
This commit is contained in:
Alex Hall 2021-09-30 14:14:52 +02:00
parent 02fd71d9bb
commit d4ea5b3761
5 changed files with 184 additions and 2 deletions

View File

@ -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):
"""

View File

@ -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):
"""

View File

@ -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],
])

View File

@ -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"]],
])

View File

@ -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.
#----------------------------------------