"sql_metrics", # Druid connector "clusters", "datasources", "columns", "metrics", # Dashboard email schedules "dashboard_email_schedules", "slice_email_schedules", ] models = { table_name: type(table_name, (Base, ImportMixin), {"__tablename__": table_name}) for table_name in table_names } models["dashboards"].position_json = sa.Column(utils.MediumText()) def add_uuids(objects, session, batch_size=100): uuid_map = {} count = len(objects) for i, object_ in enumerate(objects): object_.uuid = uuid.uuid4() uuid_map[object_.id] = object_.uuid session.merge(object_) if (i + 1) % batch_size == 0: session.commit() print(f"uuid assigned to {i + 1} out of {count}") session.commit() print(f"Done! Assigned {count} uuids")
class Dashboard( # pylint: disable=too-many-instance-attributes Model, AuditMixinNullable, ImportMixin ): """The dashboard object!""" __tablename__ = "dashboards" id = Column(Integer, primary_key=True) dashboard_title = Column(String(500)) position_json = Column(utils.MediumText()) description = Column(Text) css = Column(Text) json_metadata = Column(Text) slug = Column(String(255), unique=True) slices = relationship("Slice", secondary=dashboard_slices, backref="dashboards") owners = relationship(security_manager.user_model, secondary=dashboard_user) published = Column(Boolean, default=False) export_fields = [ "dashboard_title", "position_json", "json_metadata", "description", "css", "slug", ] def __repr__(self) -> str: return self.dashboard_title or str(self.id) @property def table_names(self) -> str: # pylint: disable=no-member return ", ".join(str(s.datasource.full_name) for s in self.slices) @property def url(self) -> str: if self.json_metadata: # add default_filters to the preselect_filters of dashboard json_metadata = json.loads(self.json_metadata) default_filters = json_metadata.get("default_filters") # make sure default_filters is not empty and is valid if default_filters and default_filters != "{}": try: if json.loads(default_filters): filters = parse.quote(default_filters.encode("utf8")) return "/superset/dashboard/{}/?preselect_filters={}".format( self.slug or self.id, filters ) except Exception: # pylint: disable=broad-except pass return f"/superset/dashboard/{self.slug or self.id}/" @property def datasources(self) -> Set[Optional["BaseDatasource"]]: return {slc.datasource for slc in self.slices} @property def charts(self) -> List[Optional["BaseDatasource"]]: return [slc.chart for slc in self.slices] @property def sqla_metadata(self) -> None: # pylint: disable=no-member meta = MetaData(bind=self.get_sqla_engine()) meta.reflect() @renders("dashboard_title") def dashboard_link(self) -> Markup: title = escape(self.dashboard_title or "<empty>") return Markup(f'<a href="{self.url}">{title}</a>') @property def digest(self) -> str: """ Returns a MD5 HEX digest that makes this dashboard unique """ unique_string = f"{self.position_json}.{self.css}.{self.json_metadata}" return utils.md5_hex(unique_string) @property def thumbnail_url(self) -> str: """ Returns a thumbnail URL with a HEX digest. We want to avoid browser cache if the dashboard has changed """ return f"/api/v1/dashboard/{self.id}/thumbnail/{self.digest}/" @property def changed_by_name(self) -> str: if not self.changed_by: return "" return str(self.changed_by) @property def changed_by_url(self) -> str: if not self.changed_by: return "" return f"/superset/profile/{self.changed_by.username}" @property def data(self) -> Dict[str, Any]: positions = self.position_json if positions: positions = json.loads(positions) return { "id": self.id, "metadata": self.params_dict, "css": self.css, "dashboard_title": self.dashboard_title, "published": self.published, "slug": self.slug, "slices": [slc.data for slc in self.slices], "position_json": positions, } @property # type: ignore def params(self) -> str: # type: ignore return self.json_metadata @params.setter def params(self, value: str) -> None: self.json_metadata = value @property def position(self) -> Dict[str, Any]: if self.position_json: return json.loads(self.position_json) return {} @classmethod def import_obj( # pylint: disable=too-many-locals,too-many-branches,too-many-statements cls, dashboard_to_import: "Dashboard", import_time: Optional[int] = None ) -> int: """Imports the dashboard from the object to the database. Once dashboard is imported, json_metadata field is extended and stores remote_id and import_time. It helps to decide if the dashboard has to be overridden or just copies over. Slices that belong to this dashboard will be wired to existing tables. This function can be used to import/export dashboards between multiple superset instances. Audit metadata isn't copied over. """ def alter_positions( dashboard: Dashboard, old_to_new_slc_id_dict: Dict[int, int] ) -> None: """ Updates slice_ids in the position json. Sample position_json data: { "DASHBOARD_VERSION_KEY": "v2", "DASHBOARD_ROOT_ID": { "type": "DASHBOARD_ROOT_TYPE", "id": "DASHBOARD_ROOT_ID", "children": ["DASHBOARD_GRID_ID"] }, "DASHBOARD_GRID_ID": { "type": "DASHBOARD_GRID_TYPE", "id": "DASHBOARD_GRID_ID", "children": ["DASHBOARD_CHART_TYPE-2"] }, "DASHBOARD_CHART_TYPE-2": { "type": "CHART", "id": "DASHBOARD_CHART_TYPE-2", "children": [], "meta": { "width": 4, "height": 50, "chartId": 118 } }, } """ position_data = json.loads(dashboard.position_json) position_json = position_data.values() for value in position_json: if ( isinstance(value, dict) and value.get("meta") and value.get("meta", {}).get("chartId") ): old_slice_id = value["meta"]["chartId"] if old_slice_id in old_to_new_slc_id_dict: value["meta"]["chartId"] = old_to_new_slc_id_dict[old_slice_id] dashboard.position_json = json.dumps(position_data) logger.info( "Started import of the dashboard: %s", dashboard_to_import.to_json() ) session = db.session logger.info("Dashboard has %d slices", len(dashboard_to_import.slices)) # copy slices object as Slice.import_slice will mutate the slice # and will remove the existing dashboard - slice association slices = copy(dashboard_to_import.slices) old_json_metadata = json.loads(dashboard_to_import.json_metadata or "{}") old_to_new_slc_id_dict: Dict[int, int] = {} new_timed_refresh_immune_slices = [] new_expanded_slices = {} new_filter_scopes = {} i_params_dict = dashboard_to_import.params_dict remote_id_slice_map = { slc.params_dict["remote_id"]: slc for slc in session.query(Slice).all() if "remote_id" in slc.params_dict } for slc in slices: logger.info( "Importing slice %s from the dashboard: %s", slc.to_json(), dashboard_to_import.dashboard_title, ) remote_slc = remote_id_slice_map.get(slc.id) new_slc_id = Slice.import_obj(slc, remote_slc, import_time=import_time) old_to_new_slc_id_dict[slc.id] = new_slc_id # update json metadata that deals with slice ids new_slc_id_str = "{}".format(new_slc_id) old_slc_id_str = "{}".format(slc.id) if ( "timed_refresh_immune_slices" in i_params_dict and old_slc_id_str in i_params_dict["timed_refresh_immune_slices"] ): new_timed_refresh_immune_slices.append(new_slc_id_str) if ( "expanded_slices" in i_params_dict and old_slc_id_str in i_params_dict["expanded_slices"] ): new_expanded_slices[new_slc_id_str] = i_params_dict["expanded_slices"][ old_slc_id_str ] # since PR #9109, filter_immune_slices and filter_immune_slice_fields # are converted to filter_scopes # but dashboard create from import may still have old dashboard filter metadata # here we convert them to new filter_scopes metadata first filter_scopes = {} if ( "filter_immune_slices" in i_params_dict or "filter_immune_slice_fields" in i_params_dict ): filter_scopes = convert_filter_scopes(old_json_metadata, slices) if "filter_scopes" in i_params_dict: filter_scopes = old_json_metadata.get("filter_scopes") # then replace old slice id to new slice id: if filter_scopes: new_filter_scopes = copy_filter_scopes( old_to_new_slc_id_dict=old_to_new_slc_id_dict, old_filter_scopes=filter_scopes, ) # override the dashboard existing_dashboard = None for dash in session.query(Dashboard).all(): if ( "remote_id" in dash.params_dict and dash.params_dict["remote_id"] == dashboard_to_import.id ): existing_dashboard = dash dashboard_to_import = dashboard_to_import.copy() dashboard_to_import.id = None dashboard_to_import.reset_ownership() # position_json can be empty for dashboards # with charts added from chart-edit page and without re-arranging if dashboard_to_import.position_json: alter_positions(dashboard_to_import, old_to_new_slc_id_dict) dashboard_to_import.alter_params(import_time=import_time) dashboard_to_import.remove_params(param_to_remove="filter_immune_slices") dashboard_to_import.remove_params(param_to_remove="filter_immune_slice_fields") if new_filter_scopes: dashboard_to_import.alter_params(filter_scopes=new_filter_scopes) if new_expanded_slices: dashboard_to_import.alter_params(expanded_slices=new_expanded_slices) if new_timed_refresh_immune_slices: dashboard_to_import.alter_params( timed_refresh_immune_slices=new_timed_refresh_immune_slices ) new_slices = ( session.query(Slice) .filter(Slice.id.in_(old_to_new_slc_id_dict.values())) .all() ) if existing_dashboard: existing_dashboard.override(dashboard_to_import) existing_dashboard.slices = new_slices session.flush() return existing_dashboard.id dashboard_to_import.slices = new_slices session.add(dashboard_to_import) session.flush() return dashboard_to_import.id # type: ignore @classmethod def export_dashboards( # pylint: disable=too-many-locals cls, dashboard_ids: List[int] ) -> str: copied_dashboards = [] datasource_ids = set() for dashboard_id in dashboard_ids: # make sure that dashboard_id is an integer dashboard_id = int(dashboard_id) dashboard = ( db.session.query(Dashboard) .options(subqueryload(Dashboard.slices)) .filter_by(id=dashboard_id) .first() ) # remove ids and relations (like owners, created by, slices, ...) copied_dashboard = dashboard.copy() for slc in dashboard.slices: datasource_ids.add((slc.datasource_id, slc.datasource_type)) copied_slc = slc.copy() # save original id into json # we need it to update dashboard's json metadata on import copied_slc.id = slc.id # add extra params for the import copied_slc.alter_params( remote_id=slc.id, datasource_name=slc.datasource.datasource_name, schema=slc.datasource.schema, database_name=slc.datasource.database.name, ) # set slices without creating ORM relations slices = copied_dashboard.__dict__.setdefault("slices", []) slices.append(copied_slc) copied_dashboard.alter_params(remote_id=dashboard_id) copied_dashboards.append(copied_dashboard) eager_datasources = [] for datasource_id, datasource_type in datasource_ids: eager_datasource = ConnectorRegistry.get_eager_datasource( db.session, datasource_type, datasource_id ) copied_datasource = eager_datasource.copy() copied_datasource.alter_params( remote_id=eager_datasource.id, database_name=eager_datasource.database.name, ) datasource_class = copied_datasource.__class__ for field_name in datasource_class.export_children: field_val = getattr(eager_datasource, field_name).copy() # set children without creating ORM relations copied_datasource.__dict__[field_name] = field_val eager_datasources.append(copied_datasource) return json.dumps( {"dashboards": copied_dashboards, "datasources": eager_datasources}, cls=utils.DashboardEncoder, indent=4, )
class Dashboard(Model, AuditMixinNullable, ImportMixin): """The dashboard object!""" __tablename__ = 'dashboards' id = Column(Integer, primary_key=True) dashboard_title = Column(String(500)) position_json = Column(utils.MediumText()) description = Column(Text) css = Column(Text) json_metadata = Column(Text) slug = Column(String(255), unique=True) slices = relationship('Slice', secondary=dashboard_slices, backref='dashboards') owners = relationship(security_manager.user_model, secondary=dashboard_user) export_fields = ('dashboard_title', 'position_json', 'json_metadata', 'description', 'css', 'slug') def __repr__(self): return self.dashboard_title @property def table_names(self): # pylint: disable=no-member return ', '.join( {'{}'.format(s.datasource.full_name) for s in self.slices}) @property def url(self): if self.json_metadata: # add default_filters to the preselect_filters of dashboard json_metadata = json.loads(self.json_metadata) default_filters = json_metadata.get('default_filters') # make sure default_filters is not empty and is valid if default_filters and default_filters != '{}': try: if json.loads(default_filters): filters = parse.quote(default_filters.encode('utf8')) return '/superset/dashboard/{}/?preselect_filters={}'.format( self.slug or self.id, filters) except Exception: pass return '/superset/dashboard/{}/'.format(self.slug or self.id) @property def datasources(self): return {slc.datasource for slc in self.slices} @property def sqla_metadata(self): # pylint: disable=no-member metadata = MetaData(bind=self.get_sqla_engine()) return metadata.reflect() def dashboard_link(self): title = escape(self.dashboard_title) return Markup(f'<a href="{self.url}">{title}</a>') @property def data(self): positions = self.position_json if positions: positions = json.loads(positions) return { 'id': self.id, 'metadata': self.params_dict, 'css': self.css, 'dashboard_title': self.dashboard_title, 'slug': self.slug, 'slices': [slc.data for slc in self.slices], 'position_json': positions, } @property def params(self): return self.json_metadata @params.setter def params(self, value): self.json_metadata = value @property def position(self): if self.position_json: return json.loads(self.position_json) return {} @classmethod def import_obj(cls, dashboard_to_import, import_time=None): """Imports the dashboard from the object to the database. Once dashboard is imported, json_metadata field is extended and stores remote_id and import_time. It helps to decide if the dashboard has to be overridden or just copies over. Slices that belong to this dashboard will be wired to existing tables. This function can be used to import/export dashboards between multiple superset instances. Audit metadata isn't copied over. """ def alter_positions(dashboard, old_to_new_slc_id_dict): """ Updates slice_ids in the position json. Sample position_json data: { "DASHBOARD_VERSION_KEY": "v2", "DASHBOARD_ROOT_ID": { "type": "DASHBOARD_ROOT_TYPE", "id": "DASHBOARD_ROOT_ID", "children": ["DASHBOARD_GRID_ID"] }, "DASHBOARD_GRID_ID": { "type": "DASHBOARD_GRID_TYPE", "id": "DASHBOARD_GRID_ID", "children": ["DASHBOARD_CHART_TYPE-2"] }, "DASHBOARD_CHART_TYPE-2": { "type": "DASHBOARD_CHART_TYPE", "id": "DASHBOARD_CHART_TYPE-2", "children": [], "meta": { "width": 4, "height": 50, "chartId": 118 } }, } """ position_data = json.loads(dashboard.position_json) position_json = position_data.values() for value in position_json: if (isinstance(value, dict) and value.get('meta') and value.get('meta').get('chartId')): old_slice_id = value.get('meta').get('chartId') if old_slice_id in old_to_new_slc_id_dict: value['meta']['chartId'] = ( old_to_new_slc_id_dict[old_slice_id]) dashboard.position_json = json.dumps(position_data) logging.info('Started import of the dashboard: {}'.format( dashboard_to_import.to_json())) session = db.session logging.info('Dashboard has {} slices'.format( len(dashboard_to_import.slices))) # copy slices object as Slice.import_slice will mutate the slice # and will remove the existing dashboard - slice association slices = copy(dashboard_to_import.slices) old_to_new_slc_id_dict = {} new_filter_immune_slices = [] new_timed_refresh_immune_slices = [] new_expanded_slices = {} i_params_dict = dashboard_to_import.params_dict remote_id_slice_map = { slc.params_dict['remote_id']: slc for slc in session.query(Slice).all() if 'remote_id' in slc.params_dict } for slc in slices: logging.info('Importing slice {} from the dashboard: {}'.format( slc.to_json(), dashboard_to_import.dashboard_title)) remote_slc = remote_id_slice_map.get(slc.id) new_slc_id = Slice.import_obj(slc, remote_slc, import_time=import_time) old_to_new_slc_id_dict[slc.id] = new_slc_id # update json metadata that deals with slice ids new_slc_id_str = '{}'.format(new_slc_id) old_slc_id_str = '{}'.format(slc.id) if ('filter_immune_slices' in i_params_dict and old_slc_id_str in i_params_dict['filter_immune_slices']): new_filter_immune_slices.append(new_slc_id_str) if ('timed_refresh_immune_slices' in i_params_dict and old_slc_id_str in i_params_dict['timed_refresh_immune_slices']): new_timed_refresh_immune_slices.append(new_slc_id_str) if ('expanded_slices' in i_params_dict and old_slc_id_str in i_params_dict['expanded_slices']): new_expanded_slices[new_slc_id_str] = ( i_params_dict['expanded_slices'][old_slc_id_str]) # override the dashboard existing_dashboard = None for dash in session.query(Dashboard).all(): if ('remote_id' in dash.params_dict and dash.params_dict['remote_id'] == dashboard_to_import.id): existing_dashboard = dash dashboard_to_import.id = None alter_positions(dashboard_to_import, old_to_new_slc_id_dict) dashboard_to_import.alter_params(import_time=import_time) if new_expanded_slices: dashboard_to_import.alter_params( expanded_slices=new_expanded_slices) if new_filter_immune_slices: dashboard_to_import.alter_params( filter_immune_slices=new_filter_immune_slices) if new_timed_refresh_immune_slices: dashboard_to_import.alter_params( timed_refresh_immune_slices=new_timed_refresh_immune_slices) new_slices = session.query(Slice).filter( Slice.id.in_(old_to_new_slc_id_dict.values())).all() if existing_dashboard: existing_dashboard.override(dashboard_to_import) existing_dashboard.slices = new_slices session.flush() return existing_dashboard.id else: # session.add(dashboard_to_import) causes sqlachemy failures # related to the attached users / slices. Creating new object # allows to avoid conflicts in the sql alchemy state. copied_dash = dashboard_to_import.copy() copied_dash.slices = new_slices session.add(copied_dash) session.flush() return copied_dash.id @classmethod def export_dashboards(cls, dashboard_ids): copied_dashboards = [] datasource_ids = set() for dashboard_id in dashboard_ids: # make sure that dashboard_id is an integer dashboard_id = int(dashboard_id) copied_dashboard = (db.session.query(Dashboard).options( subqueryload( Dashboard.slices)).filter_by(id=dashboard_id).first()) make_transient(copied_dashboard) for slc in copied_dashboard.slices: datasource_ids.add((slc.datasource_id, slc.datasource_type)) # add extra params for the import slc.alter_params( remote_id=slc.id, datasource_name=slc.datasource.name, schema=slc.datasource.name, database_name=slc.datasource.database.name, ) copied_dashboard.alter_params(remote_id=dashboard_id) copied_dashboards.append(copied_dashboard) eager_datasources = [] for dashboard_id, dashboard_type in datasource_ids: eager_datasource = ConnectorRegistry.get_eager_datasource( db.session, dashboard_type, dashboard_id) eager_datasource.alter_params( remote_id=eager_datasource.id, database_name=eager_datasource.database.name, ) make_transient(eager_datasource) eager_datasources.append(eager_datasource) return json.dumps( { 'dashboards': copied_dashboards, 'datasources': eager_datasources, }, cls=utils.DashboardEncoder, indent=4)
class Dashboard(Model, AuditMixinNullable, ImportMixin): """The dashboard object!""" __tablename__ = "dashboards" id = Column(Integer, primary_key=True) dashboard_title = Column(String(500)) position_json = Column(utils.MediumText()) description = Column(Text) css = Column(Text) json_metadata = Column(Text) slug = Column(String(255), unique=True) slices = relationship("Slice", secondary=dashboard_slices, backref="dashboards") owners = relationship(security_manager.user_model, secondary=dashboard_user) published = Column(Boolean, default=False) export_fields = ( "dashboard_title", "position_json", "json_metadata", "description", "css", "slug", ) def __repr__(self): return self.dashboard_title or str(self.id) @property def table_names(self): # pylint: disable=no-member return ", ".join( {"{}".format(s.datasource.full_name) for s in self.slices}) @property def url(self): if self.json_metadata: # add default_filters to the preselect_filters of dashboard json_metadata = json.loads(self.json_metadata) default_filters = json_metadata.get("default_filters") # make sure default_filters is not empty and is valid if default_filters and default_filters != "{}": try: if json.loads(default_filters): filters = parse.quote(default_filters.encode("utf8")) return "/superset/dashboard/{}/?preselect_filters={}".format( self.slug or self.id, filters) except Exception: pass return "/superset/dashboard/{}/".format(self.slug or self.id) @property def datasources(self): return {slc.datasource for slc in self.slices} @property def charts(self): return [slc.chart for slc in self.slices] @property def sqla_metadata(self): # pylint: disable=no-member metadata = MetaData(bind=self.get_sqla_engine()) return metadata.reflect() def dashboard_link(self): title = escape(self.dashboard_title or "<empty>") return Markup(f'<a href="{self.url}">{title}</a>') @property def data(self): positions = self.position_json if positions: positions = json.loads(positions) return { "id": self.id, "metadata": self.params_dict, "css": self.css, "dashboard_title": self.dashboard_title, "published": self.published, "slug": self.slug, "slices": [slc.data for slc in self.slices], "position_json": positions, } @property def params(self): return self.json_metadata @params.setter def params(self, value): self.json_metadata = value @property def position(self): if self.position_json: return json.loads(self.position_json) return {} @classmethod def import_obj(cls, dashboard_to_import, import_time=None): """Imports the dashboard from the object to the database. Once dashboard is imported, json_metadata field is extended and stores remote_id and import_time. It helps to decide if the dashboard has to be overridden or just copies over. Slices that belong to this dashboard will be wired to existing tables. This function can be used to import/export dashboards between multiple superset instances. Audit metadata isn't copied over. """ def alter_positions(dashboard, old_to_new_slc_id_dict): """ Updates slice_ids in the position json. Sample position_json data: { "DASHBOARD_VERSION_KEY": "v2", "DASHBOARD_ROOT_ID": { "type": "DASHBOARD_ROOT_TYPE", "id": "DASHBOARD_ROOT_ID", "children": ["DASHBOARD_GRID_ID"] }, "DASHBOARD_GRID_ID": { "type": "DASHBOARD_GRID_TYPE", "id": "DASHBOARD_GRID_ID", "children": ["DASHBOARD_CHART_TYPE-2"] }, "DASHBOARD_CHART_TYPE-2": { "type": "DASHBOARD_CHART_TYPE", "id": "DASHBOARD_CHART_TYPE-2", "children": [], "meta": { "width": 4, "height": 50, "chartId": 118 } }, } """ position_data = json.loads(dashboard.position_json) position_json = position_data.values() for value in position_json: if (isinstance(value, dict) and value.get("meta") and value.get("meta").get("chartId")): old_slice_id = value.get("meta").get("chartId") if old_slice_id in old_to_new_slc_id_dict: value["meta"]["chartId"] = old_to_new_slc_id_dict[ old_slice_id] dashboard.position_json = json.dumps(position_data) logging.info("Started import of the dashboard: {}".format( dashboard_to_import.to_json())) session = db.session logging.info("Dashboard has {} slices".format( len(dashboard_to_import.slices))) # copy slices object as Slice.import_slice will mutate the slice # and will remove the existing dashboard - slice association slices = copy(dashboard_to_import.slices) old_to_new_slc_id_dict = {} new_filter_immune_slices = [] new_timed_refresh_immune_slices = [] new_expanded_slices = {} i_params_dict = dashboard_to_import.params_dict remote_id_slice_map = { slc.params_dict["remote_id"]: slc for slc in session.query(Slice).all() if "remote_id" in slc.params_dict } for slc in slices: logging.info("Importing slice {} from the dashboard: {}".format( slc.to_json(), dashboard_to_import.dashboard_title)) remote_slc = remote_id_slice_map.get(slc.id) new_slc_id = Slice.import_obj(slc, remote_slc, import_time=import_time) old_to_new_slc_id_dict[slc.id] = new_slc_id # update json metadata that deals with slice ids new_slc_id_str = "{}".format(new_slc_id) old_slc_id_str = "{}".format(slc.id) if ("filter_immune_slices" in i_params_dict and old_slc_id_str in i_params_dict["filter_immune_slices"]): new_filter_immune_slices.append(new_slc_id_str) if ("timed_refresh_immune_slices" in i_params_dict and old_slc_id_str in i_params_dict["timed_refresh_immune_slices"]): new_timed_refresh_immune_slices.append(new_slc_id_str) if ("expanded_slices" in i_params_dict and old_slc_id_str in i_params_dict["expanded_slices"]): new_expanded_slices[new_slc_id_str] = i_params_dict[ "expanded_slices"][old_slc_id_str] # override the dashboard existing_dashboard = None for dash in session.query(Dashboard).all(): if ("remote_id" in dash.params_dict and dash.params_dict["remote_id"] == dashboard_to_import.id): existing_dashboard = dash dashboard_to_import.id = None # position_json can be empty for dashboards # with charts added from chart-edit page and without re-arranging if dashboard_to_import.position_json: alter_positions(dashboard_to_import, old_to_new_slc_id_dict) dashboard_to_import.alter_params(import_time=import_time) if new_expanded_slices: dashboard_to_import.alter_params( expanded_slices=new_expanded_slices) if new_filter_immune_slices: dashboard_to_import.alter_params( filter_immune_slices=new_filter_immune_slices) if new_timed_refresh_immune_slices: dashboard_to_import.alter_params( timed_refresh_immune_slices=new_timed_refresh_immune_slices) new_slices = (session.query(Slice).filter( Slice.id.in_(old_to_new_slc_id_dict.values())).all()) if existing_dashboard: existing_dashboard.override(dashboard_to_import) existing_dashboard.slices = new_slices session.flush() return existing_dashboard.id else: # session.add(dashboard_to_import) causes sqlachemy failures # related to the attached users / slices. Creating new object # allows to avoid conflicts in the sql alchemy state. copied_dash = dashboard_to_import.copy() copied_dash.slices = new_slices session.add(copied_dash) session.flush() return copied_dash.id @classmethod def export_dashboards(cls, dashboard_ids): copied_dashboards = [] datasource_ids = set() for dashboard_id in dashboard_ids: # make sure that dashboard_id is an integer dashboard_id = int(dashboard_id) dashboard = (db.session.query(Dashboard).options( subqueryload( Dashboard.slices)).filter_by(id=dashboard_id).first()) # remove ids and relations (like owners, created by, slices, ...) copied_dashboard = dashboard.copy() for slc in dashboard.slices: datasource_ids.add((slc.datasource_id, slc.datasource_type)) copied_slc = slc.copy() # save original id into json # we need it to update dashboard's json metadata on import copied_slc.id = slc.id # add extra params for the import copied_slc.alter_params( remote_id=slc.id, datasource_name=slc.datasource.name, schema=slc.datasource.schema, database_name=slc.datasource.database.name, ) # set slices without creating ORM relations slices = copied_dashboard.__dict__.setdefault("slices", []) slices.append(copied_slc) copied_dashboard.alter_params(remote_id=dashboard_id) copied_dashboards.append(copied_dashboard) eager_datasources = [] for datasource_id, datasource_type in datasource_ids: eager_datasource = ConnectorRegistry.get_eager_datasource( db.session, datasource_type, datasource_id) copied_datasource = eager_datasource.copy() copied_datasource.alter_params( remote_id=eager_datasource.id, database_name=eager_datasource.database.name, ) datasource_class = copied_datasource.__class__ for field_name in datasource_class.export_children: field_val = getattr(eager_datasource, field_name).copy() # set children without creating ORM relations copied_datasource.__dict__[field_name] = field_val eager_datasources.append(copied_datasource) return json.dumps( { "dashboards": copied_dashboards, "datasources": eager_datasources }, cls=utils.DashboardEncoder, indent=4, )
class Dashboard( # pylint: disable=too-many-instance-attributes Model, AuditMixinNullable, ImportExportMixin): """The dashboard object!""" __tablename__ = "dashboards" id = Column(Integer, primary_key=True) dashboard_title = Column(String(500)) position_json = Column(utils.MediumText()) description = Column(Text) css = Column(Text) json_metadata = Column(Text) slug = Column(String(255), unique=True) slices = relationship(Slice, secondary=dashboard_slices, backref="dashboards") owners = relationship(security_manager.user_model, secondary=dashboard_user) published = Column(Boolean, default=False) roles = relationship(security_manager.role_model, secondary=DashboardRoles) export_fields = [ "dashboard_title", "position_json", "json_metadata", "description", "css", "slug", ] def __repr__(self) -> str: return f"Dashboard<{self.id or self.slug}>" @property def table_names(self) -> str: # pylint: disable=no-member return ", ".join(str(s.datasource.full_name) for s in self.slices) @property def url(self) -> str: return f"/superset/dashboard/{self.slug or self.id}/" @property def datasources(self) -> Set[BaseDatasource]: return {slc.datasource for slc in self.slices} @property def charts(self) -> List[BaseDatasource]: return [slc.chart for slc in self.slices] @property def sqla_metadata(self) -> None: # pylint: disable=no-member meta = MetaData(bind=self.get_sqla_engine()) meta.reflect() @renders("dashboard_title") def dashboard_link(self) -> Markup: title = escape(self.dashboard_title or "<empty>") return Markup(f'<a href="{self.url}">{title}</a>') @property def digest(self) -> str: """ Returns a MD5 HEX digest that makes this dashboard unique """ unique_string = f"{self.position_json}.{self.css}.{self.json_metadata}" return utils.md5_hex(unique_string) @property def thumbnail_url(self) -> str: """ Returns a thumbnail URL with a HEX digest. We want to avoid browser cache if the dashboard has changed """ return f"/api/v1/dashboard/{self.id}/thumbnail/{self.digest}/" @property def changed_by_name(self) -> str: if not self.changed_by: return "" return str(self.changed_by) @property def changed_by_url(self) -> str: if not self.changed_by: return "" return f"/superset/profile/{self.changed_by.username}" @property def data(self) -> Dict[str, Any]: positions = self.position_json if positions: positions = json.loads(positions) return { "id": self.id, "metadata": self.params_dict, "css": self.css, "dashboard_title": self.dashboard_title, "published": self.published, "slug": self.slug, "slices": [slc.data for slc in self.slices], "position_json": positions, "last_modified_time": self.changed_on.replace(microsecond=0).timestamp(), } @cache_manager.cache.memoize( # manage cache version manually make_name=lambda fname: f"{fname}-v2.1", unless=lambda: not is_feature_enabled("DASHBOARD_CACHE"), ) def full_data(self) -> Dict[str, Any]: """Bootstrap data for rendering the dashboard page.""" slices = self.slices datasource_slices = utils.indexed(slices, "datasource") try: datasources = { # Filter out unneeded fields from the datasource payload datasource.uid: datasource.data_for_slices(slices) for datasource, slices in datasource_slices.items() if datasource } except (SupersetException, SQLAlchemyError): datasources = {} return { # dashboard metadata "dashboard": self.data, # slices metadata "slices": [slc.data for slc in slices], # datasource metadata "datasources": datasources, } @property # type: ignore def params(self) -> str: # type: ignore return self.json_metadata @params.setter def params(self, value: str) -> None: self.json_metadata = value @property def position(self) -> Dict[str, Any]: if self.position_json: return json.loads(self.position_json) return {} def update_thumbnail(self) -> None: url = get_url_path("Superset.dashboard", dashboard_id_or_slug=self.id) cache_dashboard_thumbnail.delay(url, self.digest, force=True) @debounce(0.1) def clear_cache(self) -> None: cache_manager.cache.delete_memoized(Dashboard.full_data, self) @classmethod @debounce(0.1) def clear_cache_for_slice(cls, slice_id: int) -> None: filter_query = select( [dashboard_slices.c.dashboard_id], distinct=True).where(dashboard_slices.c.slice_id == slice_id) for (dashboard_id, ) in db.engine.execute(filter_query): cls(id=dashboard_id).clear_cache() @classmethod @debounce(0.1) def clear_cache_for_datasource(cls, datasource_id: int) -> None: filter_query = select( [dashboard_slices.c.dashboard_id], distinct=True, ).select_from( join( dashboard_slices, Slice, (Slice.id == dashboard_slices.c.slice_id) & (Slice.datasource_id == datasource_id), )) for (dashboard_id, ) in db.engine.execute(filter_query): cls(id=dashboard_id).clear_cache() @classmethod def export_dashboards( # pylint: disable=too-many-locals cls, dashboard_ids: List[int]) -> str: copied_dashboards = [] datasource_ids = set() for dashboard_id in dashboard_ids: # make sure that dashboard_id is an integer dashboard_id = int(dashboard_id) dashboard = (db.session.query(Dashboard).options( subqueryload( Dashboard.slices)).filter_by(id=dashboard_id).first()) # remove ids and relations (like owners, created by, slices, ...) copied_dashboard = dashboard.copy() for slc in dashboard.slices: datasource_ids.add((slc.datasource_id, slc.datasource_type)) copied_slc = slc.copy() # save original id into json # we need it to update dashboard's json metadata on import copied_slc.id = slc.id # add extra params for the import copied_slc.alter_params( remote_id=slc.id, datasource_name=slc.datasource.datasource_name, schema=slc.datasource.schema, database_name=slc.datasource.database.name, ) # set slices without creating ORM relations slices = copied_dashboard.__dict__.setdefault("slices", []) slices.append(copied_slc) copied_dashboard.alter_params(remote_id=dashboard_id) copied_dashboards.append(copied_dashboard) eager_datasources = [] for datasource_id, datasource_type in datasource_ids: eager_datasource = ConnectorRegistry.get_eager_datasource( db.session, datasource_type, datasource_id) copied_datasource = eager_datasource.copy() copied_datasource.alter_params( remote_id=eager_datasource.id, database_name=eager_datasource.database.name, ) datasource_class = copied_datasource.__class__ for field_name in datasource_class.export_children: field_val = getattr(eager_datasource, field_name).copy() # set children without creating ORM relations copied_datasource.__dict__[field_name] = field_val eager_datasources.append(copied_datasource) return json.dumps( { "dashboards": copied_dashboards, "datasources": eager_datasources }, cls=utils.DashboardEncoder, indent=4, ) @classmethod def get(cls, id_or_slug: str) -> Dashboard: session = db.session() qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug)) return qry.one_or_none()
class Dashboard(Model, AuditMixinNullable, ImportExportMixin): """The dashboard object!""" __tablename__ = "dashboards" id = Column(Integer, primary_key=True) dashboard_title = Column(String(500)) position_json = Column(utils.MediumText()) description = Column(Text) css = Column(Text) certified_by = Column(Text) certification_details = Column(Text) json_metadata = Column(Text) slug = Column(String(255), unique=True) slices = relationship(Slice, secondary=dashboard_slices, backref="dashboards") owners = relationship(security_manager.user_model, secondary=dashboard_user) published = Column(Boolean, default=False) is_managed_externally = Column(Boolean, nullable=False, default=False) external_url = Column(Text, nullable=True) roles = relationship(security_manager.role_model, secondary=DashboardRoles) _filter_sets = relationship("FilterSet", back_populates="dashboard", cascade="all, delete") export_fields = [ "dashboard_title", "position_json", "json_metadata", "description", "css", "slug", ] def __repr__(self) -> str: return f"Dashboard<{self.id or self.slug}>" @property def url(self) -> str: return f"/superset/dashboard/{self.slug or self.id}/" @property def datasources(self) -> Set[BaseDatasource]: # Verbose but efficient database enumeration of dashboard datasources. datasources_by_cls_model: Dict[Type["BaseDatasource"], Set[int]] = defaultdict(set) for slc in self.slices: datasources_by_cls_model[slc.cls_model].add(slc.datasource_id) return { datasource for cls_model, datasource_ids in datasources_by_cls_model.items() for datasource in db.session.query(cls_model).filter( cls_model.id.in_(datasource_ids)).all() } @property def filter_sets(self) -> Dict[int, FilterSet]: return {fs.id: fs for fs in self._filter_sets} @property def filter_sets_lst(self) -> Dict[int, FilterSet]: if is_user_admin(): return self._filter_sets current_user = g.user.id filter_sets_by_owner_type: Dict[str, List[Any]] = { "Dashboard": [], "User": [] } for fs in self._filter_sets: filter_sets_by_owner_type[fs.owner_type].append(fs) user_filter_sets = list( filter( lambda filter_set: filter_set.owner_id == current_user, filter_sets_by_owner_type["User"], )) return { fs.id: fs for fs in user_filter_sets + filter_sets_by_owner_type["Dashboard"] } @property def charts(self) -> List[BaseDatasource]: return [slc.chart for slc in self.slices] @property def sqla_metadata(self) -> None: # pylint: disable=no-member meta = MetaData(bind=self.get_sqla_engine()) meta.reflect() @property def status(self) -> utils.DashboardStatus: if self.published: return utils.DashboardStatus.PUBLISHED return utils.DashboardStatus.DRAFT @renders("dashboard_title") def dashboard_link(self) -> Markup: title = escape(self.dashboard_title or "<empty>") return Markup(f'<a href="{self.url}">{title}</a>') @property def digest(self) -> str: """ Returns a MD5 HEX digest that makes this dashboard unique """ unique_string = f"{self.position_json}.{self.css}.{self.json_metadata}" return md5_sha_from_str(unique_string) @property def thumbnail_url(self) -> str: """ Returns a thumbnail URL with a HEX digest. We want to avoid browser cache if the dashboard has changed """ return f"/api/v1/dashboard/{self.id}/thumbnail/{self.digest}/" @property def changed_by_name(self) -> str: if not self.changed_by: return "" return str(self.changed_by) @property def changed_by_url(self) -> str: if not self.changed_by: return "" return f"/superset/profile/{self.changed_by.username}" @property def data(self) -> Dict[str, Any]: positions = self.position_json if positions: positions = json.loads(positions) return { "id": self.id, "metadata": self.params_dict, "certified_by": self.certified_by, "certification_details": self.certification_details, "css": self.css, "dashboard_title": self.dashboard_title, "published": self.published, "slug": self.slug, "slices": [slc.data for slc in self.slices], "position_json": positions, "last_modified_time": self.changed_on.replace(microsecond=0).timestamp(), } @cache_manager.cache.memoize( # manage cache version manually make_name=lambda fname: f"{fname}-v1.0", unless=lambda: not is_feature_enabled("DASHBOARD_CACHE"), ) def datasets_trimmed_for_slices(self) -> List[Dict[str, Any]]: # Verbose but efficient database enumeration of dashboard datasources. slices_by_datasource: Dict[Tuple[Type["BaseDatasource"], int], Set[Slice]] = defaultdict(set) for slc in self.slices: slices_by_datasource[(slc.cls_model, slc.datasource_id)].add(slc) result: List[Dict[str, Any]] = [] for (cls_model, datasource_id), slices in slices_by_datasource.items(): datasource = (db.session.query(cls_model).filter_by( id=datasource_id).one_or_none()) if datasource: # Filter out unneeded fields from the datasource payload result.append(datasource.data_for_slices(slices)) return result @property # type: ignore def params(self) -> str: # type: ignore return self.json_metadata @params.setter def params(self, value: str) -> None: self.json_metadata = value @property def position(self) -> Dict[str, Any]: if self.position_json: return json.loads(self.position_json) return {} def update_thumbnail(self) -> None: url = get_url_path("Superset.dashboard", dashboard_id_or_slug=self.id) cache_dashboard_thumbnail.delay(url, self.digest, force=True) @debounce(0.1) def clear_cache(self) -> None: cache_manager.cache.delete_memoized( Dashboard.datasets_trimmed_for_slices, self) @classmethod @debounce(0.1) def clear_cache_for_slice(cls, slice_id: int) -> None: filter_query = select( [dashboard_slices.c.dashboard_id], distinct=True).where(dashboard_slices.c.slice_id == slice_id) for (dashboard_id, ) in db.engine.execute(filter_query): cls(id=dashboard_id).clear_cache() @classmethod @debounce(0.1) def clear_cache_for_datasource(cls, datasource_id: int) -> None: filter_query = select( [dashboard_slices.c.dashboard_id], distinct=True, ).select_from( join( dashboard_slices, Slice, (Slice.id == dashboard_slices.c.slice_id) & (Slice.datasource_id == datasource_id), )) for (dashboard_id, ) in db.engine.execute(filter_query): cls(id=dashboard_id).clear_cache() @classmethod def export_dashboards( # pylint: disable=too-many-locals cls, dashboard_ids: List[int]) -> str: copied_dashboards = [] datasource_ids = set() for dashboard_id in dashboard_ids: # make sure that dashboard_id is an integer dashboard_id = int(dashboard_id) dashboard = (db.session.query(Dashboard).options( subqueryload( Dashboard.slices)).filter_by(id=dashboard_id).first()) # remove ids and relations (like owners, created by, slices, ...) copied_dashboard = dashboard.copy() for slc in dashboard.slices: datasource_ids.add((slc.datasource_id, slc.datasource_type)) copied_slc = slc.copy() # save original id into json # we need it to update dashboard's json metadata on import copied_slc.id = slc.id # add extra params for the import copied_slc.alter_params( remote_id=slc.id, datasource_name=slc.datasource.datasource_name, schema=slc.datasource.schema, database_name=slc.datasource.database.name, ) # set slices without creating ORM relations slices = copied_dashboard.__dict__.setdefault("slices", []) slices.append(copied_slc) json_metadata = json.loads(dashboard.json_metadata) native_filter_configuration: List[Dict[ str, Any]] = json_metadata.get("native_filter_configuration", []) for native_filter in native_filter_configuration: session = db.session() for target in native_filter.get("targets", []): id_ = target.get("datasetId") if id_ is None: continue datasource = ConnectorRegistry.get_datasource_by_id( session, id_) datasource_ids.add((datasource.id, datasource.type)) copied_dashboard.alter_params(remote_id=dashboard_id) copied_dashboards.append(copied_dashboard) eager_datasources = [] for datasource_id, datasource_type in datasource_ids: eager_datasource = ConnectorRegistry.get_eager_datasource( db.session, datasource_type, datasource_id) copied_datasource = eager_datasource.copy() copied_datasource.alter_params( remote_id=eager_datasource.id, database_name=eager_datasource.database.name, ) datasource_class = copied_datasource.__class__ for field_name in datasource_class.export_children: field_val = getattr(eager_datasource, field_name).copy() # set children without creating ORM relations copied_datasource.__dict__[field_name] = field_val eager_datasources.append(copied_datasource) return json.dumps( { "dashboards": copied_dashboards, "datasources": eager_datasources }, cls=utils.DashboardEncoder, indent=4, ) @classmethod def get(cls, id_or_slug: str) -> Dashboard: session = db.session() qry = session.query(Dashboard).filter(id_or_slug_filter(id_or_slug)) return qry.one_or_none() def is_actor_owner(self) -> bool: if g.user is None or g.user.is_anonymous or not g.user.is_authenticated: return False return g.user.id in set(map(lambda user: user.id, self.owners))