import logging from six.moves import zip import column import identifiers log = logging.getLogger(__name__) # Prefix for transform columns created during imports. _import_transform_col_prefix = 'gristHelper_Import_' def _gen_colids(transform_rule): """ For a transform_rule with colIds = None, fills in colIds generated from labels and returns the updated transform_rule. """ dest_cols = transform_rule["destCols"] if any(dc["colId"] for dc in dest_cols): raise ValueError("transform_rule already has colIds in _gen_colids") col_labels = [dest_col["label"] for dest_col in dest_cols] col_ids = identifiers.pick_col_ident_list(col_labels, avoid={'id'}) 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" dest_cols = transform_rule["destCols"] for dest_col in dest_cols: colId = dest_col["colId"] if colId and colId.startswith(_import_transform_col_prefix): dest_col["colId"] = colId[len(_import_transform_col_prefix):] class ImportActions(object): def __init__(self, useractions, docmodel, engine): self._useractions = useractions self._docmodel = docmodel self._engine = engine ######################## ## NOTES # transform_rule is an object like this: { # destCols: [ { colId, label, type, formula }, ... ], # ..., # other params unused in sandbox # } # # colId is defined if into_new_table, otherwise is None # GenImporterView gets a hidden table with a preview of the import data (~100 rows) # It adds formula cols and viewsections to the hidden table for the user to # preview and edit import options. GenImporterView can start with a default transform_rule # from table columns, or use one that's passed in (for reimporting). # client/components/Importer.ts then puts together transform_rule, which # specifies destination column formulas, types, labels, and colIds. It only contains colIds # if importing into an existing table, and they are sometimes prefixed with # _import_transform_col_prefix (if transform_rule comes from client) def _MakeDefaultTransformRule(self, hidden_table_id, dest_table_id): """ Makes a basic transform_rule.dest_cols copying all the source cols hidden_table_id: table with src data dest_table_id: table data is going to If dst_table is null, copy all src columns If dst_table exists, copy all dst columns, and make copy formulas if any names match returns transform_rule with only destCols filled in """ tables = self._docmodel.tables hidden_table_rec = tables.lookupOne(tableId=hidden_table_id) # will use these to set default formulas (if column names match in src and dest table) src_cols = {c.colId: c for c in hidden_table_rec.columns} target_table = tables.lookupOne(tableId=dest_table_id) if dest_table_id else hidden_table_rec target_cols = target_table.columns # makes dest_cols for each column in target_cols (defaults to same columns as hidden_table) #loop through visible, non-formula target columns dest_cols = [] for c in target_cols: if column.is_visible_column(c.colId) and (not c.isFormula or c.formula == ""): source_col = src_cols.get(c.colId) dest_col = { "label": c.label, "colId": c.colId if dest_table_id else None, #should be None if into new table "type": c.type, "widgetOptions": getattr(c, "widgetOptions", ""), "formula": ("$" + source_col.colId) if source_col else '', } if source_col and c.type.startswith("Ref:"): ref_table_id = c.type.split(':')[1] visible_col = c.visibleCol if visible_col: dest_col["visibleCol"] = visible_col.id dest_col["formula"] = '{}.lookupOne({}=${c}) or (${c} and str(${c}))'.format( ref_table_id, visible_col.colId, c=source_col.colId) dest_cols.append(dest_col) return {"destCols": dest_cols} # doesnt generate other fields of transform_rule, but sandbox only used destCols def _MakeImportTransformColumns(self, hidden_table_id, transform_rule, gen_all): """ Makes prefixed columns in the grist hidden import table (hidden_table_id) hidden_table_id: id of temporary hidden table in which columns are made transform_rule: defines columns to make (colids must be filled in!) gen_all: If true, all columns will be generated If false, formulas that just copy will be skipped returns list of newly created colrefs (rowids into _grist_Tables_column) """ tables = self._docmodel.tables hidden_table_rec = tables.lookupOne(tableId=hidden_table_id) src_cols = {c.colId for c in hidden_table_rec.columns} log.debug("destCols: %r", transform_rule['destCols']) dest_cols = transform_rule['destCols'] log.debug("_MakeImportTransformColumns: %s", "gen_all" if gen_all else "optimize") # Calling rebuild_usercode once per added column is wasteful and can be very slow. self._engine._should_rebuild_usercode = False #create prefixed formula column for each of dest_cols #take formula from transform_rule new_cols = [] try: for c in dest_cols: # skip copy columns (unless gen_all) formula = c["formula"].strip() isCopyFormula = (formula.startswith("$") and formula[1:] in src_cols) if gen_all or not isCopyFormula: # If colId specified, use that. Otherwise, use the (sanitized) label. col_id = c["colId"] or identifiers.pick_col_ident(c["label"]) visible_col_ref = c.get("visibleCol", 0) new_col_id = _import_transform_col_prefix + col_id new_col_spec = { "label": c["label"], "type": c["type"], "widgetOptions": c.get("widgetOptions", ""), "isFormula": True, "formula": c["formula"], "visibleCol": visible_col_ref, } result = self._useractions.doAddColumn(hidden_table_id, new_col_id, new_col_spec) new_col_id, new_col_ref = result["colId"], result["colRef"] if visible_col_ref: visible_col_id = self._docmodel.columns.table.get_record(visible_col_ref).colId self._useractions.SetDisplayFormula(hidden_table_id, None, new_col_ref, '${}.{}'.format(new_col_id, visible_col_id)) new_cols.append(new_col_ref) finally: self._engine._should_rebuild_usercode = True self._engine.rebuild_usercode() return new_cols def DoGenImporterView(self, source_table_id, dest_table_id, transform_rule, options): """ Generates formula columns for transformed importer columns, and optionally a new viewsection. source_table_id: id of temporary hidden table, containing source data and used for preview. dest_table_id: id of table to import to, or None for new table. transform_rule: transform_rule to reuse (if it still applies), if None will generate new one options: a dictionary with optional keys: createViewSection: defaults to True, in which case creates a new view-section to show the generated columns, for use in review, and remove any previous ones. genAll: defaults to True; if False, transform formulas that just copy will not be generated. refsAsInts: if set, treat Ref columns as type Int for a new dest_table. This is used when finishing imports from multi-table sources (e.g. from json) to avoid issues such as importing linked tables in the wrong order. Caller is expected to fix these up separately. Returns and object with: transformRule: updated (normalized) transform rule, or a newly generated one. viewSectionRef: rowId of the newly added section, present only if createViewSection is set. """ createViewSection = options.get("createViewSection", True) genAll = options.get("genAll", True) refsAsInts = options.get("refsAsInts", True) if createViewSection: src_table_rec = self._docmodel.tables.lookupOne(tableId=source_table_id) # ======== Cleanup old sections/columns # Transform columns are those that start with a special prefix. old_cols = [c for c in src_table_rec.columns if c.colId.startswith(_import_transform_col_prefix)] old_sections = {field.parentId for c in old_cols for field in c.viewFields} self._docmodel.remove(old_sections) self._docmodel.remove(old_cols) #======== Prepare/normalize transform_rule, Create new formula columns # Defaults to duplicating dest_table columns (or src_table columns for a new table) # If transform_rule provided, use that if transform_rule is None: transform_rule = self._MakeDefaultTransformRule(source_table_id, dest_table_id) # ensure prefixes, colIds are correct _strip_prefixes(transform_rule) if not dest_table_id: # into new table: 'colId's are undefined _gen_colids(transform_rule) # Treat destination Ref:* columns as Int instead, for new tables, to avoid issues when # importing linked tables in the wrong order. Caller is expected to fix up afterwards. if refsAsInts: for col in transform_rule["destCols"]: if col["type"].startswith("Ref:"): col["type"] = "Int" else: if None in (dc["colId"] for dc in transform_rule["destCols"]): errstr = "colIds must be defined in transform_rule for importing into existing table: " raise ValueError(errstr + repr(transform_rule)) new_cols = self._MakeImportTransformColumns(source_table_id, transform_rule, gen_all=genAll) result = {"transformRule": transform_rule} if createViewSection: #========= Create new transform view section. new_section = self._docmodel.add(self._docmodel.view_sections, tableRef=src_table_rec.id, parentKey='record', borderWidth=1, defaultWidth=100, sortColRefs='[]')[0] self._docmodel.add(new_section.fields, colRef=new_cols) result["viewSectionRef"] = new_section.id return result