123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- # pylint: disable=C,R,W
- """Views used by the SqlAlchemy connector"""
- from flask import flash, Markup, redirect
- from flask_appbuilder import CompactCRUDMixin, expose
- from flask_appbuilder.actions import action
- from flask_appbuilder.models.sqla.interface import SQLAInterface
- from flask_appbuilder.security.decorators import has_access
- from flask_babel import gettext as __
- from flask_babel import lazy_gettext as _
- from past.builtins import basestring
- from superset import appbuilder, db, security_manager, utils
- from superset.connectors.base.views import DatasourceModelView
- from superset.views.base import (
- DatasourceFilter, DeleteMixin, get_datasource_exist_error_msg,
- ListWidgetWithCheckboxes, SupersetModelView, YamlExportMixin,
- )
- from . import models
- class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
- datamodel = SQLAInterface(models.TableColumn)
- list_title = _('List Columns')
- show_title = _('Show Column')
- add_title = _('Add Column')
- edit_title = _('Edit Column')
- can_delete = False
- list_widget = ListWidgetWithCheckboxes
- edit_columns = [
- 'column_name', 'verbose_name', 'description',
- 'type', 'groupby', 'filterable',
- 'table', 'expression',
- 'is_dttm', 'python_date_format', 'database_expression']
- add_columns = edit_columns
- list_columns = [
- 'column_name', 'verbose_name', 'type', 'groupby', 'filterable',
- 'is_dttm']
- page_size = 500
- description_columns = {
- 'is_dttm': _(
- 'Whether to make this column available as a '
- '[Time Granularity] option, column has to be DATETIME or '
- 'DATETIME-like'),
- 'filterable': _(
- 'Whether this column is exposed in the `Filters` section '
- 'of the explore view.'),
- 'type': _(
- 'The data type that was inferred by the database. '
- 'It may be necessary to input a type manually for '
- 'expression-defined columns in some cases. In most case '
- 'users should not need to alter this.'),
- 'expression': utils.markdown(
- 'a valid, *non-aggregating* SQL expression as supported by the '
- 'underlying backend. Example: `substr(name, 1, 1)`', True),
- 'python_date_format': utils.markdown(Markup(
- 'The pattern of timestamp format, use '
- '<a href="https://docs.python.org/2/library/'
- 'datetime.html#strftime-strptime-behavior">'
- 'python datetime string pattern</a> '
- 'expression. If time is stored in epoch '
- 'format, put `epoch_s` or `epoch_ms`. Leave `Database Expression` '
- 'below empty if timestamp is stored in '
- 'String or Integer(epoch) type'), True),
- 'database_expression': utils.markdown(
- 'The database expression to cast internal datetime '
- 'constants to database date/timestamp type according to the DBAPI. '
- 'The expression should follow the pattern of '
- '%Y-%m-%d %H:%M:%S, based on different DBAPI. '
- 'The string should be a python string formatter \n'
- "`Ex: TO_DATE('{}', 'YYYY-MM-DD HH24:MI:SS')` for Oracle "
- 'Superset uses default expression based on DB URI if this '
- 'field is blank.', True),
- }
- label_columns = {
- 'column_name': _('Column'),
- 'verbose_name': _('Verbose Name'),
- 'description': _('Description'),
- 'groupby': _('Groupable'),
- 'filterable': _('Filterable'),
- 'table': _('Table'),
- 'expression': _('Expression'),
- 'is_dttm': _('Is temporal'),
- 'python_date_format': _('Datetime Format'),
- 'database_expression': _('Database Expression'),
- 'type': _('Type'),
- }
- appbuilder.add_view_no_menu(TableColumnInlineView)
- class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
- datamodel = SQLAInterface(models.SqlMetric)
- list_title = _('List Metrics')
- show_title = _('Show Metric')
- add_title = _('Add Metric')
- edit_title = _('Edit Metric')
- list_columns = ['metric_name', 'verbose_name', 'metric_type']
- edit_columns = [
- 'metric_name', 'description', 'verbose_name', 'metric_type',
- 'expression', 'table', 'd3format', 'is_restricted', 'warning_text']
- description_columns = {
- 'expression': utils.markdown(
- 'a valid, *aggregating* SQL expression as supported by the '
- 'underlying backend. Example: `count(DISTINCT userid)`', True),
- 'is_restricted': _('Whether the access to this metric is restricted '
- 'to certain roles. Only roles with the permission '
- "'metric access on XXX (the name of this metric)' "
- 'are allowed to access this metric'),
- 'd3format': utils.markdown(
- 'd3 formatting string as defined [here]'
- '(https://github.com/d3/d3-format/blob/master/README.md#format). '
- 'For instance, this default formatting applies in the Table '
- 'visualization and allow for different metric to use different '
- 'formats', True,
- ),
- }
- add_columns = edit_columns
- page_size = 500
- label_columns = {
- 'metric_name': _('Metric'),
- 'description': _('Description'),
- 'verbose_name': _('Verbose Name'),
- 'metric_type': _('Type'),
- 'expression': _('SQL Expression'),
- 'table': _('Table'),
- 'd3format': _('D3 Format'),
- 'is_restricted': _('Is Restricted'),
- 'warning_text': _('Warning Message'),
- }
- def post_add(self, metric):
- if metric.is_restricted:
- security_manager.merge_perm('metric_access', metric.get_perm())
- def post_update(self, metric):
- if metric.is_restricted:
- security_manager.merge_perm('metric_access', metric.get_perm())
- appbuilder.add_view_no_menu(SqlMetricInlineView)
- class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
- datamodel = SQLAInterface(models.SqlaTable)
- list_title = _('List Tables')
- show_title = _('Show Table')
- add_title = _('Import a table definition')
- edit_title = _('Edit Table')
- list_columns = [
- 'link', 'database_name',
- 'changed_by_', 'modified']
- order_columns = ['modified']
- add_columns = ['database', 'schema', 'table_name']
- edit_columns = [
- 'table_name', 'sql', 'filter_select_enabled',
- 'fetch_values_predicate', 'database', 'schema',
- 'description', 'owner',
- 'main_dttm_col', 'default_endpoint', 'offset', 'cache_timeout',
- 'is_sqllab_view', 'template_params',
- ]
- base_filters = [['id', DatasourceFilter, lambda: []]]
- show_columns = edit_columns + ['perm', 'slices']
- related_views = [TableColumnInlineView, SqlMetricInlineView]
- base_order = ('changed_on', 'desc')
- search_columns = (
- 'database', 'schema', 'table_name', 'owner', 'is_sqllab_view',
- )
- description_columns = {
- 'slices': _(
- 'The list of charts associated with this table. By '
- 'altering this datasource, you may change how these associated '
- 'charts behave. '
- 'Also note that charts need to point to a datasource, so '
- 'this form will fail at saving if removing charts from a '
- 'datasource. If you want to change the datasource for a chart, '
- "overwrite the chart from the 'explore view'"),
- 'offset': _('Timezone offset (in hours) for this datasource'),
- 'table_name': _(
- 'Name of the table that exists in the source database'),
- 'schema': _(
- 'Schema, as used only in some databases like Postgres, Redshift '
- 'and DB2'),
- 'description': Markup(
- 'Supports <a href="https://daringfireball.net/projects/markdown/">'
- 'markdown</a>'),
- 'sql': _(
- 'This fields acts a Superset view, meaning that Superset will '
- 'run a query against this string as a subquery.',
- ),
- 'fetch_values_predicate': _(
- 'Predicate applied when fetching distinct value to '
- 'populate the filter control component. Supports '
- 'jinja template syntax. Applies only when '
- '`Enable Filter Select` is on.',
- ),
- 'default_endpoint': _(
- 'Redirects to this endpoint when clicking on the table '
- 'from the table list'),
- 'filter_select_enabled': _(
- "Whether to populate the filter's dropdown in the explore "
- "view's filter section with a list of distinct values fetched "
- 'from the backend on the fly'),
- 'is_sqllab_view': _(
- "Whether the table was generated by the 'Visualize' flow "
- 'in SQL Lab'),
- 'template_params': _(
- 'A set of parameters that become available in the query using '
- 'Jinja templating syntax'),
- 'cache_timeout': _(
- 'Duration (in seconds) of the caching timeout for this table. '
- 'A timeout of 0 indicates that the cache never expires. '
- 'Note this defaults to the database timeout if undefined.'),
- }
- label_columns = {
- 'slices': _('Associated Charts'),
- 'link': _('Table'),
- 'changed_by_': _('Changed By'),
- 'database': _('Database'),
- 'database_name': _('Database'),
- 'changed_on_': _('Last Changed'),
- 'filter_select_enabled': _('Enable Filter Select'),
- 'schema': _('Schema'),
- 'default_endpoint': _('Default Endpoint'),
- 'offset': _('Offset'),
- 'cache_timeout': _('Cache Timeout'),
- 'table_name': _('Table Name'),
- 'fetch_values_predicate': _('Fetch Values Predicate'),
- 'owner': _('Owner'),
- 'main_dttm_col': _('Main Datetime Column'),
- 'description': _('Description'),
- 'is_sqllab_view': _('SQL Lab View'),
- 'template_params': _('Template parameters'),
- 'modified': _('Modified'),
- }
- def pre_add(self, table):
- with db.session.no_autoflush:
- table_query = db.session.query(models.SqlaTable).filter(
- models.SqlaTable.table_name == table.table_name,
- models.SqlaTable.schema == table.schema,
- models.SqlaTable.database_id == table.database.id)
- if db.session.query(table_query.exists()).scalar():
- raise Exception(
- get_datasource_exist_error_msg(table.full_name))
- # Fail before adding if the table can't be found
- try:
- table.get_sqla_table_object()
- except Exception:
- raise Exception(_(
- 'Table [{}] could not be found, '
- 'please double check your '
- 'database connection, schema, and '
- 'table name').format(table.name))
- def post_add(self, table, flash_message=True):
- table.fetch_metadata()
- security_manager.merge_perm('datasource_access', table.get_perm())
- if table.schema:
- security_manager.merge_perm('schema_access', table.schema_perm)
- if flash_message:
- flash(_(
- 'The table was created. '
- 'As part of this two phase configuration '
- 'process, you should now click the edit button by '
- 'the new table to configure it.'), 'info')
- def post_update(self, table):
- self.post_add(table, flash_message=False)
- def _delete(self, pk):
- DeleteMixin._delete(self, pk)
- @expose('/edit/<pk>', methods=['GET', 'POST'])
- @has_access
- def edit(self, pk):
- """Simple hack to redirect to explore view after saving"""
- resp = super(TableModelView, self).edit(pk)
- if isinstance(resp, basestring):
- return resp
- return redirect('/superset/explore/table/{}/'.format(pk))
- @action(
- 'refresh',
- __('Refresh Metadata'),
- __('Refresh column metadata'),
- 'fa-refresh')
- def refresh(self, tables):
- if not isinstance(tables, list):
- tables = [tables]
- successes = []
- failures = []
- for t in tables:
- try:
- t.fetch_metadata()
- successes.append(t)
- except Exception:
- failures.append(t)
- if len(successes) > 0:
- success_msg = _(
- 'Metadata refreshed for the following table(s): %(tables)s',
- tables=', '.join([t.table_name for t in successes]))
- flash(success_msg, 'info')
- if len(failures) > 0:
- failure_msg = _(
- 'Unable to retrieve metadata for the following table(s): %(tables)s',
- tables=', '.join([t.table_name for t in failures]))
- flash(failure_msg, 'danger')
- return redirect('/tablemodelview/list/')
- appbuilder.add_view_no_menu(TableModelView)
- appbuilder.add_link(
- 'Tables',
- label=__('Tables'),
- href='/tablemodelview/list/?_flt_1_is_sqllab_view=y',
- icon='fa-table',
- category='Sources',
- category_label=__('Sources'),
- category_icon='fa-table')
- appbuilder.add_separator('Sources')
|