superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From christ...@apache.org
Subject [incubator-superset] branch master updated: Backend only tagging system (#6823)
Date Wed, 06 Feb 2019 21:42:49 GMT
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 <roberto@dealmeida.net>
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)


Mime
View raw message