(core) Adding UI for reverse columns

Summary:
- Adding an UI for two-way reference column.
- Reusing table name as label for the reverse column

Test Plan: Updated

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D4344
This commit is contained in:
Jarosław Sadziński
2024-09-13 15:20:18 +02:00
parent da6c39aa50
commit e97a45143f
25 changed files with 1356 additions and 137 deletions

View File

@@ -545,6 +545,8 @@ class BaseReferenceColumn(BaseColumn):
value = objtypes.decode_object(value)
return self._target_table.lookup_one_record(**{col_id: value})
class UniqueReferenceError(ValueError):
pass
class ReferenceColumn(BaseReferenceColumn):
"""
@@ -564,7 +566,7 @@ class ReferenceColumn(BaseReferenceColumn):
def _list_to_value(self, value_as_list):
if len(value_as_list) > 1:
raise ValueError("UNIQUE reference constraint failed for action")
raise UniqueReferenceError("UNIQUE reference constraint violated")
return value_as_list[0] if value_as_list else 0
def _clean_up_value(self, value):

View File

@@ -1157,6 +1157,41 @@ class TestTwoWayReferences(test_engine.EngineTestCase):
[Azor, "Azor", Alice],
])
def test_back_update_empty_column(self):
"""
There was a bug. When user cretes a reverse column for an empty column, and then updates the
reverse column first, the empty column wasn't updated (as it was seen as empty).
"""
# Load pets sample
self.load_pets()
# Remove owner and add it back as empty column.
self.apply_user_action(["RemoveColumn", "Pets", "Owner"])
self.apply_user_action(["AddColumn", "Pets", "Owner", {
"type": "Ref:Owners",
"isFormula": True,
"formula": '',
}])
# Now add reverse column for Owner
self.apply_user_action(["AddReverseColumn", 'Pets', 'Owner'])
# And now add Rex with Alice as an owner using Owners table
self.apply_user_action(["UpdateRecord", "Owners", Alice, {"Pets": ['L', Rex]}])
# Make sure we see the data
self.assertTableData("Owners", cols="subset", data=[
["id", "Name", "Pets"],
[1, "Alice", [Rex]],
[2, "Bob", EmptyList],
])
self.assertTableData("Pets", cols="subset", data=[
["id", "Name", "Owner"],
[Rex, "Rex", Alice],
])
def uniqueReferences(rec):
return rec.reverseCol and rec.reverseCol.type.startswith('Ref:')

View File

@@ -257,6 +257,26 @@ class UserActions(object):
self._engine.out_actions.direct.append(self._indirection_level == DIRECT_ACTION)
self._engine.apply_doc_action(action)
def _do_extra_doc_action(self, action):
# It this is Update, Add (or Bulks), run thouse actions through ensure_column_accepts_data
# to ensure that the data is valid.
converted_action = action
if isinstance(action, (actions.BulkAddRecord, actions.BulkUpdateRecord)):
if isinstance(action, actions.BulkAddRecord):
ActionType = actions.BulkAddRecord
else:
ActionType = actions.BulkUpdateRecord
# Iterate over every column and make sure it accepts data.
table_id, row_ids, column_values = action
for col_id, values in six.iteritems(column_values):
column_values[col_id] = self._ensure_column_accepts_data(table_id, col_id, values)
converted_action = ActionType(table_id, row_ids, column_values)
return self._do_doc_action(converted_action)
def _bulk_action_iter(self, table_id, row_ids, col_values=None):
"""
Helper for processing Bulk actions, which generates a list of (i, record, value_dict) tuples,
@@ -408,7 +428,7 @@ class UserActions(object):
# If any extra actions were generated (e.g. to adjust positions), apply them.
for a in extra_actions:
self._do_doc_action(a)
self._do_extra_doc_action(a)
# We could set static default values for omitted data columns, or we can ensure that other
# code (JS, DocStorage) is aware of the static defaults. Since other code is already aware,
@@ -504,7 +524,7 @@ class UserActions(object):
# If any extra actions were generated (e.g. to adjust positions), apply them.
for a in extra_actions:
self._do_doc_action(a)
self._do_extra_doc_action(a)
# Finally, update the record
self._do_doc_action(action)
@@ -1781,6 +1801,13 @@ class UserActions(object):
if widgetOptions is None:
widgetOptions = src_col.widgetOptions
# If we are changing type, and this column is reverse column, make sure it is compatible.
# If not, break the connection first, UI should have already warned the user.
existing_type = dst_col.type
new_type = src_col.type
if not is_compatible_ref_type(new_type, existing_type) and dst_col.reverseCol:
self._docmodel.update([dst_col, src_col], reverseCol=0)
# Update the destination column to match the source's type and options. Also unset displayCol,
# except if src_col has a displayCol, then keep it unchanged until SetDisplayFormula below.
self._docmodel.update([dst_col], type=src_col.type, widgetOptions=[widgetOptions],
@@ -1936,6 +1963,7 @@ class UserActions(object):
ret = self.AddVisibleColumn(target_table_id, reverse_label, {
"isFormula": False,
"type": "RefList:" + table_id,
"label": reverse_label,
})
added_col = self._docmodel.columns.table.get_record(ret['colRef'])
self._docmodel.update([col_rec], reverseCol=added_col.id)