Make owner a m2m relation on datasources (#6544)

* Make owner a m2m relation on datasources

* Fix pylint

* Make migration work in mysql & sqlite
This commit is contained in:
leakingoxide
2018-12-21 05:35:32 +01:00
committed by Maxime Beauchemin
parent 6a95f8070a
commit fd0338614a
8 changed files with 149 additions and 26 deletions

View File

@@ -349,13 +349,13 @@ export class DatasourceEditor extends React.PureComponent {
control={<TextControl />}
/>}
<Field
fieldKey="owner"
label={t('Owner')}
descr={t('Owner of the datasource')}
fieldKey="owners"
label={t('Owners')}
descr={t('Owners of the datasource')}
control={
<SelectAsyncControl
dataEndpoint="/users/api/read"
multi={false}
multi
mutator={data => data.pks.map((pk, i) => ({
value: pk,
label: `${data.result[i].first_name} ${data.result[i].last_name}`,

View File

@@ -25,6 +25,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
baselink = None # url portion pointing to ModelView endpoint
column_class = None # link to derivative of BaseColumn
metric_class = None # link to derivative of BaseMetric
owner_class = None
# Used to do code highlighting when displaying the query in the UI
query_language = None
@@ -45,7 +46,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
perm = Column(String(1000))
sql = None
owner = None
owners = None
update_from_object_fields = None
@declared_attr
@@ -205,7 +206,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
'metrics': [o.data for o in self.metrics],
'metrics_combo': self.metrics_combo,
'order_by_choices': order_by_choices,
'owner': self.owner.id if self.owner else None,
'owners': [owner.id for owner in self.owners],
'verbose_map': verbose_map,
'select_star': self.select_star,
}
@@ -325,7 +326,7 @@ class BaseDatasource(AuditMixinNullable, ImportMixin):
for attr in self.update_from_object_fields:
setattr(self, attr, obj.get(attr))
self.user_id = obj.get('owner')
self.owners = obj.get('owners', [])
# Syncing metrics
metrics = self.get_fk_many_from_list(

View File

@@ -26,7 +26,7 @@ from pydruid.utils.postaggregator import (
import requests
import sqlalchemy as sa
from sqlalchemy import (
Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint,
Boolean, Column, DateTime, ForeignKey, Integer, String, Table, Text, UniqueConstraint,
)
from sqlalchemy.orm import backref, relationship
@@ -43,6 +43,7 @@ from superset.utils.core import (
DRUID_TZ = conf.get('DRUID_TZ')
POST_AGG_TYPE = 'postagg'
metadata = Model.metadata # pylint: disable=no-member
# Function wrapper because bound methods cannot
@@ -446,6 +447,14 @@ class DruidMetric(Model, BaseMetric):
return import_datasource.import_simple_obj(db.session, i_metric, lookup_obj)
druiddatasource_user = Table(
'druiddatasource_user', metadata,
Column('id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey('ab_user.id')),
Column('datasource_id', Integer, ForeignKey('datasources.id')),
)
class DruidDatasource(Model, BaseDatasource):
"""ORM object referencing Druid datasources (tables)"""
@@ -458,6 +467,7 @@ class DruidDatasource(Model, BaseDatasource):
cluster_class = DruidCluster
metric_class = DruidMetric
column_class = DruidColumn
owner_class = security_manager.user_model
baselink = 'druiddatasourcemodelview'
@@ -470,11 +480,8 @@ class DruidDatasource(Model, BaseDatasource):
String(250), ForeignKey('clusters.cluster_name'))
cluster = relationship(
'DruidCluster', backref='datasources', foreign_keys=[cluster_name])
user_id = Column(Integer, ForeignKey('ab_user.id'))
owner = relationship(
security_manager.user_model,
backref=backref('datasources', cascade='all, delete-orphan'),
foreign_keys=[user_id])
owners = relationship(owner_class, secondary=druiddatasource_user,
backref='druiddatasources')
UniqueConstraint('cluster_name', 'datasource_name')
export_fields = (
@@ -657,7 +664,7 @@ class DruidDatasource(Model, BaseDatasource):
datasource = cls(
datasource_name=druid_config['name'],
cluster=cluster,
owner=user,
owners=[user],
changed_by_fk=user.id,
created_by_fk=user.id,
)

View File

@@ -214,12 +214,12 @@ class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin
order_columns = ['datasource_link', 'modified']
related_views = [DruidColumnInlineView, DruidMetricInlineView]
edit_columns = [
'datasource_name', 'cluster', 'description', 'owner',
'datasource_name', 'cluster', 'description', 'owners',
'is_hidden',
'filter_select_enabled', 'fetch_values_from',
'default_endpoint', 'offset', 'cache_timeout']
search_columns = (
'datasource_name', 'cluster', 'description', 'owner',
'datasource_name', 'cluster', 'description', 'owners',
)
add_columns = edit_columns
show_columns = add_columns + ['perm', 'slices']
@@ -263,7 +263,7 @@ class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin
'datasource_link': _('Data Source'),
'cluster': _('Cluster'),
'description': _('Description'),
'owner': _('Owner'),
'owners': _('Owners'),
'is_hidden': _('Is Hidden'),
'filter_select_enabled': _('Enable Filter Select'),
'default_endpoint': _('Default Endpoint'),

View File

@@ -9,7 +9,7 @@ import pandas as pd
import sqlalchemy as sa
from sqlalchemy import (
and_, asc, Boolean, Column, DateTime, desc, ForeignKey, Integer, or_,
select, String, Text,
select, String, Table, Text,
)
from sqlalchemy.exc import CompileError
from sqlalchemy.orm import backref, relationship
@@ -27,6 +27,7 @@ from superset.models.helpers import QueryResult
from superset.utils import core as utils, import_datasource
config = app.config
metadata = Model.metadata # pylint: disable=no-member
class AnnotationDatasource(BaseDatasource):
@@ -250,6 +251,14 @@ class SqlMetric(Model, BaseMetric):
return import_datasource.import_simple_obj(db.session, i_metric, lookup_obj)
sqlatable_user = Table(
'sqlatable_user', metadata,
Column('id', Integer, primary_key=True),
Column('user_id', Integer, ForeignKey('ab_user.id')),
Column('table_id', Integer, ForeignKey('tables.id')),
)
class SqlaTable(Model, BaseDatasource):
"""An ORM object for SqlAlchemy table references"""
@@ -258,6 +267,7 @@ class SqlaTable(Model, BaseDatasource):
query_language = 'sql'
metric_class = SqlMetric
column_class = TableColumn
owner_class = security_manager.user_model
__tablename__ = 'tables'
__table_args__ = (UniqueConstraint('database_id', 'table_name'),)
@@ -266,11 +276,7 @@ class SqlaTable(Model, BaseDatasource):
main_dttm_col = Column(String(250))
database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False)
fetch_values_predicate = Column(String(1000))
user_id = Column(Integer, ForeignKey('ab_user.id'))
owner = relationship(
security_manager.user_model,
backref='tables',
foreign_keys=[user_id])
owners = relationship(owner_class, secondary=sqlatable_user, backref='tables')
database = relationship(
'Database',
backref=backref('tables', cascade='all, delete-orphan'),

View File

@@ -162,7 +162,7 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
edit_columns = [
'table_name', 'sql', 'filter_select_enabled',
'fetch_values_predicate', 'database', 'schema',
'description', 'owner',
'description', 'owners',
'main_dttm_col', 'default_endpoint', 'offset', 'cache_timeout',
'is_sqllab_view', 'template_params',
]
@@ -171,7 +171,7 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
related_views = [TableColumnInlineView, SqlMetricInlineView]
base_order = ('changed_on', 'desc')
search_columns = (
'database', 'schema', 'table_name', 'owner', 'is_sqllab_view',
'database', 'schema', 'table_name', 'owners', 'is_sqllab_view',
)
description_columns = {
'slices': _(
@@ -233,7 +233,7 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
'cache_timeout': _('Cache Timeout'),
'table_name': _('Table Name'),
'fetch_values_predicate': _('Fetch Values Predicate'),
'owner': _('Owner'),
'owners': _('Owners'),
'main_dttm_col': _('Main Datetime Column'),
'description': _('Description'),
'is_sqllab_view': _('SQL Lab View'),

View File

@@ -0,0 +1,105 @@
"""change_owner_to_m2m_relation_on_datasources.py
Revision ID: 3e1b21cd94a4
Revises: 4ce8df208545
Create Date: 2018-12-15 12:34:47.228756
"""
# revision identifiers, used by Alembic.
from superset import db
from superset.utils.core import generic_find_fk_constraint_name
revision = '3e1b21cd94a4'
down_revision = '6c7537a6004a'
from alembic import op
import sqlalchemy as sa
sqlatable_user = sa.Table(
'sqlatable_user', sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', sa.Integer, sa.ForeignKey('ab_user.id')),
sa.Column('table_id', sa.Integer, sa.ForeignKey('tables.id')),
)
SqlaTable = sa.Table(
'tables', sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', sa.Integer, sa.ForeignKey('ab_user.id')),
)
druiddatasource_user = sa.Table(
'druiddatasource_user', sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', sa.Integer, sa.ForeignKey('ab_user.id')),
sa.Column('datasource_id', sa.Integer, sa.ForeignKey('datasources.id')),
)
DruidDatasource = sa.Table(
'datasources', sa.MetaData(),
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('user_id', sa.Integer, sa.ForeignKey('ab_user.id')),
)
def upgrade():
op.create_table('sqlatable_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('table_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('druiddatasource_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('datasource_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['datasource_id'], ['datasources.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
sa.PrimaryKeyConstraint('id')
)
bind = op.get_bind()
insp = sa.engine.reflection.Inspector.from_engine(bind)
session = db.Session(bind=bind)
tables = session.query(SqlaTable).all()
for table in tables:
if table.user_id is not None:
session.execute(
sqlatable_user.insert().values(user_id=table.user_id, table_id=table.id)
)
druiddatasources = session.query(DruidDatasource).all()
for druiddatasource in druiddatasources:
if druiddatasource.user_id is not None:
session.execute(
druiddatasource_user.insert().values(user_id=druiddatasource.user_id, datasource_id=druiddatasource.id)
)
session.close()
with op.batch_alter_table('tables') as batch_op:
batch_op.drop_constraint('user_id', type_='foreignkey')
batch_op.drop_column('user_id')
with op.batch_alter_table('datasources') as batch_op:
batch_op.drop_constraint(generic_find_fk_constraint_name(
'datasources',
{'id'},
'ab_user',
insp,
), type_='foreignkey')
batch_op.drop_column('user_id')
def downgrade():
op.drop_table('sqlatable_user')
op.drop_table('druiddatasource_user')
with op.batch_alter_table('tables') as batch_op:
batch_op.add_column(sa.Column('user_id', sa.INTEGER(), nullable=True))
batch_op.create_foreign_key('user_id', 'ab_user', ['user_id'], ['id'])
with op.batch_alter_table('datasources') as batch_op:
batch_op.add_column(sa.Column('user_id', sa.INTEGER(), nullable=True))
batch_op.create_foreign_key('fk_datasources_user_id_ab_user', 'ab_user', ['user_id'], ['id'])

View File

@@ -29,6 +29,10 @@ class Datasource(BaseSupersetView):
'this data source configuration'),
status='401',
)
if 'owners' in datasource:
datasource['owners'] = db.session.query(orm_datasource.owner_class).filter(
orm_datasource.owner_class.id.in_(datasource['owners'])).all()
orm_datasource.update_from_object(datasource)
data = orm_datasource.data
db.session.commit()