mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Migrate import code from data engine to Node
Summary: Finishing imports now occurs in Node instead of the data engine, which makes it possible to import into on-demand tables. Merging code was also refactored and now uses a SQL query to diff source and destination tables in order to determine what to update or add. Also fixes a bug where incremental imports involving Excel files with multiple sheets would fail due to the UI not serializing merge options correctly. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3046
This commit is contained in:
@@ -15,7 +15,8 @@ _import_transform_col_prefix = 'gristHelper_Import_'
|
||||
def _gen_colids(transform_rule):
|
||||
"""
|
||||
For a transform_rule with colIds = None,
|
||||
fills in colIds generated from labels.
|
||||
fills in colIds generated from labels and returns the updated
|
||||
transform_rule.
|
||||
"""
|
||||
|
||||
dest_cols = transform_rule["destCols"]
|
||||
@@ -29,6 +30,8 @@ def _gen_colids(transform_rule):
|
||||
for dest_col, col_id in zip(dest_cols, col_ids):
|
||||
dest_col["colId"] = col_id
|
||||
|
||||
return transform_rule
|
||||
|
||||
|
||||
def _strip_prefixes(transform_rule):
|
||||
"If transform_rule has prefixed _col_ids, strips prefix"
|
||||
@@ -40,68 +43,6 @@ def _strip_prefixes(transform_rule):
|
||||
dest_col["colId"] = colId[len(_import_transform_col_prefix):]
|
||||
|
||||
|
||||
def _is_blank(value):
|
||||
"If value is blank (e.g. None, blank string), returns true."
|
||||
if value is None:
|
||||
return True
|
||||
elif isinstance(value, six.string_types) and value.strip() == '':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _build_merge_col_map(column_data, merge_cols):
|
||||
"""
|
||||
Returns a dictionary with keys that are comprised of
|
||||
the values from column_data for the columns in
|
||||
merge_cols. The values are the row ids (index) in
|
||||
column_data for that particular key; multiple row ids
|
||||
imply that duplicates exist that contain the same values
|
||||
for all columns in merge_cols.
|
||||
|
||||
Used for merging into tables where fast, constant-time lookups
|
||||
are needed. For example, a source table can pass in its
|
||||
column_data into this function to build the map, and the
|
||||
destination table can then query the map using its own
|
||||
values for the columns in merge_cols to check for any
|
||||
matching rows that are candidates for updating.
|
||||
"""
|
||||
|
||||
merge_col_map = defaultdict(list)
|
||||
|
||||
for row_id, key in enumerate(zip(*[column_data[col] for col in merge_cols])):
|
||||
# If any part of the key is blank, don't include it in the map.
|
||||
if any(_is_blank(val) for val in key):
|
||||
continue
|
||||
|
||||
try:
|
||||
merge_col_map[key].append(row_id + 1)
|
||||
except TypeError:
|
||||
pass # If key isn't hashable, don't include it in the map.
|
||||
|
||||
return merge_col_map
|
||||
|
||||
# Dictionary mapping merge strategy types from ActiveDocAPI.ts to functions
|
||||
# that merge source and destination column values.
|
||||
#
|
||||
# NOTE: This dictionary should be kept in sync with the types in that file.
|
||||
#
|
||||
# All functions have the same signature: (src, dest) => output,
|
||||
# where src and dest are column values from a source and destination
|
||||
# table respectively, and output is either src or destination.
|
||||
#
|
||||
# For example, a key of replace-with-nonblank-source will return a merge function
|
||||
# that returns the src argument if it's not blank. Otherwise it returns the
|
||||
# dest argument. In the context of incremental imports, this is a function
|
||||
# that update destination fields when the source field isn't blank, preserving
|
||||
# existing values in the destination field that aren't replaced.
|
||||
_merge_funcs = {
|
||||
'replace-with-nonblank-source': lambda src, dest: dest if _is_blank(src) else src,
|
||||
'replace-all-fields': lambda src, _: src,
|
||||
'replace-blank-fields-only': lambda src, dest: src if _is_blank(dest) else dest
|
||||
}
|
||||
|
||||
|
||||
class ImportActions(object):
|
||||
|
||||
def __init__(self, useractions, docmodel, engine):
|
||||
@@ -130,11 +71,6 @@ class ImportActions(object):
|
||||
# if importing into an existing table, and they are sometimes prefixed with
|
||||
# _import_transform_col_prefix (if transform_rule comes from client)
|
||||
|
||||
# TransformAndFinishImport gets the full hidden_table (reparsed) and a transform_rule,
|
||||
# (or can use a default one if it's not provided). It fills in colIds if necessary and
|
||||
# strips colId prefixes. It also skips creating some formula columns
|
||||
# (ones with trivial copy formulas) as an optimization.
|
||||
|
||||
|
||||
def _MakeDefaultTransformRule(self, hidden_table_id, dest_table_id):
|
||||
"""
|
||||
@@ -174,9 +110,21 @@ class ImportActions(object):
|
||||
# doesnt generate other fields of transform_rule, but sandbox only used destCols
|
||||
|
||||
|
||||
def FillTransformRuleColIds(self, transform_rule):
|
||||
"""
|
||||
Takes a transform rule with missing dest col ids, and returns it
|
||||
with sanitized and de-duplicated ids generated from the original
|
||||
column labels.
|
||||
|
||||
# Returns
|
||||
def _MakeImportTransformColumns(self, hidden_table_id, transform_rule, gen_all):
|
||||
NOTE: This work could be done outside the data engine, but the logic
|
||||
for cleaning column identifiers is quite complex and currently only lives
|
||||
in the data engine. In the future, it may be worth porting it to
|
||||
Node to avoid an extra trip to the data engine.
|
||||
"""
|
||||
return _gen_colids(transform_rule)
|
||||
|
||||
|
||||
def MakeImportTransformColumns(self, hidden_table_id, transform_rule, gen_all):
|
||||
"""
|
||||
Makes prefixed columns in the grist hidden import table (hidden_table_id)
|
||||
|
||||
@@ -197,7 +145,7 @@ class ImportActions(object):
|
||||
#wrap dest_cols as namedtuples, to allow access like 'dest_col.param'
|
||||
dest_cols = [namedtuple('col', c.keys())(*c.values()) for c in transform_rule['destCols']]
|
||||
|
||||
log.debug("_MakeImportTransformColumns: {}".format("gen_all" if gen_all else "optimize"))
|
||||
log.debug("MakeImportTransformColumns: {}".format("gen_all" if gen_all else "optimize"))
|
||||
|
||||
#create prefixed formula column for each of dest_cols
|
||||
#take formula from transform_rule
|
||||
@@ -222,69 +170,6 @@ class ImportActions(object):
|
||||
return new_cols
|
||||
|
||||
|
||||
def _MergeColumnData(self, dest_table_id, column_data, merge_options):
|
||||
"""
|
||||
Merges column_data into table dest_table_id, replacing rows that
|
||||
match all merge_cols with values from column_data, and adding
|
||||
unmatched rows to the end of table dest_table_id.
|
||||
|
||||
dest_table_id: id of destination table
|
||||
column_data: column data from source table to merge into destination table
|
||||
merge_cols: list of column ids to use as keys for merging
|
||||
"""
|
||||
|
||||
dest_table = self._engine.tables[dest_table_id]
|
||||
merge_cols = merge_options['mergeCols']
|
||||
merge_col_map = _build_merge_col_map(column_data, merge_cols)
|
||||
|
||||
updated_row_ids = []
|
||||
updated_rows = {}
|
||||
new_rows = {}
|
||||
matched_src_table_rows = set()
|
||||
|
||||
# Initialize column data for new and updated rows.
|
||||
for col_id in six.iterkeys(column_data):
|
||||
updated_rows[col_id] = []
|
||||
new_rows[col_id] = []
|
||||
|
||||
strategy_type = merge_options['mergeStrategy']['type']
|
||||
merge = _merge_funcs[strategy_type]
|
||||
|
||||
# Compute which source table rows should update existing records in destination table.
|
||||
dest_cols = [dest_table.get_column(col) for col in merge_cols]
|
||||
for dest_row_id in dest_table.row_ids:
|
||||
lookup_key = tuple(col.raw_get(dest_row_id) for col in dest_cols)
|
||||
try:
|
||||
src_row_ids = merge_col_map.get(lookup_key)
|
||||
except TypeError:
|
||||
# We can arrive here if lookup_key isn't hashable. If that's the case, skip
|
||||
# this row since we can't efficiently search for a match in the source table.
|
||||
continue
|
||||
|
||||
if src_row_ids:
|
||||
matched_src_table_rows.update(src_row_ids)
|
||||
updated_row_ids.append(dest_row_id)
|
||||
for col_id, col_vals in six.iteritems(column_data):
|
||||
src_val = col_vals[src_row_ids[-1] - 1]
|
||||
dest_val = dest_table.get_column(col_id).raw_get(dest_row_id)
|
||||
updated_rows[col_id].append(merge(src_val, dest_val))
|
||||
|
||||
num_src_rows = len(column_data[merge_cols[0]])
|
||||
|
||||
# Compute which source table rows should be added to destination table as new records.
|
||||
for row_id in xrange(1, num_src_rows + 1):
|
||||
# If we've matched against the row before, we shouldn't add it.
|
||||
if row_id in matched_src_table_rows:
|
||||
continue
|
||||
|
||||
for col_id, col_val in six.iteritems(column_data):
|
||||
new_rows[col_id].append(col_val[row_id - 1])
|
||||
|
||||
self._useractions.BulkUpdateRecord(dest_table_id, updated_row_ids, updated_rows)
|
||||
self._useractions.BulkAddRecord(dest_table_id,
|
||||
[None] * (num_src_rows - len(matched_src_table_rows)), new_rows)
|
||||
|
||||
|
||||
def DoGenImporterView(self, source_table_id, dest_table_id, transform_rule = None):
|
||||
"""
|
||||
Generates viewsections/formula columns for importer
|
||||
@@ -336,7 +221,7 @@ class ImportActions(object):
|
||||
raise ValueError(errstr + repr(transform_rule))
|
||||
|
||||
|
||||
new_cols = self._MakeImportTransformColumns(source_table_id, transform_rule, gen_all=True)
|
||||
new_cols = self.MakeImportTransformColumns(source_table_id, transform_rule, gen_all=True)
|
||||
# we want to generate all columns so user can see them and edit
|
||||
|
||||
#========= Create new transform view section.
|
||||
@@ -348,94 +233,3 @@ class ImportActions(object):
|
||||
self._docmodel.add(new_section.fields, colRef=new_cols)
|
||||
|
||||
return new_section.id
|
||||
|
||||
|
||||
def DoTransformAndFinishImport(self, hidden_table_id, dest_table_id,
|
||||
into_new_table, transform_rule,
|
||||
merge_options):
|
||||
"""
|
||||
Finishes import into new or existing table depending on flag 'into_new_table'
|
||||
Returns destination table id. (new or existing)
|
||||
"""
|
||||
|
||||
hidden_table = self._engine.tables[hidden_table_id]
|
||||
hidden_table_rec = self._docmodel.tables.lookupOne(tableId=hidden_table_id)
|
||||
|
||||
src_cols = {c.colId for c in hidden_table_rec.columns}
|
||||
|
||||
log.debug("Starting TransformAndFinishImport, dest_cols:\n "
|
||||
+ str(transform_rule["destCols"] if transform_rule else "None"))
|
||||
log.debug("hidden_table_id:" + hidden_table_id)
|
||||
log.debug("hidden table columns: "
|
||||
+ str([(a.colId, a.label, a.type) for a in hidden_table_rec.columns]))
|
||||
log.debug("dest_table_id: "
|
||||
+ str(dest_table_id) + ('(NEW)' if into_new_table else '(Existing)'))
|
||||
|
||||
# === fill in blank transform rule
|
||||
if not transform_rule:
|
||||
transform_dest = None if into_new_table else dest_table_id
|
||||
transform_rule = self._MakeDefaultTransformRule(hidden_table_id, transform_dest)
|
||||
dest_cols = transform_rule["destCols"]
|
||||
|
||||
|
||||
# === Normalize transform rule (gen colids)
|
||||
|
||||
_strip_prefixes(transform_rule) #when transform_rule from client, colIds will be prefixed
|
||||
|
||||
if into_new_table: # 'colId's are undefined if making new table
|
||||
_gen_colids(transform_rule)
|
||||
else:
|
||||
if None in (dc["colId"] for dc in dest_cols):
|
||||
errstr = "colIds must be defined in transform_rule for importing into existing table: "
|
||||
raise ValueError(errstr + repr(transform_rule))
|
||||
|
||||
log.debug("Normalized dest_cols:\n " + str(dest_cols))
|
||||
|
||||
|
||||
# ======== Make and update formula columns
|
||||
# Make columns from transform_rule (now with filled-in colIds colIds),
|
||||
# gen_all false skips copy columns (faster)
|
||||
new_cols = self._MakeImportTransformColumns(hidden_table_id, transform_rule, gen_all=False)
|
||||
self._engine._bring_all_up_to_date()
|
||||
|
||||
# ========= Fetch Data for each col
|
||||
# (either copying, blank, or from formula column)
|
||||
|
||||
row_ids = list(hidden_table.row_ids) #fetch row_ids now, before we remove hidden_table
|
||||
log.debug("num rows: " + str(len(row_ids)))
|
||||
|
||||
column_data = {} # { col:[values...], ... }
|
||||
for curr_col in dest_cols:
|
||||
formula = curr_col["formula"].strip()
|
||||
|
||||
if formula:
|
||||
if (formula.startswith("$") and formula[1:] in src_cols): #copy formula
|
||||
src_col_id = formula[1:]
|
||||
else: #normal formula, fetch from prefix column
|
||||
src_col_id = _import_transform_col_prefix + curr_col["colId"]
|
||||
log.debug("Copying from: " + src_col_id)
|
||||
|
||||
src_col = hidden_table.get_column(src_col_id)
|
||||
column_data[curr_col["colId"]] = [src_col.raw_get(r) for r in row_ids]
|
||||
|
||||
|
||||
# ========= Cleanup, Prepare new table (if needed), insert data
|
||||
|
||||
self._useractions.RemoveTable(hidden_table_id)
|
||||
|
||||
if into_new_table:
|
||||
col_specs = [ {'type': curr_col['type'], 'id': curr_col['colId'], 'label': curr_col['label']}
|
||||
for curr_col in dest_cols]
|
||||
|
||||
log.debug("Making new table. Columns:\n " + str(col_specs))
|
||||
new_table = self._useractions.AddTable(dest_table_id, col_specs)
|
||||
dest_table_id = new_table['table_id']
|
||||
|
||||
if not merge_options.get('mergeCols'):
|
||||
self._useractions.BulkAddRecord(dest_table_id, [None] * len(row_ids), column_data)
|
||||
else:
|
||||
self._MergeColumnData(dest_table_id, column_data, merge_options)
|
||||
|
||||
log.debug("Finishing TransformAndFinishImport")
|
||||
|
||||
return dest_table_id
|
||||
|
||||
@@ -1,578 +0,0 @@
|
||||
# pylint: disable=line-too-long
|
||||
import logger
|
||||
import test_engine
|
||||
|
||||
log = logger.Logger(__name__, logger.INFO)
|
||||
|
||||
|
||||
#TODO: test naming (basics done, maybe check numbered column renaming)
|
||||
#TODO: check autoimport into existing table (match up column names)
|
||||
|
||||
|
||||
class TestImportTransform(test_engine.EngineTestCase):
|
||||
def init_state(self):
|
||||
# Add source table
|
||||
self.apply_user_action(['AddTable', 'Hidden_table', [
|
||||
{'id': 'employee_id', 'type': 'Int'},
|
||||
{'id': 'fname', 'type': 'Text'},
|
||||
{'id': 'mname', 'type': 'Text'},
|
||||
{'id': 'lname', 'type': 'Text'},
|
||||
{'id': 'email', 'type': 'Text'},
|
||||
]])
|
||||
self.apply_user_action(['BulkAddRecord', 'Hidden_table', [1, 2, 3, 4, 5, 6, 7], {
|
||||
'employee_id': [1, 2, 3, 4, 5, 6, 7],
|
||||
'fname': ['Bob', 'Carry', 'Don', 'Amir', 'Ken', 'George', 'Barbara'],
|
||||
'mname': ['F.', None, 'B.', '', 'C.', '', 'D.'],
|
||||
'lname': ['Nike', 'Jonson', "Yoon", "Greene", "Foster", "Huang", "Kinney"],
|
||||
'email': [
|
||||
'bob@example.com', None, "don@example.com", "amir@example.com",
|
||||
"ken@example.com", "", "barbara@example.com"
|
||||
]
|
||||
}])
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[1, "manualSort", "ManualSortPos", False, ""],
|
||||
[2, "employee_id", "Int", False, ""],
|
||||
[3, "fname", "Text", False, ""],
|
||||
[4, "mname", "Text", False, ""],
|
||||
[5, "lname", "Text", False, ""],
|
||||
[6, "email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 1)
|
||||
|
||||
#Filled in colids for existing table
|
||||
self.TEMP_transform_rule_colids = {
|
||||
"destCols": [
|
||||
{ "colId": "Employee_ID", "label": "Employee ID",
|
||||
"type": "Int", "formula": "$employee_id" },
|
||||
{ "colId": "First_Name", "label": "First Name",
|
||||
"type": "Text", "formula": "$fname" },
|
||||
{ "colId": "Last_Name", "label": "Last Name",
|
||||
"type": "Text", "formula": "$lname" },
|
||||
{ "colId": "Middle_Initial", "label": "Middle Initial",
|
||||
"type": "Text", "formula": "$mname[0] if $mname else ''" },
|
||||
{ "colId": "Email", "label": "Email",
|
||||
"type": "Text", "formula": "$email" },
|
||||
#{ "colId": "Blank", "label": "Blank", // Destination1 has no blank column
|
||||
# "type": "Text", "formula": "" },
|
||||
]
|
||||
}
|
||||
|
||||
#Then try it with blank in colIds (for new tables)
|
||||
self.TEMP_transform_rule_no_colids = {
|
||||
"destCols": [
|
||||
{ "colId": None, "label": "Employee ID",
|
||||
"type": "Int", "formula": "$employee_id" },
|
||||
{ "colId": None, "label": "First Name",
|
||||
"type": "Text", "formula": "$fname" },
|
||||
{ "colId": None, "label": "Last Name",
|
||||
"type": "Text", "formula": "$lname" },
|
||||
{ "colId": None, "label": "Middle Initial",
|
||||
"type": "Text", "formula": "$mname[0] if $mname else ''" },
|
||||
{ "colId": None, "label": "Email",
|
||||
"type": "Text", "formula": "$email" },
|
||||
{ "colId": None, "label": "Blank",
|
||||
"type": "Text", "formula": "" },
|
||||
]
|
||||
}
|
||||
|
||||
# Add destination table which contains columns corresponding to source table with different names
|
||||
self.apply_user_action(['AddTable', 'Destination1', [
|
||||
{'label': 'Employee ID', 'id': 'Employee_ID', 'type': 'Int'},
|
||||
{'label': 'First Name', 'id': 'First_Name', 'type': 'Text'},
|
||||
{'label': 'Last Name', 'id': 'Last_Name', 'type': 'Text'},
|
||||
{'label': 'Middle Initial', 'id': 'Middle_Initial', 'type': 'Text'},
|
||||
{'label': 'Email', 'id': 'Email', 'type': 'Text'}]])
|
||||
self.apply_user_action(['BulkAddRecord', 'Destination1', [1, 2, 3], {
|
||||
'Employee_ID': [1, 2, 3],
|
||||
'First_Name': ['Bob', 'Carry', 'Don'],
|
||||
'Last_Name': ['Nike', 'Jonson', "Yoon"],
|
||||
'Middle_Initial': ['F.', 'M.', None],
|
||||
'Email': ['', 'carry.m.jonson@example.com', 'don.b.yoon@example.com']
|
||||
}])
|
||||
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# Verify created tables
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [
|
||||
[1, "Hidden_table"],
|
||||
[2, "Destination1"]
|
||||
])
|
||||
|
||||
|
||||
def test_finish_import_into_new_table(self):
|
||||
# Add source and destination tables
|
||||
self.init_state()
|
||||
|
||||
#into_new_table = True, transform_rule : no colids (will be generated for new table), merge_options = {}
|
||||
out_actions = self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'NewTable', True, self.TEMP_transform_rule_no_colids, {}])
|
||||
self.assertPartialOutActions(out_actions, {
|
||||
"stored": [
|
||||
["AddColumn", "Hidden_table", "gristHelper_Import_Middle_Initial", {"formula": "$mname[0] if $mname else ''", "isFormula": True, "type": "Text"}],
|
||||
["AddRecord", "_grist_Tables_column", 13, {"colId": "gristHelper_Import_Middle_Initial", "formula": "$mname[0] if $mname else ''", "isFormula": True, "label": "Middle Initial", "parentId": 1, "parentPos": 13.0, "type": "Text", "widgetOptions": ""}],
|
||||
["BulkRemoveRecord", "_grist_Views_section_field", [1, 2, 3, 4, 5]],
|
||||
["RemoveRecord", "_grist_Views_section", 1],
|
||||
["RemoveRecord", "_grist_TabBar", 1],
|
||||
["RemoveRecord", "_grist_Pages", 1],
|
||||
["RemoveRecord", "_grist_Views", 1],
|
||||
["UpdateRecord", "_grist_Tables", 1, {"primaryViewId": 0}],
|
||||
["BulkRemoveRecord", "_grist_Tables_column", [1, 2, 3, 4, 5, 6, 13]],
|
||||
["RemoveRecord", "_grist_Tables", 1],
|
||||
["RemoveTable", "Hidden_table"],
|
||||
["AddTable", "NewTable", [{"formula": "", "id": "manualSort", "isFormula": False, "type": "ManualSortPos"}, {"formula": "", "id": "Employee_ID", "isFormula": False, "type": "Int"}, {"formula": "", "id": "First_Name", "isFormula": False, "type": "Text"}, {"formula": "", "id": "Last_Name", "isFormula": False, "type": "Text"}, {"formula": "", "id": "Middle_Initial", "isFormula": False, "type": "Text"}, {"formula": "", "id": "Email", "isFormula": False, "type": "Text"}, {"formula": "", "id": "Blank", "isFormula": False, "type": "Text"}]],
|
||||
["AddRecord", "_grist_Tables", 3, {"primaryViewId": 0, "tableId": "NewTable"}],
|
||||
["BulkAddRecord", "_grist_Tables_column", [13, 14, 15, 16, 17, 18, 19], {"colId": ["manualSort", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "Blank"], "formula": ["", "", "", "", "", "", ""], "isFormula": [False, False, False, False, False, False, False], "label": ["manualSort", "Employee ID", "First Name", "Last Name", "Middle Initial", "Email", "Blank"], "parentId": [3, 3, 3, 3, 3, 3, 3], "parentPos": [13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0], "type": ["ManualSortPos", "Int", "Text", "Text", "Text", "Text", "Text"], "widgetOptions": ["", "", "", "", "", "", ""]}],
|
||||
["AddRecord", "_grist_Views", 3, {"name": "NewTable", "type": "raw_data"}],
|
||||
["AddRecord", "_grist_TabBar", 3, {"tabPos": 3.0, "viewRef": 3}],
|
||||
["AddRecord", "_grist_Pages", 3, {"indentation": 0, "pagePos": 3.0, "viewRef": 3}],
|
||||
["AddRecord", "_grist_Views_section", 3, {"borderWidth": 1, "defaultWidth": 100, "parentId": 3, "parentKey": "record", "sortColRefs": "[]", "tableRef": 3, "title": ""}],
|
||||
["BulkAddRecord", "_grist_Views_section_field", [11, 12, 13, 14, 15, 16], {"colRef": [14, 15, 16, 17, 18, 19], "parentId": [3, 3, 3, 3, 3, 3], "parentPos": [11.0, 12.0, 13.0, 14.0, 15.0, 16.0]}],
|
||||
["UpdateRecord", "_grist_Tables", 3, {"primaryViewId": 3}],
|
||||
["BulkAddRecord", "NewTable", [1, 2, 3, 4, 5, 6, 7], {"Email": ["bob@example.com", None, "don@example.com", "amir@example.com", "ken@example.com", "", "barbara@example.com"], "Employee_ID": [1, 2, 3, 4, 5, 6, 7], "First_Name": ["Bob", "Carry", "Don", "Amir", "Ken", "George", "Barbara"], "Last_Name": ["Nike", "Jonson", "Yoon", "Greene", "Foster", "Huang", "Kinney"], "Middle_Initial": ["F", "", "B", "", "C", "", "D"], "manualSort": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]}],
|
||||
]
|
||||
})
|
||||
|
||||
#1-6 in hidden table, 7-12 in destTable, 13-19 for new table
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[13, "manualSort", "ManualSortPos", False, ""],
|
||||
[14, "Employee_ID", "Int", False, ""],
|
||||
[15, "First_Name", "Text", False, ""],
|
||||
[16, "Last_Name", "Text", False, ""],
|
||||
[17, "Middle_Initial", "Text", False, ""],
|
||||
[18, "Email", "Text", False, ""],
|
||||
[19, "Blank", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 3)
|
||||
|
||||
self.assertTableData('NewTable', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "Blank", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F", "bob@example.com", "", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "", None, "", 2.0],
|
||||
[3, 3, "Don", "Yoon", "B", "don@example.com", "", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", "", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C", "ken@example.com", "", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D", "barbara@example.com", "", 7.0],
|
||||
])
|
||||
|
||||
# Verify removed hidden table and add the new one
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [
|
||||
[2, "Destination1"],
|
||||
[3, "NewTable"]
|
||||
])
|
||||
|
||||
def test_finish_import_into_existing_table(self):
|
||||
# Add source and destination tables
|
||||
self.init_state()
|
||||
|
||||
#into_new_table = False, transform_rule : colids, merge_options = None
|
||||
self.apply_user_action(['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids, None])
|
||||
|
||||
#1-6 in hidden table, 7-12 in destTable
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# First 3 rows were already in Destination1 before import
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F.", "", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "Yoon", None, "don.b.yoon@example.com", 3.0],
|
||||
[4, 1, "Bob", "Nike", "F", "bob@example.com", 4.0],
|
||||
[5, 2, "Carry", "Jonson", "", None, 5.0],
|
||||
[6, 3, "Don", "Yoon", "B", "don@example.com", 6.0],
|
||||
[7, 4, "Amir", "Greene", "", "amir@example.com", 7.0],
|
||||
[8, 5, "Ken", "Foster", "C", "ken@example.com", 8.0],
|
||||
[9, 6, "George", "Huang", "", "", 9.0],
|
||||
[10, 7, "Barbara", "Kinney", "D", "barbara@example.com", 10.0],
|
||||
])
|
||||
|
||||
# Verify removed hidden table
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
#does the same thing using a blank transform rule
|
||||
def test_finish_import_into_new_table_blank(self):
|
||||
# Add source and destination tables
|
||||
self.init_state()
|
||||
|
||||
#into_new_table = True, transform_rule = None, merge_options = None
|
||||
self.apply_user_action(['TransformAndFinishImport', 'Hidden_table', 'NewTable', True, None, None])
|
||||
|
||||
#1-6 in src table, 7-12 in hiddentable
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[13, "manualSort", "ManualSortPos", False, ""],
|
||||
[14, "employee_id", "Int", False, ""],
|
||||
[15, "fname", "Text", False, ""],
|
||||
[16, "mname", "Text", False, ""],
|
||||
[17, "lname", "Text", False, ""],
|
||||
[18, "email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 3)
|
||||
|
||||
self.assertTableData('NewTable', cols="all", data=[
|
||||
["id", "employee_id", "fname", "lname", "mname", "email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F.", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", None, None, 2.0],
|
||||
[3, 3, "Don", "Yoon", "B.", "don@example.com", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C.", "ken@example.com", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D.", "barbara@example.com", 7.0],
|
||||
])
|
||||
|
||||
|
||||
# Verify removed hidden table and add the new one
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [
|
||||
[2, "Destination1"],
|
||||
[3, "NewTable"]
|
||||
])
|
||||
|
||||
def test_finish_import_into_existing_table_with_single_merge_col(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Use 'Employee_ID' as the merge column, updating existing employees in Destination1 with the same employee id.
|
||||
out_actions = self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['Employee_ID'], 'mergeStrategy': {'type': 'replace-with-nonblank-source'}}]
|
||||
)
|
||||
|
||||
# Check that the right actions were created.
|
||||
self.assertPartialOutActions(out_actions, {
|
||||
"stored": [
|
||||
["AddColumn", "Hidden_table", "gristHelper_Import_Middle_Initial", {"formula": "$mname[0] if $mname else ''", "isFormula": True, "type": "Text"}],
|
||||
["AddRecord", "_grist_Tables_column", 13, {"colId": "gristHelper_Import_Middle_Initial", "formula": "$mname[0] if $mname else ''", "isFormula": True, "label": "Middle Initial", "parentId": 1, "parentPos": 13.0, "type": "Text", "widgetOptions": ""}],
|
||||
["BulkRemoveRecord", "_grist_Views_section_field", [1, 2, 3, 4, 5]],
|
||||
["RemoveRecord", "_grist_Views_section", 1],
|
||||
["RemoveRecord", "_grist_TabBar", 1],
|
||||
["RemoveRecord", "_grist_Pages", 1],
|
||||
["RemoveRecord", "_grist_Views", 1],
|
||||
["UpdateRecord", "_grist_Tables", 1, {"primaryViewId": 0}],
|
||||
["BulkRemoveRecord", "_grist_Tables_column", [1, 2, 3, 4, 5, 6, 13]],
|
||||
["RemoveRecord", "_grist_Tables", 1],
|
||||
["RemoveTable", "Hidden_table"],
|
||||
["BulkUpdateRecord", "Destination1", [1, 3], {"Email": ["bob@example.com", "don@example.com"], "Middle_Initial": ["F", "B"]}],
|
||||
["BulkAddRecord", "Destination1", [4, 5, 6, 7], {"Email": ["amir@example.com", "ken@example.com", "", "barbara@example.com"], "Employee_ID": [4, 5, 6, 7], "First_Name": ["Amir", "Ken", "George", "Barbara"], "Last_Name": ["Greene", "Foster", "Huang", "Kinney"], "Middle_Initial": ["", "C", "", "D"], "manualSort": [4.0, 5.0, 6.0, 7.0]}],
|
||||
]
|
||||
})
|
||||
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# Check that Destination1 has no duplicates and that previous records (1 - 3) are updated.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "Yoon", "B", "don@example.com", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C", "ken@example.com", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D", "barbara@example.com", 7.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
def test_finish_import_into_existing_table_with_multiple_merge_cols(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Use 'First_Name' and 'Last_Name' as the merge columns, updating existing employees in Destination1 with the same name.
|
||||
out_actions = self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['First_Name', 'Last_Name'], 'mergeStrategy': {'type': 'replace-with-nonblank-source'}}]
|
||||
)
|
||||
|
||||
# Check that the right actions were created.
|
||||
self.assertPartialOutActions(out_actions, {
|
||||
"stored": [
|
||||
["AddColumn", "Hidden_table", "gristHelper_Import_Middle_Initial", {"formula": "$mname[0] if $mname else ''", "isFormula": True, "type": "Text"}],
|
||||
["AddRecord", "_grist_Tables_column", 13, {"colId": "gristHelper_Import_Middle_Initial", "formula": "$mname[0] if $mname else ''", "isFormula": True, "label": "Middle Initial", "parentId": 1, "parentPos": 13.0, "type": "Text", "widgetOptions": ""}],
|
||||
["BulkRemoveRecord", "_grist_Views_section_field", [1, 2, 3, 4, 5]],
|
||||
["RemoveRecord", "_grist_Views_section", 1],
|
||||
["RemoveRecord", "_grist_TabBar", 1],
|
||||
["RemoveRecord", "_grist_Pages", 1],
|
||||
["RemoveRecord", "_grist_Views", 1],
|
||||
["UpdateRecord", "_grist_Tables", 1, {"primaryViewId": 0}],
|
||||
["BulkRemoveRecord", "_grist_Tables_column", [1, 2, 3, 4, 5, 6, 13]],
|
||||
["RemoveRecord", "_grist_Tables", 1],
|
||||
["RemoveTable", "Hidden_table"],
|
||||
["BulkUpdateRecord", "Destination1", [1, 3], {"Email": ["bob@example.com", "don@example.com"], "Middle_Initial": ["F", "B"]}],
|
||||
["BulkAddRecord", "Destination1", [4, 5, 6, 7], {"Email": ["amir@example.com", "ken@example.com", "", "barbara@example.com"], "Employee_ID": [4, 5, 6, 7], "First_Name": ["Amir", "Ken", "George", "Barbara"], "Last_Name": ["Greene", "Foster", "Huang", "Kinney"], "Middle_Initial": ["", "C", "", "D"], "manualSort": [4.0, 5.0, 6.0, 7.0]}],
|
||||
]
|
||||
})
|
||||
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# Check that Destination1 has no duplicates and that previous records (1 - 3) are updated.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "Yoon", "B", "don@example.com", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C", "ken@example.com", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D", "barbara@example.com", 7.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
def test_finish_import_into_existing_table_with_no_matching_merge_cols(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Use 'Email' as the merge column: existing employees in Destination1 have different emails, so none should match incoming data.
|
||||
out_actions = self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['Email'], 'mergeStrategy': {'type': 'replace-with-nonblank-source'}}]
|
||||
)
|
||||
|
||||
# Check that the right actions were created.
|
||||
self.assertPartialOutActions(out_actions, {
|
||||
"stored": [
|
||||
["AddColumn", "Hidden_table", "gristHelper_Import_Middle_Initial", {"formula": "$mname[0] if $mname else ''", "isFormula": True, "type": "Text"}],
|
||||
["AddRecord", "_grist_Tables_column", 13, {"colId": "gristHelper_Import_Middle_Initial", "formula": "$mname[0] if $mname else ''", "isFormula": True, "label": "Middle Initial", "parentId": 1, "parentPos": 13.0, "type": "Text", "widgetOptions": ""}],
|
||||
["BulkRemoveRecord", "_grist_Views_section_field", [1, 2, 3, 4, 5]],
|
||||
["RemoveRecord", "_grist_Views_section", 1],
|
||||
["RemoveRecord", "_grist_TabBar", 1],
|
||||
["RemoveRecord", "_grist_Pages", 1],
|
||||
["RemoveRecord", "_grist_Views", 1],
|
||||
["UpdateRecord", "_grist_Tables", 1, {"primaryViewId": 0}],
|
||||
["BulkRemoveRecord", "_grist_Tables_column", [1, 2, 3, 4, 5, 6, 13]],
|
||||
["RemoveRecord", "_grist_Tables", 1],
|
||||
["RemoveTable", "Hidden_table"],
|
||||
["BulkAddRecord", "Destination1", [4, 5, 6, 7, 8, 9, 10], {"Email": ["bob@example.com", None, "don@example.com", "amir@example.com", "ken@example.com", "", "barbara@example.com"], "Employee_ID": [1, 2, 3, 4, 5, 6, 7], "First_Name": ["Bob", "Carry", "Don", "Amir", "Ken", "George", "Barbara"], "Last_Name": ["Nike", "Jonson", "Yoon", "Greene", "Foster", "Huang", "Kinney"], "Middle_Initial": ["F", "", "B", "", "C", "", "D"], "manualSort": [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]}],
|
||||
]
|
||||
})
|
||||
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# Check that no existing records were updated.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F.", "", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "Yoon", None, "don.b.yoon@example.com", 3.0],
|
||||
[4, 1, "Bob", "Nike", "F", "bob@example.com", 4.0],
|
||||
[5, 2, "Carry", "Jonson", "", None, 5.0],
|
||||
[6, 3, "Don", "Yoon", "B", "don@example.com", 6.0],
|
||||
[7, 4, "Amir", "Greene", "", "amir@example.com", 7.0],
|
||||
[8, 5, "Ken", "Foster", "C", "ken@example.com", 8.0],
|
||||
[9, 6, "George", "Huang", "", "", 9.0],
|
||||
[10, 7, "Barbara", "Kinney", "D", "barbara@example.com", 10.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
def test_replace_all_fields_merge_strategy(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Use replace all fields strategy on the 'Employee_ID' column.
|
||||
out_actions = self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['Employee_ID'], 'mergeStrategy': {'type': 'replace-all-fields'}}]
|
||||
)
|
||||
|
||||
# Check that the right actions were created.
|
||||
self.assertPartialOutActions(out_actions, {
|
||||
"stored": [
|
||||
["AddColumn", "Hidden_table", "gristHelper_Import_Middle_Initial", {"formula": "$mname[0] if $mname else ''", "isFormula": True, "type": "Text"}],
|
||||
["AddRecord", "_grist_Tables_column", 13, {"colId": "gristHelper_Import_Middle_Initial", "formula": "$mname[0] if $mname else ''", "isFormula": True, "label": "Middle Initial", "parentId": 1, "parentPos": 13.0, "type": "Text", "widgetOptions": ""}],
|
||||
["BulkRemoveRecord", "_grist_Views_section_field", [1, 2, 3, 4, 5]],
|
||||
["RemoveRecord", "_grist_Views_section", 1],
|
||||
["RemoveRecord", "_grist_TabBar", 1],
|
||||
["RemoveRecord", "_grist_Pages", 1],
|
||||
["RemoveRecord", "_grist_Views", 1],
|
||||
["UpdateRecord", "_grist_Tables", 1, {"primaryViewId": 0}],
|
||||
["BulkRemoveRecord", "_grist_Tables_column", [1, 2, 3, 4, 5, 6, 13]],
|
||||
["RemoveRecord", "_grist_Tables", 1],
|
||||
["RemoveTable", "Hidden_table"],
|
||||
["BulkUpdateRecord", "Destination1", [1, 2, 3], {"Email": ["bob@example.com", None, "don@example.com"], "Middle_Initial": ["F", "", "B"]}],
|
||||
["BulkAddRecord", "Destination1", [4, 5, 6, 7], {"Email": ["amir@example.com", "ken@example.com", "", "barbara@example.com"], "Employee_ID": [4, 5, 6, 7], "First_Name": ["Amir", "Ken", "George", "Barbara"], "Last_Name": ["Greene", "Foster", "Huang", "Kinney"], "Middle_Initial": ["", "C", "", "D"], "manualSort": [4.0, 5.0, 6.0, 7.0]}],
|
||||
]
|
||||
})
|
||||
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# Check that existing fields were replaced with incoming fields.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "", None, 2.0],
|
||||
[3, 3, "Don", "Yoon", "B", "don@example.com", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C", "ken@example.com", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D", "barbara@example.com", 7.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
def test_replace_blank_fields_only_merge_strategy(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Use replace blank fields only strategy on the 'Employee_ID' column.
|
||||
out_actions = self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['Employee_ID'], 'mergeStrategy': {'type': 'replace-blank-fields-only'}}]
|
||||
)
|
||||
|
||||
# Check that the right actions were created.
|
||||
self.assertPartialOutActions(out_actions, {
|
||||
"stored": [
|
||||
["AddColumn", "Hidden_table", "gristHelper_Import_Middle_Initial", {"formula": "$mname[0] if $mname else ''", "isFormula": True, "type": "Text"}],
|
||||
["AddRecord", "_grist_Tables_column", 13, {"colId": "gristHelper_Import_Middle_Initial", "formula": "$mname[0] if $mname else ''", "isFormula": True, "label": "Middle Initial", "parentId": 1, "parentPos": 13.0, "type": "Text", "widgetOptions": ""}],
|
||||
["BulkRemoveRecord", "_grist_Views_section_field", [1, 2, 3, 4, 5]],
|
||||
["RemoveRecord", "_grist_Views_section", 1],
|
||||
["RemoveRecord", "_grist_TabBar", 1],
|
||||
["RemoveRecord", "_grist_Pages", 1],
|
||||
["RemoveRecord", "_grist_Views", 1],
|
||||
["UpdateRecord", "_grist_Tables", 1, {"primaryViewId": 0}],
|
||||
["BulkRemoveRecord", "_grist_Tables_column", [1, 2, 3, 4, 5, 6, 13]],
|
||||
["RemoveRecord", "_grist_Tables", 1],
|
||||
["RemoveTable", "Hidden_table"],
|
||||
["BulkUpdateRecord", "Destination1", [1, 3], {"Email": ["bob@example.com", "don.b.yoon@example.com"], "Middle_Initial": ["F.", "B"]}],
|
||||
["BulkAddRecord", "Destination1", [4, 5, 6, 7], {"Email": ["amir@example.com", "ken@example.com", "", "barbara@example.com"], "Employee_ID": [4, 5, 6, 7], "First_Name": ["Amir", "Ken", "George", "Barbara"], "Last_Name": ["Greene", "Foster", "Huang", "Kinney"], "Middle_Initial": ["", "C", "", "D"], "manualSort": [4.0, 5.0, 6.0, 7.0]}],
|
||||
]
|
||||
})
|
||||
|
||||
self.assertTableData('_grist_Tables_column', cols="subset", data=[
|
||||
["id", "colId", "type", "isFormula", "formula"],
|
||||
[7, "manualSort", "ManualSortPos", False, ""],
|
||||
[8, "Employee_ID", "Int", False, ""],
|
||||
[9, "First_Name", "Text", False, ""],
|
||||
[10, "Last_Name", "Text", False, ""],
|
||||
[11, "Middle_Initial", "Text", False, ""],
|
||||
[12, "Email", "Text", False, ""],
|
||||
], rows=lambda r: r.parentId.id == 2)
|
||||
|
||||
# Check that only blank existing fields were updated.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F.", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "Yoon", "B", "don.b.yoon@example.com", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C", "ken@example.com", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D", "barbara@example.com", 7.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
def test_merging_updates_all_duplicates_in_destination_table(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Add duplicates to the destination table with different values than original.
|
||||
self.apply_user_action(['BulkAddRecord', 'Destination1', [4, 5], {
|
||||
'Employee_ID': [3, 3],
|
||||
'First_Name': ['Don', 'Don'],
|
||||
'Last_Name': ["Yoon", "Yoon"],
|
||||
'Middle_Initial': [None, 'B'],
|
||||
'Email': ['don.yoon@example.com', 'yoon.don@example.com']
|
||||
}])
|
||||
|
||||
# Use replace with nonblank source strategy on the 'Employee_ID' column.
|
||||
self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['Employee_ID'], 'mergeStrategy': {'type': 'replace-with-nonblank-source'}}]
|
||||
)
|
||||
|
||||
# Check that all duplicates were updated with new data from the source table.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "Yoon", "B", "don@example.com", 3.0],
|
||||
[4, 3, "Don", "Yoon", "B", "don@example.com", 4.0],
|
||||
[5, 3, "Don", "Yoon", "B", "don@example.com", 5.0],
|
||||
[6, 4, "Amir", "Greene", "", "amir@example.com", 6.0],
|
||||
[7, 5, "Ken", "Foster", "C", "ken@example.com", 7.0],
|
||||
[8, 6, "George", "Huang", "", "", 8.0],
|
||||
[9, 7, "Barbara", "Kinney", "D", "barbara@example.com", 9.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
|
||||
def test_merging_uses_latest_duplicate_in_source_table_for_matching(self):
|
||||
# Add source and destination tables.
|
||||
self.init_state()
|
||||
|
||||
# Add duplicates to the source table with different values than the original.
|
||||
self.apply_user_action(['BulkAddRecord', 'Hidden_table', [8, 9], {
|
||||
'employee_id': [3, 3],
|
||||
'fname': ['Don', 'Don'],
|
||||
'lname': ["Yoon", "yoon"],
|
||||
'mname': [None, None],
|
||||
'email': ['d.yoon@example.com', 'yoon.don@example.com']
|
||||
}])
|
||||
|
||||
# Use replace with nonblank source strategy on the 'Employee_ID' column.
|
||||
self.apply_user_action(
|
||||
['TransformAndFinishImport', 'Hidden_table', 'Destination1', False, self.TEMP_transform_rule_colids,
|
||||
{'mergeCols': ['Employee_ID'], 'mergeStrategy': {'type': 'replace-with-nonblank-source'}}]
|
||||
)
|
||||
|
||||
# Check that the last record for Don Yoon in the source table was used for updating the destination table.
|
||||
self.assertTableData('Destination1', cols="all", data=[
|
||||
["id", "Employee_ID", "First_Name", "Last_Name", "Middle_Initial", "Email", "manualSort"],
|
||||
[1, 1, "Bob", "Nike", "F", "bob@example.com", 1.0],
|
||||
[2, 2, "Carry", "Jonson", "M.", "carry.m.jonson@example.com", 2.0],
|
||||
[3, 3, "Don", "yoon", None, "yoon.don@example.com", 3.0],
|
||||
[4, 4, "Amir", "Greene", "", "amir@example.com", 4.0],
|
||||
[5, 5, "Ken", "Foster", "C", "ken@example.com", 5.0],
|
||||
[6, 6, "George", "Huang", "", "", 6.0],
|
||||
[7, 7, "Barbara", "Kinney", "D", "barbara@example.com", 7.0],
|
||||
])
|
||||
|
||||
self.assertPartialData("_grist_Tables", ["id", "tableId"], [[2, "Destination1"]])
|
||||
@@ -1550,8 +1550,9 @@ class UserActions(object):
|
||||
return self._import_actions.DoGenImporterView(source_table_id, dest_table_id, transform_rule)
|
||||
|
||||
@useraction
|
||||
def TransformAndFinishImport(self, hidden_table_id, dest_table_id, into_new_table,
|
||||
transform_rule, merge_options = None):
|
||||
return self._import_actions.DoTransformAndFinishImport(hidden_table_id, dest_table_id,
|
||||
into_new_table, transform_rule,
|
||||
merge_options or {})
|
||||
def MakeImportTransformColumns(self, source_table_id, transform_rule, gen_all):
|
||||
return self._import_actions.MakeImportTransformColumns(source_table_id, transform_rule, gen_all)
|
||||
|
||||
@useraction
|
||||
def FillTransformRuleColIds(self, transform_rule):
|
||||
return self._import_actions.FillTransformRuleColIds(transform_rule)
|
||||
|
||||
Reference in New Issue
Block a user