From commits-return-2240-archive-asf-public=cust-asf.ponee.io@superset.incubator.apache.org Wed Feb 6 21:42:52 2019 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id CC0BB180679 for ; Wed, 6 Feb 2019 22:42:50 +0100 (CET) Received: (qmail 67791 invoked by uid 500); 6 Feb 2019 21:42:50 -0000 Mailing-List: contact commits-help@superset.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@superset.incubator.apache.org Delivered-To: mailing list commits@superset.incubator.apache.org Received: (qmail 67782 invoked by uid 99); 6 Feb 2019 21:42:49 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 06 Feb 2019 21:42:49 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 487B9807DE; Wed, 6 Feb 2019 21:42:49 +0000 (UTC) Date: Wed, 06 Feb 2019 21:42:49 +0000 To: "commits@superset.apache.org" Subject: [incubator-superset] branch master updated: Backend only tagging system (#6823) MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Message-ID: <154948936880.30748.5304675844177444917@gitbox.apache.org> From: christine@apache.org X-Git-Host: gitbox.apache.org X-Git-Repo: incubator-superset X-Git-Refname: refs/heads/master X-Git-Reftype: branch X-Git-Oldrev: 16a8e314a12e894ec4f94aff8506998ebfd05b6f X-Git-Newrev: 8041b63af647e625d6b8f0f8f109ed142e405718 X-Git-Rev: 8041b63af647e625d6b8f0f8f109ed142e405718 X-Git-NotificationType: ref_changed_plus_diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated 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 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 + '
  • ' + href + '
  • ' return '
      ' + action_list + '
    ' + + +# 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///', 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///', 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///', 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)