views.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. # pylint: disable=C,R,W
  2. """Views used by the SqlAlchemy connector"""
  3. from flask import flash, Markup, redirect
  4. from flask_appbuilder import CompactCRUDMixin, expose
  5. from flask_appbuilder.actions import action
  6. from flask_appbuilder.models.sqla.interface import SQLAInterface
  7. from flask_appbuilder.security.decorators import has_access
  8. from flask_babel import gettext as __
  9. from flask_babel import lazy_gettext as _
  10. from past.builtins import basestring
  11. from superset import appbuilder, db, security_manager, utils
  12. from superset.connectors.base.views import DatasourceModelView
  13. from superset.views.base import (
  14. DatasourceFilter, DeleteMixin, get_datasource_exist_error_msg,
  15. ListWidgetWithCheckboxes, SupersetModelView, YamlExportMixin,
  16. )
  17. from . import models
  18. class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
  19. datamodel = SQLAInterface(models.TableColumn)
  20. list_title = _('List Columns')
  21. show_title = _('Show Column')
  22. add_title = _('Add Column')
  23. edit_title = _('Edit Column')
  24. can_delete = False
  25. list_widget = ListWidgetWithCheckboxes
  26. edit_columns = [
  27. 'column_name', 'verbose_name', 'description',
  28. 'type', 'groupby', 'filterable',
  29. 'table', 'expression',
  30. 'is_dttm', 'python_date_format', 'database_expression']
  31. add_columns = edit_columns
  32. list_columns = [
  33. 'column_name', 'verbose_name', 'type', 'groupby', 'filterable',
  34. 'is_dttm']
  35. page_size = 500
  36. description_columns = {
  37. 'is_dttm': _(
  38. 'Whether to make this column available as a '
  39. '[Time Granularity] option, column has to be DATETIME or '
  40. 'DATETIME-like'),
  41. 'filterable': _(
  42. 'Whether this column is exposed in the `Filters` section '
  43. 'of the explore view.'),
  44. 'type': _(
  45. 'The data type that was inferred by the database. '
  46. 'It may be necessary to input a type manually for '
  47. 'expression-defined columns in some cases. In most case '
  48. 'users should not need to alter this.'),
  49. 'expression': utils.markdown(
  50. 'a valid, *non-aggregating* SQL expression as supported by the '
  51. 'underlying backend. Example: `substr(name, 1, 1)`', True),
  52. 'python_date_format': utils.markdown(Markup(
  53. 'The pattern of timestamp format, use '
  54. '<a href="https://docs.python.org/2/library/'
  55. 'datetime.html#strftime-strptime-behavior">'
  56. 'python datetime string pattern</a> '
  57. 'expression. If time is stored in epoch '
  58. 'format, put `epoch_s` or `epoch_ms`. Leave `Database Expression` '
  59. 'below empty if timestamp is stored in '
  60. 'String or Integer(epoch) type'), True),
  61. 'database_expression': utils.markdown(
  62. 'The database expression to cast internal datetime '
  63. 'constants to database date/timestamp type according to the DBAPI. '
  64. 'The expression should follow the pattern of '
  65. '%Y-%m-%d %H:%M:%S, based on different DBAPI. '
  66. 'The string should be a python string formatter \n'
  67. "`Ex: TO_DATE('{}', 'YYYY-MM-DD HH24:MI:SS')` for Oracle "
  68. 'Superset uses default expression based on DB URI if this '
  69. 'field is blank.', True),
  70. }
  71. label_columns = {
  72. 'column_name': _('Column'),
  73. 'verbose_name': _('Verbose Name'),
  74. 'description': _('Description'),
  75. 'groupby': _('Groupable'),
  76. 'filterable': _('Filterable'),
  77. 'table': _('Table'),
  78. 'expression': _('Expression'),
  79. 'is_dttm': _('Is temporal'),
  80. 'python_date_format': _('Datetime Format'),
  81. 'database_expression': _('Database Expression'),
  82. 'type': _('Type'),
  83. }
  84. appbuilder.add_view_no_menu(TableColumnInlineView)
  85. class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
  86. datamodel = SQLAInterface(models.SqlMetric)
  87. list_title = _('List Metrics')
  88. show_title = _('Show Metric')
  89. add_title = _('Add Metric')
  90. edit_title = _('Edit Metric')
  91. list_columns = ['metric_name', 'verbose_name', 'metric_type']
  92. edit_columns = [
  93. 'metric_name', 'description', 'verbose_name', 'metric_type',
  94. 'expression', 'table', 'd3format', 'is_restricted', 'warning_text']
  95. description_columns = {
  96. 'expression': utils.markdown(
  97. 'a valid, *aggregating* SQL expression as supported by the '
  98. 'underlying backend. Example: `count(DISTINCT userid)`', True),
  99. 'is_restricted': _('Whether the access to this metric is restricted '
  100. 'to certain roles. Only roles with the permission '
  101. "'metric access on XXX (the name of this metric)' "
  102. 'are allowed to access this metric'),
  103. 'd3format': utils.markdown(
  104. 'd3 formatting string as defined [here]'
  105. '(https://github.com/d3/d3-format/blob/master/README.md#format). '
  106. 'For instance, this default formatting applies in the Table '
  107. 'visualization and allow for different metric to use different '
  108. 'formats', True,
  109. ),
  110. }
  111. add_columns = edit_columns
  112. page_size = 500
  113. label_columns = {
  114. 'metric_name': _('Metric'),
  115. 'description': _('Description'),
  116. 'verbose_name': _('Verbose Name'),
  117. 'metric_type': _('Type'),
  118. 'expression': _('SQL Expression'),
  119. 'table': _('Table'),
  120. 'd3format': _('D3 Format'),
  121. 'is_restricted': _('Is Restricted'),
  122. 'warning_text': _('Warning Message'),
  123. }
  124. def post_add(self, metric):
  125. if metric.is_restricted:
  126. security_manager.merge_perm('metric_access', metric.get_perm())
  127. def post_update(self, metric):
  128. if metric.is_restricted:
  129. security_manager.merge_perm('metric_access', metric.get_perm())
  130. appbuilder.add_view_no_menu(SqlMetricInlineView)
  131. class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
  132. datamodel = SQLAInterface(models.SqlaTable)
  133. list_title = _('List Tables')
  134. show_title = _('Show Table')
  135. add_title = _('Import a table definition')
  136. edit_title = _('Edit Table')
  137. list_columns = [
  138. 'link', 'database_name',
  139. 'changed_by_', 'modified']
  140. order_columns = ['modified']
  141. add_columns = ['database', 'schema', 'table_name']
  142. edit_columns = [
  143. 'table_name', 'sql', 'filter_select_enabled',
  144. 'fetch_values_predicate', 'database', 'schema',
  145. 'description', 'owner',
  146. 'main_dttm_col', 'default_endpoint', 'offset', 'cache_timeout',
  147. 'is_sqllab_view', 'template_params',
  148. ]
  149. base_filters = [['id', DatasourceFilter, lambda: []]]
  150. show_columns = edit_columns + ['perm', 'slices']
  151. related_views = [TableColumnInlineView, SqlMetricInlineView]
  152. base_order = ('changed_on', 'desc')
  153. search_columns = (
  154. 'database', 'schema', 'table_name', 'owner', 'is_sqllab_view',
  155. )
  156. description_columns = {
  157. 'slices': _(
  158. 'The list of charts associated with this table. By '
  159. 'altering this datasource, you may change how these associated '
  160. 'charts behave. '
  161. 'Also note that charts need to point to a datasource, so '
  162. 'this form will fail at saving if removing charts from a '
  163. 'datasource. If you want to change the datasource for a chart, '
  164. "overwrite the chart from the 'explore view'"),
  165. 'offset': _('Timezone offset (in hours) for this datasource'),
  166. 'table_name': _(
  167. 'Name of the table that exists in the source database'),
  168. 'schema': _(
  169. 'Schema, as used only in some databases like Postgres, Redshift '
  170. 'and DB2'),
  171. 'description': Markup(
  172. 'Supports <a href="https://daringfireball.net/projects/markdown/">'
  173. 'markdown</a>'),
  174. 'sql': _(
  175. 'This fields acts a Superset view, meaning that Superset will '
  176. 'run a query against this string as a subquery.',
  177. ),
  178. 'fetch_values_predicate': _(
  179. 'Predicate applied when fetching distinct value to '
  180. 'populate the filter control component. Supports '
  181. 'jinja template syntax. Applies only when '
  182. '`Enable Filter Select` is on.',
  183. ),
  184. 'default_endpoint': _(
  185. 'Redirects to this endpoint when clicking on the table '
  186. 'from the table list'),
  187. 'filter_select_enabled': _(
  188. "Whether to populate the filter's dropdown in the explore "
  189. "view's filter section with a list of distinct values fetched "
  190. 'from the backend on the fly'),
  191. 'is_sqllab_view': _(
  192. "Whether the table was generated by the 'Visualize' flow "
  193. 'in SQL Lab'),
  194. 'template_params': _(
  195. 'A set of parameters that become available in the query using '
  196. 'Jinja templating syntax'),
  197. 'cache_timeout': _(
  198. 'Duration (in seconds) of the caching timeout for this table. '
  199. 'A timeout of 0 indicates that the cache never expires. '
  200. 'Note this defaults to the database timeout if undefined.'),
  201. }
  202. label_columns = {
  203. 'slices': _('Associated Charts'),
  204. 'link': _('Table'),
  205. 'changed_by_': _('Changed By'),
  206. 'database': _('Database'),
  207. 'database_name': _('Database'),
  208. 'changed_on_': _('Last Changed'),
  209. 'filter_select_enabled': _('Enable Filter Select'),
  210. 'schema': _('Schema'),
  211. 'default_endpoint': _('Default Endpoint'),
  212. 'offset': _('Offset'),
  213. 'cache_timeout': _('Cache Timeout'),
  214. 'table_name': _('Table Name'),
  215. 'fetch_values_predicate': _('Fetch Values Predicate'),
  216. 'owner': _('Owner'),
  217. 'main_dttm_col': _('Main Datetime Column'),
  218. 'description': _('Description'),
  219. 'is_sqllab_view': _('SQL Lab View'),
  220. 'template_params': _('Template parameters'),
  221. 'modified': _('Modified'),
  222. }
  223. def pre_add(self, table):
  224. with db.session.no_autoflush:
  225. table_query = db.session.query(models.SqlaTable).filter(
  226. models.SqlaTable.table_name == table.table_name,
  227. models.SqlaTable.schema == table.schema,
  228. models.SqlaTable.database_id == table.database.id)
  229. if db.session.query(table_query.exists()).scalar():
  230. raise Exception(
  231. get_datasource_exist_error_msg(table.full_name))
  232. # Fail before adding if the table can't be found
  233. try:
  234. table.get_sqla_table_object()
  235. except Exception:
  236. raise Exception(_(
  237. 'Table [{}] could not be found, '
  238. 'please double check your '
  239. 'database connection, schema, and '
  240. 'table name').format(table.name))
  241. def post_add(self, table, flash_message=True):
  242. table.fetch_metadata()
  243. security_manager.merge_perm('datasource_access', table.get_perm())
  244. if table.schema:
  245. security_manager.merge_perm('schema_access', table.schema_perm)
  246. if flash_message:
  247. flash(_(
  248. 'The table was created. '
  249. 'As part of this two phase configuration '
  250. 'process, you should now click the edit button by '
  251. 'the new table to configure it.'), 'info')
  252. def post_update(self, table):
  253. self.post_add(table, flash_message=False)
  254. def _delete(self, pk):
  255. DeleteMixin._delete(self, pk)
  256. @expose('/edit/<pk>', methods=['GET', 'POST'])
  257. @has_access
  258. def edit(self, pk):
  259. """Simple hack to redirect to explore view after saving"""
  260. resp = super(TableModelView, self).edit(pk)
  261. if isinstance(resp, basestring):
  262. return resp
  263. return redirect('/superset/explore/table/{}/'.format(pk))
  264. @action(
  265. 'refresh',
  266. __('Refresh Metadata'),
  267. __('Refresh column metadata'),
  268. 'fa-refresh')
  269. def refresh(self, tables):
  270. if not isinstance(tables, list):
  271. tables = [tables]
  272. successes = []
  273. failures = []
  274. for t in tables:
  275. try:
  276. t.fetch_metadata()
  277. successes.append(t)
  278. except Exception:
  279. failures.append(t)
  280. if len(successes) > 0:
  281. success_msg = _(
  282. 'Metadata refreshed for the following table(s): %(tables)s',
  283. tables=', '.join([t.table_name for t in successes]))
  284. flash(success_msg, 'info')
  285. if len(failures) > 0:
  286. failure_msg = _(
  287. 'Unable to retrieve metadata for the following table(s): %(tables)s',
  288. tables=', '.join([t.table_name for t in failures]))
  289. flash(failure_msg, 'danger')
  290. return redirect('/tablemodelview/list/')
  291. appbuilder.add_view_no_menu(TableModelView)
  292. appbuilder.add_link(
  293. 'Tables',
  294. label=__('Tables'),
  295. href='/tablemodelview/list/?_flt_1_is_sqllab_view=y',
  296. icon='fa-table',
  297. category='Sources',
  298. category_label=__('Sources'),
  299. category_icon='fa-table')
  300. appbuilder.add_separator('Sources')