This is an automated email from the ASF dual-hosted git repository.
christine pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 8041b63 Backend only tagging system (#6823)
8041b63 is described below
commit 8041b63af647e625d6b8f0f8f109ed142e405718
Author: Beto Dealmeida <[email protected]>
AuthorDate: Wed Feb 6 13:42:42 2019 -0800
Backend only tagging system (#6823)
This PR introduces the backend changes for a tagging system for Superset,
allowing dashboards, charts and queries to be tagged. It also allows searching
for a given tag, and will be the basis for a new landing page (see #5327).
# Implicit tags
Dashboard, chart and (saved) queries have implicit tags related to their
owners, types and favorites. For example, all objects owned by the admin have
the tag `owner:1`. All charts have the tag `type:chart`. Objects favorited by
the admin have the tag `favorited_by:1`.
These tags are automatically added by a migration script, and kept in sync
through SQLAlchemy event listeners. They are currently not surfaced to the
user, but can be searched for. For example, it's possible to search for
`owner:1` in the welcome page to see all objects owned by the admin, or even
search for `owner:{{ current_user_id() }}`.
---
.../versions/c82ee8a39623_add_implicit_tags.py | 203 +++++++++++++++++
superset/models/core.py | 19 ++
superset/models/sql_lab.py | 10 +
superset/models/tags.py | 244 +++++++++++++++++++++
superset/views/__init__.py | 1 +
superset/views/tags.py | 217 ++++++++++++++++++
6 files changed, 694 insertions(+)
diff --git a/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py
b/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py
new file mode 100644
index 0000000..cfb568d
--- /dev/null
+++ b/superset/migrations/versions/c82ee8a39623_add_implicit_tags.py
@@ -0,0 +1,203 @@
+# 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.
+"""Add implicit tags
+
+Revision ID: c82ee8a39623
+Revises: c18bd4186f15
+Create Date: 2018-07-26 11:10:23.653524
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'c82ee8a39623'
+down_revision = 'c617da68de7d'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy import Column, Enum, Integer, ForeignKey, String, Table
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relationship
+
+from superset import db
+from superset.models.helpers import AuditMixinNullable
+from superset.models.tags import (
+ get_object_type,
+ get_tag,
+ ObjectTypes,
+ TagTypes,
+)
+
+
+Base = declarative_base()
+
+
+class Tag(Base, AuditMixinNullable):
+ """A tag attached to an object (query, chart or dashboard)."""
+ __tablename__ = 'tag'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String(250), unique=True)
+ type = Column(Enum(TagTypes))
+
+
+class TaggedObject(Base, AuditMixinNullable):
+ __tablename__ = 'tagged_object'
+
+ id = Column(Integer, primary_key=True)
+ tag_id = Column(Integer, ForeignKey('tag.id'))
+ object_id = Column(Integer)
+ object_type = Column(Enum(ObjectTypes))
+
+
+class User(Base):
+ """Declarative class to do query in upgrade"""
+ __tablename__ = 'ab_user'
+ id = Column(Integer, primary_key=True)
+
+
+slice_user = Table(
+ 'slice_user',
+ Base.metadata,
+ Column('id', Integer, primary_key=True),
+ Column('user_id', Integer, ForeignKey('ab_user.id')),
+ Column('slice_id', Integer, ForeignKey('slices.id'))
+)
+
+
+dashboard_user = Table(
+ 'dashboard_user',
+ Base.metadata,
+ Column('id', Integer, primary_key=True),
+ Column('user_id', Integer, ForeignKey('ab_user.id')),
+ Column('dashboard_id', Integer, ForeignKey('dashboards.id'))
+)
+
+
+class Slice(Base, AuditMixinNullable):
+ """Declarative class to do query in upgrade"""
+ __tablename__ = 'slices'
+
+ id = Column(Integer, primary_key=True)
+ owners = relationship("User", secondary=slice_user)
+
+
+class Dashboard(Base, AuditMixinNullable):
+ """Declarative class to do query in upgrade"""
+ __tablename__ = 'dashboards'
+ id = Column(Integer, primary_key=True)
+ owners = relationship("User", secondary=dashboard_user)
+
+
+class SavedQuery(Base):
+ __tablename__ = 'saved_query'
+ id = Column(Integer, primary_key=True)
+ user_id = Column(Integer, ForeignKey('ab_user.id'))
+
+
+class Favstar(Base):
+ __tablename__ = 'favstar'
+ id = Column(Integer, primary_key=True)
+ user_id = Column(Integer, ForeignKey('ab_user.id'))
+ class_name = Column(String(50))
+ obj_id = Column(Integer)
+
+
+def upgrade():
+ bind = op.get_bind()
+ session = db.Session(bind=bind)
+
+ Tag.__table__.create(bind)
+ TaggedObject.__table__.create(bind)
+
+ # add type tags (eg, `type:dashboard` for dashboards)
+ for type in ObjectTypes.__members__:
+ session.add(Tag(name='type:{0}'.format(type), type=TagTypes.type))
+
+ # add owner tags (eg, `owner:1` for things owned by the admin)
+ for chart in session.query(Slice):
+ for owner in chart.owners:
+ name = 'owner:{0}'.format(owner.id)
+ tag = get_tag(name, session, TagTypes.owner)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=chart.id,
+ object_type=ObjectTypes.chart,
+ )
+ session.add(tagged_object)
+
+ tag = get_tag('type:chart', session, TagTypes.type)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=chart.id,
+ object_type=ObjectTypes.chart,
+ )
+ session.add(tagged_object)
+
+ for dashboard in session.query(Dashboard):
+ for owner in dashboard.owners:
+ name = 'owner:{0}'.format(owner.id)
+ tag = get_tag(name, session, TagTypes.owner)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=dashboard.id,
+ object_type=ObjectTypes.dashboard,
+ )
+ session.add(tagged_object)
+
+ tag = get_tag('type:dashboard', session, TagTypes.type)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=dashboard.id,
+ object_type=ObjectTypes.dashboard,
+ )
+ session.add(tagged_object)
+
+ for query in session.query(SavedQuery):
+ name = 'owner:{0}'.format(query.user_id)
+ tag = get_tag(name, session, TagTypes.owner)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=query.id,
+ object_type=ObjectTypes.query,
+ )
+ session.add(tagged_object)
+
+ tag = get_tag('type:query', session, TagTypes.type)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=query.id,
+ object_type=ObjectTypes.query,
+ )
+ session.add(tagged_object)
+
+ # add favorited_by tags
+ for star in session.query(Favstar):
+ name = 'favorited_by:{0}'.format(star.user_id)
+ tag = get_tag(name, session, TagTypes.favorited_by)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=star.obj_id,
+ object_type=get_object_type(star.class_name),
+ )
+ session.add(tagged_object)
+
+ session.commit()
+
+
+def downgrade():
+ op.drop_table('tag')
+ op.drop_table('tagged_object')
diff --git a/superset/models/core.py b/superset/models/core.py
index fc4b7e9..f68e94e 100644
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -48,6 +48,7 @@ from superset import app, db, db_engine_specs,
security_manager
from superset.connectors.connector_registry import ConnectorRegistry
from superset.legacy import update_time_range
from superset.models.helpers import AuditMixinNullable, ImportMixin
+from superset.models.tags import ChartUpdater, DashboardUpdater, FavStarUpdater
from superset.models.user_attributes import UserAttribute
from superset.utils import (
cache as cache_util,
@@ -359,6 +360,13 @@ class Slice(Model, AuditMixinNullable, ImportMixin):
session.flush()
return slc_to_import.id
+ @property
+ def url(self):
+ return (
+ '/superset/explore/?form_data=%7B%22slice_id%22%3A%20{0}%7D'
+ .format(self.id)
+ )
+
sqla.event.listen(Slice, 'before_insert', set_related_perm)
sqla.event.listen(Slice, 'before_update', set_related_perm)
@@ -1253,3 +1261,14 @@ class DatasourceAccessRequest(Model, AuditMixinNullable):
href = '{} Role'.format(r.name)
action_list = action_list + '<li>' + href + '</li>'
return '<ul>' + action_list + '</ul>'
+
+
+# events for updating tags
+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)
+sqla.event.listen(Dashboard, 'after_insert', DashboardUpdater.after_insert)
+sqla.event.listen(Dashboard, 'after_update', DashboardUpdater.after_update)
+sqla.event.listen(Dashboard, 'after_delete', DashboardUpdater.after_delete)
+sqla.event.listen(FavStar, 'after_insert', FavStarUpdater.after_insert)
+sqla.event.listen(FavStar, 'after_delete', FavStarUpdater.after_delete)
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index b843a52..93eae2f 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -29,6 +29,7 @@ from sqlalchemy.orm import backref, relationship
from superset import security_manager
from superset.models.helpers import AuditMixinNullable, ExtraJSONMixin
+from superset.models.tags import QueryUpdater
from superset.utils.core import QueryStatus, user_label
@@ -173,3 +174,12 @@ class SavedQuery(Model, AuditMixinNullable,
ExtraJSONMixin):
@property
def sqlalchemy_uri(self):
return self.database.sqlalchemy_uri
+
+ def url(self):
+ return '/superset/sqllab?savedQueryId={0}'.format(self.id)
+
+
+# events for updating tags
+sqla.event.listen(SavedQuery, 'after_insert', QueryUpdater.after_insert)
+sqla.event.listen(SavedQuery, 'after_update', QueryUpdater.after_update)
+sqla.event.listen(SavedQuery, 'after_delete', QueryUpdater.after_delete)
diff --git a/superset/models/tags.py b/superset/models/tags.py
new file mode 100644
index 0000000..897c189
--- /dev/null
+++ b/superset/models/tags.py
@@ -0,0 +1,244 @@
+# 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.
+# pylint: disable=C,R,W,no-init
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import enum
+
+from flask_appbuilder import Model
+from sqlalchemy import Column, Enum, ForeignKey, Integer, String
+from sqlalchemy.orm import relationship, sessionmaker
+from sqlalchemy.orm.exc import NoResultFound
+
+from superset.models.helpers import AuditMixinNullable
+
+
+Session = sessionmaker(autoflush=False)
+
+
+class TagTypes(enum.Enum):
+
+ """
+ Types for tags.
+
+ Objects (queries, charts and dashboards) will have with implicit tags based
+ on metadata: types, owners and who favorited them. This way, user "alice"
+ can find all their objects by querying for the tag `owner:alice`.
+ """
+
+ # explicit tags, added manually by the owner
+ custom = 1
+
+ # implicit tags, generated automatically
+ type = 2
+ owner = 3
+ favorited_by = 4
+
+
+class ObjectTypes(enum.Enum):
+
+ """Object types."""
+
+ query = 1
+ chart = 2
+ dashboard = 3
+
+
+class Tag(Model, AuditMixinNullable):
+
+ """A tag attached to an object (query, chart or dashboard)."""
+
+ __tablename__ = 'tag'
+ id = Column(Integer, primary_key=True) # pylint: disable=invalid-name
+ name = Column(String(250), unique=True)
+ type = Column(Enum(TagTypes))
+
+
+class TaggedObject(Model, AuditMixinNullable):
+
+ """An association between an object and a tag."""
+
+ __tablename__ = 'tagged_object'
+ id = Column(Integer, primary_key=True) # pylint: disable=invalid-name
+ tag_id = Column(Integer, ForeignKey('tag.id'))
+ object_id = Column(Integer)
+ object_type = Column(Enum(ObjectTypes))
+
+ tag = relationship('Tag')
+
+
+def get_tag(name, session, type_):
+ try:
+ tag = session.query(Tag).filter_by(name=name, type=type_).one()
+ except NoResultFound:
+ tag = Tag(name=name, type=type_)
+ session.add(tag)
+ session.commit()
+
+ return tag
+
+
+def get_object_type(class_name):
+ mapping = {
+ 'slice': ObjectTypes.chart,
+ 'dashboard': ObjectTypes.dashboard,
+ 'query': ObjectTypes.query,
+ }
+ try:
+ return mapping[class_name.lower()]
+ except KeyError:
+ raise Exception('No mapping found for {0}'.format(class_name))
+
+
+class ObjectUpdater(object):
+
+ object_type = None
+
+ @classmethod
+ def get_owners_ids(cls, target):
+ raise NotImplementedError('Subclass should implement `get_owners_ids`')
+
+ @classmethod
+ def _add_owners(cls, session, target):
+ for owner_id in cls.get_owners_ids(target):
+ name = 'owner:{0}'.format(owner_id)
+ tag = get_tag(name, session, TagTypes.owner)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=target.id,
+ object_type=ObjectTypes.chart,
+ )
+ session.add(tagged_object)
+
+ @classmethod
+ def after_insert(cls, mapper, connection, target):
+ # pylint: disable=unused-argument
+ session = Session(bind=connection)
+
+ # add `owner:` tags
+ cls._add_owners(session, target)
+
+ # add `type:` tags
+ tag = get_tag(
+ 'type:{0}'.format(cls.object_type), session, TagTypes.type)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=target.id,
+ object_type=ObjectTypes.query,
+ )
+ session.add(tagged_object)
+
+ session.commit()
+
+ @classmethod
+ def after_update(cls, mapper, connection, target):
+ # pylint: disable=unused-argument
+ session = Session(bind=connection)
+
+ # delete current `owner:` tags
+ query = session.query(TaggedObject.id).join(Tag).filter(
+ TaggedObject.object_type == cls.object_type,
+ TaggedObject.object_id == target.id,
+ Tag.type == TagTypes.owner,
+ )
+ ids = [row[0] for row in query]
+ session.query(TaggedObject).filter(
+ TaggedObject.id.in_(ids)).delete(
+ synchronize_session=False)
+
+ # add `owner:` tags
+ cls._add_owners(session, target)
+
+ session.commit()
+
+ @classmethod
+ def after_delete(cls, mapper, connection, target):
+ # pylint: disable=unused-argument
+ session = Session(bind=connection)
+
+ # delete row from `tagged_objects`
+ session.query(TaggedObject).filter(
+ TaggedObject.object_type == cls.object_type,
+ TaggedObject.object_id == target.id,
+ ).delete()
+
+ session.commit()
+
+
+class ChartUpdater(ObjectUpdater):
+
+ object_type = 'chart'
+
+ @classmethod
+ def get_owners_ids(cls, target):
+ return [owner.id for owner in target.owners]
+
+
+class DashboardUpdater(ObjectUpdater):
+
+ object_type = 'dashboard'
+
+ @classmethod
+ def get_owners_ids(cls, target):
+ return [owner.id for owner in target.owners]
+
+
+class QueryUpdater(ObjectUpdater):
+
+ object_type = 'query'
+
+ @classmethod
+ def get_owners_ids(cls, target):
+ return [target.user_id]
+
+
+class FavStarUpdater(object):
+
+ @classmethod
+ def after_insert(cls, mapper, connection, target):
+ # pylint: disable=unused-argument
+ session = Session(bind=connection)
+ name = 'favorited_by:{0}'.format(target.user_id)
+ tag = get_tag(name, session, TagTypes.favorited_by)
+ tagged_object = TaggedObject(
+ tag_id=tag.id,
+ object_id=target.obj_id,
+ object_type=get_object_type(target.class_name),
+ )
+ session.add(tagged_object)
+
+ session.commit()
+
+ @classmethod
+ def after_delete(cls, mapper, connection, target):
+ # pylint: disable=unused-argument
+ session = Session(bind=connection)
+ name = 'favorited_by:{0}'.format(target.user_id)
+ query = session.query(TaggedObject.id).join(Tag).filter(
+ TaggedObject.object_id == target.obj_id,
+ Tag.type == TagTypes.favorited_by,
+ Tag.name == name,
+ )
+ ids = [row[0] for row in query]
+ session.query(TaggedObject).filter(
+ TaggedObject.id.in_(ids)).delete(
+ synchronize_session=False)
+
+ session.commit()
diff --git a/superset/views/__init__.py b/superset/views/__init__.py
index 386e16e..380ea6e 100644
--- a/superset/views/__init__.py
+++ b/superset/views/__init__.py
@@ -22,3 +22,4 @@ from . import dashboard # noqa
from . import annotations # noqa
from . import datasource # noqa
from . import schedules # noqa
+from . import tags # noqa
diff --git a/superset/views/tags.py b/superset/views/tags.py
new file mode 100644
index 0000000..fc34490
--- /dev/null
+++ b/superset/views/tags.py
@@ -0,0 +1,217 @@
+# 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.
+# pylint: disable=C,R,W
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+from flask import request, Response
+from flask_appbuilder import expose
+from flask_appbuilder.security.decorators import has_access_api
+from jinja2.sandbox import SandboxedEnvironment
+import simplejson as json
+from sqlalchemy import and_, func
+from werkzeug.routing import BaseConverter
+
+from superset import app, appbuilder, db, utils
+from superset.jinja_context import current_user_id, current_username
+import superset.models.core
+from superset.models.sql_lab import SavedQuery
+from superset.models.tags import ObjectTypes, Tag, TaggedObject, TagTypes
+from .base import BaseSupersetView, json_success
+
+
+class ObjectTypeConverter(BaseConverter):
+
+ """Validate that object_type is indeed an object type."""
+
+ def to_python(self, object_type):
+ return ObjectTypes[object_type]
+
+ def to_url(self, object_type):
+ return object_type.name
+
+
+def process_template(content):
+ env = SandboxedEnvironment()
+ template = env.from_string(content)
+ context = {
+ 'current_user_id': current_user_id,
+ 'current_username': current_username,
+ }
+ return template.render(context)
+
+
+def get_name(obj):
+ if obj.Dashboard:
+ return obj.Dashboard.dashboard_title
+ elif obj.Slice:
+ return obj.Slice.slice_name
+ elif obj.SavedQuery:
+ return obj.SavedQuery.label
+
+
+def get_creator(obj):
+ if obj.Dashboard:
+ return obj.Dashboard.creator()
+ elif obj.Slice:
+ return obj.Slice.creator()
+ elif obj.SavedQuery:
+ return obj.SavedQuery.creator()
+
+
+def get_attribute(obj, attr):
+ if obj.Dashboard:
+ return getattr(obj.Dashboard, attr)
+ elif obj.Slice:
+ return getattr(obj.Slice, attr)
+ elif obj.SavedQuery:
+ return getattr(obj.SavedQuery, attr)
+
+
+class TagView(BaseSupersetView):
+
+ @has_access_api
+ @expose('/tags/suggestions/', methods=['GET'])
+ def suggestions(self):
+ query = (
+ db.session.query(TaggedObject)
+ .group_by(TaggedObject.tag_id)
+ .order_by(func.count().desc())
+ .all()
+ )
+ tags = [{'id': obj.tag.id, 'name': obj.tag.name} for obj in query]
+ return json_success(json.dumps(tags))
+
+ @has_access_api
+ @expose('/tags/<object_type:object_type>/<int:object_id>/',
methods=['GET'])
+ def get(self, object_type, object_id):
+ """List all tags a given object has."""
+ query = db.session.query(TaggedObject).filter(and_(
+ TaggedObject.object_type == object_type,
+ TaggedObject.object_id == object_id))
+ tags = [{'id': obj.tag.id, 'name': obj.tag.name} for obj in query]
+ return json_success(json.dumps(tags))
+
+ @has_access_api
+ @expose('/tags/<object_type:object_type>/<int:object_id>/',
methods=['POST'])
+ def post(self, object_type, object_id):
+ """Add new tags to an object."""
+ tagged_objects = []
+ for name in request.get_json(force=True):
+ if ':' in name:
+ type_name = name.split(':', 1)[0]
+ type_ = TagTypes[type_name]
+ else:
+ type_ = TagTypes.custom
+
+ tag = db.session.query(Tag).filter_by(name=name,
type=type_).first()
+ if not tag:
+ tag = Tag(name=name, type=type_)
+
+ tagged_objects.append(
+ TaggedObject(
+ object_id=object_id,
+ object_type=object_type,
+ tag=tag,
+ ),
+ )
+
+ db.session.add_all(tagged_objects)
+ db.session.commit()
+
+ return Response(status=201) # 201 CREATED
+
+ @has_access_api
+ @expose('/tags/<object_type:object_type>/<int:object_id>/',
methods=['DELETE'])
+ def delete(self, object_type, object_id):
+ """Remove tags from an object."""
+ tag_names = request.get_json(force=True)
+ if not tag_names:
+ return Response(status=403)
+
+ db.session.query(TaggedObject).filter(and_(
+ TaggedObject.object_type == object_type,
+ TaggedObject.object_id == object_id),
+ TaggedObject.tag.has(Tag.name.in_(tag_names)),
+ ).delete(synchronize_session=False)
+ db.session.commit()
+
+ return Response(status=204) # 204 NO CONTENT
+
+ @has_access_api
+ @expose('/tagged_objects/', methods=['GET', 'POST'])
+ def tagged_objects(self):
+ query = db.session.query(
+ TaggedObject,
+ superset.models.core.Dashboard,
+ superset.models.core.Slice,
+ SavedQuery,
+ ).join(Tag)
+
+ tags = request.args.get('tags')
+ if not tags:
+ return json_success(json.dumps([]))
+
+ tags = [process_template(tag) for tag in tags.split(',')]
+ query = query.filter(Tag.name.in_(tags))
+
+ # filter types
+ types = request.args.get('types')
+ if types:
+ query =
query.filter(TaggedObject.object_type.in_(types.split(',')))
+
+ # get names
+ query = query.outerjoin(
+ superset.models.core.Dashboard,
+ and_(
+ TaggedObject.object_id == superset.models.core.Dashboard.id,
+ TaggedObject.object_type == ObjectTypes.dashboard,
+ ),
+ ).outerjoin(
+ superset.models.core.Slice,
+ and_(
+ TaggedObject.object_id == superset.models.core.Slice.id,
+ TaggedObject.object_type == ObjectTypes.chart,
+ ),
+ ).outerjoin(
+ SavedQuery,
+ and_(
+ TaggedObject.object_id == SavedQuery.id,
+ TaggedObject.object_type == ObjectTypes.query,
+ ),
+ ).group_by(TaggedObject.object_id, TaggedObject.object_type)
+
+ objects = [
+ {
+ 'id': get_attribute(obj, 'id'),
+ 'type': obj.TaggedObject.object_type.name,
+ 'name': get_name(obj),
+ 'url': get_attribute(obj, 'url'),
+ 'changed_on': get_attribute(obj, 'changed_on'),
+ 'created_by': get_attribute(obj, 'created_by_fk'),
+ 'creator': get_creator(obj),
+ }
+ for obj in query if get_attribute(obj, 'id')
+ ]
+
+ return json_success(json.dumps(objects,
default=utils.core.json_int_dttm_ser))
+
+
+app.url_map.converters['object_type'] = ObjectTypeConverter
+appbuilder.add_view_no_menu(TagView)