slice.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. import json
  18. import logging
  19. from typing import Any, Dict, Optional, Type, TYPE_CHECKING
  20. from urllib import parse
  21. import sqlalchemy as sqla
  22. from flask_appbuilder import Model
  23. from flask_appbuilder.models.decorators import renders
  24. from markupsafe import escape, Markup
  25. from sqlalchemy import Column, ForeignKey, Integer, String, Table, Text
  26. from sqlalchemy.orm import make_transient, relationship
  27. from superset import ConnectorRegistry, db, is_feature_enabled, security_manager
  28. from superset.legacy import update_time_range
  29. from superset.models.helpers import AuditMixinNullable, ImportMixin
  30. from superset.models.tags import ChartUpdater
  31. from superset.utils import core as utils
  32. from superset.viz import BaseViz, viz_types
  33. if TYPE_CHECKING:
  34. # pylint: disable=unused-import
  35. from superset.connectors.base.models import BaseDatasource
  36. metadata = Model.metadata # pylint: disable=no-member
  37. slice_user = Table(
  38. "slice_user",
  39. metadata,
  40. Column("id", Integer, primary_key=True),
  41. Column("user_id", Integer, ForeignKey("ab_user.id")),
  42. Column("slice_id", Integer, ForeignKey("slices.id")),
  43. )
  44. logger = logging.getLogger(__name__)
  45. class Slice(
  46. Model, AuditMixinNullable, ImportMixin
  47. ): # pylint: disable=too-many-public-methods
  48. """A slice is essentially a report or a view on data"""
  49. __tablename__ = "slices"
  50. id = Column(Integer, primary_key=True) # pylint: disable=invalid-name
  51. slice_name = Column(String(250))
  52. datasource_id = Column(Integer)
  53. datasource_type = Column(String(200))
  54. datasource_name = Column(String(2000))
  55. viz_type = Column(String(250))
  56. params = Column(Text)
  57. description = Column(Text)
  58. cache_timeout = Column(Integer)
  59. perm = Column(String(1000))
  60. schema_perm = Column(String(1000))
  61. owners = relationship(security_manager.user_model, secondary=slice_user)
  62. token = ""
  63. export_fields = [
  64. "slice_name",
  65. "datasource_type",
  66. "datasource_name",
  67. "viz_type",
  68. "params",
  69. "cache_timeout",
  70. ]
  71. def __repr__(self):
  72. return self.slice_name or str(self.id)
  73. @property
  74. def cls_model(self) -> Type["BaseDatasource"]:
  75. return ConnectorRegistry.sources[self.datasource_type]
  76. @property
  77. def datasource(self) -> "BaseDatasource":
  78. return self.get_datasource
  79. def clone(self) -> "Slice":
  80. return Slice(
  81. slice_name=self.slice_name,
  82. datasource_id=self.datasource_id,
  83. datasource_type=self.datasource_type,
  84. datasource_name=self.datasource_name,
  85. viz_type=self.viz_type,
  86. params=self.params,
  87. description=self.description,
  88. cache_timeout=self.cache_timeout,
  89. )
  90. # pylint: disable=using-constant-test
  91. @datasource.getter # type: ignore
  92. @utils.memoized
  93. def get_datasource(self) -> Optional["BaseDatasource"]:
  94. return db.session.query(self.cls_model).filter_by(id=self.datasource_id).first()
  95. @renders("datasource_name")
  96. def datasource_link(self) -> Optional[Markup]:
  97. # pylint: disable=no-member
  98. datasource = self.datasource
  99. return datasource.link if datasource else None
  100. def datasource_name_text(self) -> Optional[str]:
  101. # pylint: disable=no-member
  102. datasource = self.datasource
  103. return datasource.name if datasource else None
  104. @property
  105. def datasource_edit_url(self) -> Optional[str]:
  106. # pylint: disable=no-member
  107. datasource = self.datasource
  108. return datasource.url if datasource else None
  109. # pylint: enable=using-constant-test
  110. @property # type: ignore
  111. @utils.memoized
  112. def viz(self) -> BaseViz:
  113. d = json.loads(self.params)
  114. viz_class = viz_types[self.viz_type]
  115. return viz_class(datasource=self.datasource, form_data=d)
  116. @property
  117. def description_markeddown(self) -> str:
  118. return utils.markdown(self.description)
  119. @property
  120. def data(self) -> Dict[str, Any]:
  121. """Data used to render slice in templates"""
  122. d: Dict[str, Any] = {}
  123. self.token = ""
  124. try:
  125. d = self.viz.data
  126. self.token = d.get("token") # type: ignore
  127. except Exception as e: # pylint: disable=broad-except
  128. logger.exception(e)
  129. d["error"] = str(e)
  130. return {
  131. "cache_timeout": self.cache_timeout,
  132. "datasource": self.datasource_name,
  133. "description": self.description,
  134. "description_markeddown": self.description_markeddown,
  135. "edit_url": self.edit_url,
  136. "form_data": self.form_data,
  137. "slice_id": self.id,
  138. "slice_name": self.slice_name,
  139. "slice_url": self.slice_url,
  140. "modified": self.modified(),
  141. "changed_on_humanized": self.changed_on_humanized,
  142. "changed_on": self.changed_on.isoformat(),
  143. }
  144. @property
  145. def json_data(self) -> str:
  146. return json.dumps(self.data)
  147. @property
  148. def form_data(self) -> Dict[str, Any]:
  149. form_data: Dict[str, Any] = {}
  150. try:
  151. form_data = json.loads(self.params)
  152. except Exception as e: # pylint: disable=broad-except
  153. logger.error("Malformed json in slice's params")
  154. logger.exception(e)
  155. form_data.update(
  156. {
  157. "slice_id": self.id,
  158. "viz_type": self.viz_type,
  159. "datasource": "{}__{}".format(self.datasource_id, self.datasource_type),
  160. }
  161. )
  162. if self.cache_timeout:
  163. form_data["cache_timeout"] = self.cache_timeout
  164. update_time_range(form_data)
  165. return form_data
  166. def get_explore_url(
  167. self,
  168. base_url: str = "/superset/explore",
  169. overrides: Optional[Dict[str, Any]] = None,
  170. ) -> str:
  171. overrides = overrides or {}
  172. form_data = {"slice_id": self.id}
  173. form_data.update(overrides)
  174. params = parse.quote(json.dumps(form_data))
  175. return f"{base_url}/?form_data={params}"
  176. @property
  177. def slice_url(self) -> str:
  178. """Defines the url to access the slice"""
  179. return self.get_explore_url()
  180. @property
  181. def explore_json_url(self) -> str:
  182. """Defines the url to access the slice"""
  183. return self.get_explore_url("/superset/explore_json")
  184. @property
  185. def edit_url(self) -> str:
  186. return f"/chart/edit/{self.id}"
  187. @property
  188. def chart(self) -> str:
  189. return self.slice_name or "<empty>"
  190. @property
  191. def slice_link(self) -> Markup:
  192. name = escape(self.chart)
  193. return Markup(f'<a href="{self.url}">{name}</a>')
  194. @property
  195. def changed_by_url(self) -> str:
  196. return f"/superset/profile/{self.created_by.username}"
  197. def get_viz(self, force: bool = False) -> BaseViz:
  198. """Creates :py:class:viz.BaseViz object from the url_params_multidict.
  199. :return: object of the 'viz_type' type that is taken from the
  200. url_params_multidict or self.params.
  201. :rtype: :py:class:viz.BaseViz
  202. """
  203. slice_params = json.loads(self.params)
  204. slice_params["slice_id"] = self.id
  205. slice_params["json"] = "false"
  206. slice_params["slice_name"] = self.slice_name
  207. slice_params["viz_type"] = self.viz_type if self.viz_type else "table"
  208. return viz_types[slice_params.get("viz_type")](
  209. self.datasource, form_data=slice_params, force=force
  210. )
  211. @property
  212. def icons(self) -> str:
  213. return f"""
  214. <a
  215. href="{self.datasource_edit_url}"
  216. data-toggle="tooltip"
  217. title="{self.datasource}">
  218. <i class="fa fa-database"></i>
  219. </a>
  220. """
  221. @classmethod
  222. def import_obj(
  223. cls,
  224. slc_to_import: "Slice",
  225. slc_to_override: Optional["Slice"],
  226. import_time: Optional[int] = None,
  227. ) -> int:
  228. """Inserts or overrides slc in the database.
  229. remote_id and import_time fields in params_dict are set to track the
  230. slice origin and ensure correct overrides for multiple imports.
  231. Slice.perm is used to find the datasources and connect them.
  232. :param Slice slc_to_import: Slice object to import
  233. :param Slice slc_to_override: Slice to replace, id matches remote_id
  234. :returns: The resulting id for the imported slice
  235. :rtype: int
  236. """
  237. session = db.session
  238. make_transient(slc_to_import)
  239. slc_to_import.dashboards = []
  240. slc_to_import.alter_params(remote_id=slc_to_import.id, import_time=import_time)
  241. slc_to_import = slc_to_import.copy()
  242. slc_to_import.reset_ownership()
  243. params = slc_to_import.params_dict
  244. datasource = ConnectorRegistry.get_datasource_by_name(
  245. session,
  246. slc_to_import.datasource_type,
  247. params["datasource_name"],
  248. params["schema"],
  249. params["database_name"],
  250. )
  251. slc_to_import.datasource_id = datasource.id # type: ignore
  252. if slc_to_override:
  253. slc_to_override.override(slc_to_import)
  254. session.flush()
  255. return slc_to_override.id
  256. session.add(slc_to_import)
  257. logger.info("Final slice: %s", str(slc_to_import.to_json()))
  258. session.flush()
  259. return slc_to_import.id
  260. @property
  261. def url(self) -> str:
  262. return f"/superset/explore/?form_data=%7B%22slice_id%22%3A%20{self.id}%7D"
  263. def set_related_perm(mapper, connection, target):
  264. # pylint: disable=unused-argument
  265. src_class = target.cls_model
  266. id_ = target.datasource_id
  267. if id_:
  268. ds = db.session.query(src_class).filter_by(id=int(id_)).first()
  269. if ds:
  270. target.perm = ds.perm
  271. target.schema_perm = ds.schema_perm
  272. sqla.event.listen(Slice, "before_insert", set_related_perm)
  273. sqla.event.listen(Slice, "before_update", set_related_perm)
  274. # events for updating tags
  275. if is_feature_enabled("TAGGING_SYSTEM"):
  276. sqla.event.listen(Slice, "after_insert", ChartUpdater.after_insert)
  277. sqla.event.listen(Slice, "after_update", ChartUpdater.after_update)
  278. sqla.event.listen(Slice, "after_delete", ChartUpdater.after_delete)