123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- # Licensed to the Apache Software Foundation (ASF) under one
- # or more contributor license agreements. See the NOTICE file
- # distributed with this work for additional information
- # regarding copyright ownership. The ASF licenses this file
- # to you under the Apache License, Version 2.0 (the
- # "License"); you may not use this file except in compliance
- # with the License. You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing,
- # software distributed under the License is distributed on an
- # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- # KIND, either express or implied. See the License for the
- # specific language governing permissions and limitations
- # under the License.
- import json
- import logging
- from typing import Any, Dict, Optional, Type, TYPE_CHECKING
- from urllib import parse
- import sqlalchemy as sqla
- from flask_appbuilder import Model
- from flask_appbuilder.models.decorators import renders
- from markupsafe import escape, Markup
- from sqlalchemy import Column, ForeignKey, Integer, String, Table, Text
- from sqlalchemy.orm import make_transient, relationship
- from superset import ConnectorRegistry, db, is_feature_enabled, security_manager
- from superset.legacy import update_time_range
- from superset.models.helpers import AuditMixinNullable, ImportMixin
- from superset.models.tags import ChartUpdater
- from superset.utils import core as utils
- from superset.viz import BaseViz, viz_types
- if TYPE_CHECKING:
- # pylint: disable=unused-import
- from superset.connectors.base.models import BaseDatasource
- metadata = Model.metadata # pylint: disable=no-member
- slice_user = Table(
- "slice_user",
- metadata,
- Column("id", Integer, primary_key=True),
- Column("user_id", Integer, ForeignKey("ab_user.id")),
- Column("slice_id", Integer, ForeignKey("slices.id")),
- )
- logger = logging.getLogger(__name__)
- class Slice(
- Model, AuditMixinNullable, ImportMixin
- ): # pylint: disable=too-many-public-methods
- """A slice is essentially a report or a view on data"""
- __tablename__ = "slices"
- id = Column(Integer, primary_key=True) # pylint: disable=invalid-name
- slice_name = Column(String(250))
- datasource_id = Column(Integer)
- datasource_type = Column(String(200))
- datasource_name = Column(String(2000))
- viz_type = Column(String(250))
- params = Column(Text)
- description = Column(Text)
- cache_timeout = Column(Integer)
- perm = Column(String(1000))
- schema_perm = Column(String(1000))
- owners = relationship(security_manager.user_model, secondary=slice_user)
- token = ""
- export_fields = [
- "slice_name",
- "datasource_type",
- "datasource_name",
- "viz_type",
- "params",
- "cache_timeout",
- ]
- def __repr__(self):
- return self.slice_name or str(self.id)
- @property
- def cls_model(self) -> Type["BaseDatasource"]:
- return ConnectorRegistry.sources[self.datasource_type]
- @property
- def datasource(self) -> "BaseDatasource":
- return self.get_datasource
- def clone(self) -> "Slice":
- return Slice(
- slice_name=self.slice_name,
- datasource_id=self.datasource_id,
- datasource_type=self.datasource_type,
- datasource_name=self.datasource_name,
- viz_type=self.viz_type,
- params=self.params,
- description=self.description,
- cache_timeout=self.cache_timeout,
- )
- # pylint: disable=using-constant-test
- @datasource.getter # type: ignore
- @utils.memoized
- def get_datasource(self) -> Optional["BaseDatasource"]:
- return db.session.query(self.cls_model).filter_by(id=self.datasource_id).first()
- @renders("datasource_name")
- def datasource_link(self) -> Optional[Markup]:
- # pylint: disable=no-member
- datasource = self.datasource
- return datasource.link if datasource else None
- def datasource_name_text(self) -> Optional[str]:
- # pylint: disable=no-member
- datasource = self.datasource
- return datasource.name if datasource else None
- @property
- def datasource_edit_url(self) -> Optional[str]:
- # pylint: disable=no-member
- datasource = self.datasource
- return datasource.url if datasource else None
- # pylint: enable=using-constant-test
- @property # type: ignore
- @utils.memoized
- def viz(self) -> BaseViz:
- d = json.loads(self.params)
- viz_class = viz_types[self.viz_type]
- return viz_class(datasource=self.datasource, form_data=d)
- @property
- def description_markeddown(self) -> str:
- return utils.markdown(self.description)
- @property
- def data(self) -> Dict[str, Any]:
- """Data used to render slice in templates"""
- d: Dict[str, Any] = {}
- self.token = ""
- try:
- d = self.viz.data
- self.token = d.get("token") # type: ignore
- except Exception as e: # pylint: disable=broad-except
- logger.exception(e)
- d["error"] = str(e)
- return {
- "cache_timeout": self.cache_timeout,
- "datasource": self.datasource_name,
- "description": self.description,
- "description_markeddown": self.description_markeddown,
- "edit_url": self.edit_url,
- "form_data": self.form_data,
- "slice_id": self.id,
- "slice_name": self.slice_name,
- "slice_url": self.slice_url,
- "modified": self.modified(),
- "changed_on_humanized": self.changed_on_humanized,
- "changed_on": self.changed_on.isoformat(),
- }
- @property
- def json_data(self) -> str:
- return json.dumps(self.data)
- @property
- def form_data(self) -> Dict[str, Any]:
- form_data: Dict[str, Any] = {}
- try:
- form_data = json.loads(self.params)
- except Exception as e: # pylint: disable=broad-except
- logger.error("Malformed json in slice's params")
- logger.exception(e)
- form_data.update(
- {
- "slice_id": self.id,
- "viz_type": self.viz_type,
- "datasource": "{}__{}".format(self.datasource_id, self.datasource_type),
- }
- )
- if self.cache_timeout:
- form_data["cache_timeout"] = self.cache_timeout
- update_time_range(form_data)
- return form_data
- def get_explore_url(
- self,
- base_url: str = "/superset/explore",
- overrides: Optional[Dict[str, Any]] = None,
- ) -> str:
- overrides = overrides or {}
- form_data = {"slice_id": self.id}
- form_data.update(overrides)
- params = parse.quote(json.dumps(form_data))
- return f"{base_url}/?form_data={params}"
- @property
- def slice_url(self) -> str:
- """Defines the url to access the slice"""
- return self.get_explore_url()
- @property
- def explore_json_url(self) -> str:
- """Defines the url to access the slice"""
- return self.get_explore_url("/superset/explore_json")
- @property
- def edit_url(self) -> str:
- return f"/chart/edit/{self.id}"
- @property
- def chart(self) -> str:
- return self.slice_name or "<empty>"
- @property
- def slice_link(self) -> Markup:
- name = escape(self.chart)
- return Markup(f'<a href="{self.url}">{name}</a>')
- @property
- def changed_by_url(self) -> str:
- return f"/superset/profile/{self.created_by.username}"
- def get_viz(self, force: bool = False) -> BaseViz:
- """Creates :py:class:viz.BaseViz object from the url_params_multidict.
- :return: object of the 'viz_type' type that is taken from the
- url_params_multidict or self.params.
- :rtype: :py:class:viz.BaseViz
- """
- slice_params = json.loads(self.params)
- slice_params["slice_id"] = self.id
- slice_params["json"] = "false"
- slice_params["slice_name"] = self.slice_name
- slice_params["viz_type"] = self.viz_type if self.viz_type else "table"
- return viz_types[slice_params.get("viz_type")](
- self.datasource, form_data=slice_params, force=force
- )
- @property
- def icons(self) -> str:
- return f"""
- <a
- href="{self.datasource_edit_url}"
- data-toggle="tooltip"
- title="{self.datasource}">
- <i class="fa fa-database"></i>
- </a>
- """
- @classmethod
- def import_obj(
- cls,
- slc_to_import: "Slice",
- slc_to_override: Optional["Slice"],
- import_time: Optional[int] = None,
- ) -> int:
- """Inserts or overrides slc in the database.
- remote_id and import_time fields in params_dict are set to track the
- slice origin and ensure correct overrides for multiple imports.
- Slice.perm is used to find the datasources and connect them.
- :param Slice slc_to_import: Slice object to import
- :param Slice slc_to_override: Slice to replace, id matches remote_id
- :returns: The resulting id for the imported slice
- :rtype: int
- """
- session = db.session
- make_transient(slc_to_import)
- slc_to_import.dashboards = []
- slc_to_import.alter_params(remote_id=slc_to_import.id, import_time=import_time)
- slc_to_import = slc_to_import.copy()
- slc_to_import.reset_ownership()
- params = slc_to_import.params_dict
- datasource = ConnectorRegistry.get_datasource_by_name(
- session,
- slc_to_import.datasource_type,
- params["datasource_name"],
- params["schema"],
- params["database_name"],
- )
- slc_to_import.datasource_id = datasource.id # type: ignore
- if slc_to_override:
- slc_to_override.override(slc_to_import)
- session.flush()
- return slc_to_override.id
- session.add(slc_to_import)
- logger.info("Final slice: %s", str(slc_to_import.to_json()))
- session.flush()
- return slc_to_import.id
- @property
- def url(self) -> str:
- return f"/superset/explore/?form_data=%7B%22slice_id%22%3A%20{self.id}%7D"
- def set_related_perm(mapper, connection, target):
- # pylint: disable=unused-argument
- src_class = target.cls_model
- id_ = target.datasource_id
- if id_:
- ds = db.session.query(src_class).filter_by(id=int(id_)).first()
- if ds:
- target.perm = ds.perm
- target.schema_perm = ds.schema_perm
- sqla.event.listen(Slice, "before_insert", set_related_perm)
- sqla.event.listen(Slice, "before_update", set_related_perm)
- # events for updating tags
- if is_feature_enabled("TAGGING_SYSTEM"):
- sqla.event.listen(Slice, "after_insert", ChartUpdater.after_insert)
- sqla.event.listen(Slice, "after_update", ChartUpdater.after_update)
- sqla.event.listen(Slice, "after_delete", ChartUpdater.after_delete)
|