Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add dataset tagging to the back-end #20892

Merged
merged 40 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a7d19eb
Added back-end dataset tagging code
cccs-Dustin Jul 27, 2022
9d9dce5
Added back-end dataset tagging code
cccs-Dustin Jul 27, 2022
ba5df77
Ran pre-commit hook
cccs-Dustin Jul 28, 2022
1c9e70a
Broke up large function into multiple smaller functions so that the l…
cccs-Dustin Jul 28, 2022
555eccf
Merge branch 'master' into Dataset-Tagging
cccs-Dustin Aug 24, 2022
12322cd
Added integration tests to make sure that the code which was added wo…
cccs-Dustin Aug 26, 2022
d0e3cca
Fixed integration tests which were failing
cccs-Dustin Aug 26, 2022
907e7bb
Ran pre-commit hook
cccs-Dustin Aug 26, 2022
0ee29eb
Reverse which table has entries deleted first
cccs-Dustin Aug 26, 2022
bd43570
Keep what is in the tag table, as without it, the tests do not work
cccs-Dustin Aug 26, 2022
6952304
Removing the code that clears the tagged_object table, going to try t…
cccs-Dustin Aug 26, 2022
9babd50
Modified integration tests
cccs-Dustin Aug 30, 2022
7e00d28
Merge branch 'master' into Dataset-Tagging
cccs-Dustin Aug 30, 2022
b988747
Clear tagged_object table before running the tests
cccs-Dustin Aug 31, 2022
2fadd3d
Merge branch 'apache:master' into Dataset-Tagging
cccs-Dustin Aug 31, 2022
5aa32eb
Run the sync_tags function so that the tags are applied to the new ob…
cccs-Dustin Sep 1, 2022
976b0f0
Merge branch 'Dataset-Tagging' of github.com:CybercentreCanada/supers…
cccs-Dustin Sep 1, 2022
26a53f6
Testing with integration tests
cccs-Dustin Sep 1, 2022
dc0bfd9
Modifying tests
cccs-Dustin Sep 1, 2022
bfb0aa6
Updated the way we mock the feature flag
cccs-Dustin Sep 2, 2022
cfa30b2
Merge branch 'master' into Dataset-Tagging
cccs-Dustin Sep 8, 2022
8eb510d
Change where the SQLAlchemy event listeners are placed
cccs-Dustin Sep 8, 2022
fffc790
Removing un-needed import
cccs-Dustin Sep 9, 2022
c4136df
Adding back code which does not appear to have been properly tracked …
cccs-Dustin Sep 9, 2022
c8deabb
Removing code which does not appear to have been properly tracked in git
cccs-Dustin Sep 9, 2022
f9bb93c
Moving event listeners into their own function
cccs-Dustin Sep 14, 2022
0b51f50
Created functions to register and unregister event listeners, and cre…
cccs-Dustin Sep 15, 2022
aaae5aa
Moving and renaming file from superset.models.tags.py -> superset.tag…
cccs-Dustin Sep 15, 2022
4ad8d8a
Added the 'Query' object to some of the methods where it was required
cccs-Dustin Sep 15, 2022
33ee25b
Added a test to verify that if the 'TAGGING_SYSTEM' flag is set to Fa…
cccs-Dustin Sep 15, 2022
2fdedfa
Moved import statements and where the feature flag check occurs
cccs-Dustin Sep 16, 2022
a94caca
Ran pre-commit hook
cccs-Dustin Sep 16, 2022
fd9bd21
Renamed fixture
cccs-Dustin Sep 16, 2022
d8a5e11
Removed quotation marks after importing annotations from '__future__
cccs-Dustin Sep 16, 2022
079cb7e
Remove unused import
cccs-Dustin Sep 16, 2022
aef30fe
Merge branch 'master' into Dataset-Tagging
cccs-Dustin Sep 16, 2022
160c95d
Commenting out a test to see if it is causing another integration tes…
cccs-Dustin Sep 16, 2022
461b6d5
Removing newly created objects from the db when they are no longer ne…
cccs-Dustin Sep 20, 2022
2375ab1
Added a check to make sure that each tag was deleted when the associa…
cccs-Dustin Sep 20, 2022
3611579
Update tests/integration_tests/fixtures/tags.py
cccs-Dustin Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 225 additions & 90 deletions superset/common/tags.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions superset/initialization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
)
from superset.security import SupersetSecurityManager
from superset.superset_typing import FlaskResponse
from superset.tags.core import register_sqla_event_listeners
from superset.utils.core import pessimistic_connection_handling
from superset.utils.log import DBEventLogger, get_event_logger_from_cfg_value

Expand Down Expand Up @@ -426,6 +427,9 @@ def init_app_in_ctx(self) -> None:
if flask_app_mutator:
flask_app_mutator(self.superset_app)

if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
register_sqla_event_listeners()

self.init_views()

def check_secret_key(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base, declared_attr

from superset.models.tags import ObjectTypes, TagTypes
from superset.tags.models import ObjectTypes, TagTypes
from superset.utils.core import get_user_id

Base = declarative_base()
Expand Down
9 changes: 1 addition & 8 deletions superset/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,12 @@
from sqlalchemy.schema import UniqueConstraint
from sqlalchemy.sql import expression, Select

from superset import app, db_engine_specs, is_feature_enabled
from superset import app, db_engine_specs
from superset.constants import PASSWORD_MASK
from superset.databases.utils import make_url_safe
from superset.db_engine_specs.base import MetricType, TimeGrain
from superset.extensions import cache_manager, encrypted_field_factory, security_manager
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
from superset.models.tags import FavStarUpdater
from superset.result_set import SupersetResultSet
from superset.utils import cache as cache_util, core as utils
from superset.utils.core import get_username
Expand Down Expand Up @@ -808,9 +807,3 @@ class FavStar(Model): # pylint: disable=too-few-public-methods
class_name = Column(String(50))
obj_id = Column(Integer)
dttm = Column(DateTime, default=datetime.utcnow)


# events for updating tags
if is_feature_enabled("TAGGING_SYSTEM"):
sqla.event.listen(FavStar, "after_insert", FavStarUpdater.after_insert)
sqla.event.listen(FavStar, "after_delete", FavStarUpdater.after_delete)
7 changes: 0 additions & 7 deletions superset/models/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
from superset.models.filter_set import FilterSet
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
from superset.models.slice import Slice
from superset.models.tags import DashboardUpdater
from superset.models.user_attributes import UserAttribute
from superset.tasks.thumbnails import cache_dashboard_thumbnail
from superset.utils import core as utils
Expand Down Expand Up @@ -454,12 +453,6 @@ def id_or_slug_filter(id_or_slug: Union[int, str]) -> BinaryExpression:

OnDashboardChange = Callable[[Mapper, Connection, Dashboard], Any]

# events for updating tags
if is_feature_enabled("TAGGING_SYSTEM"):
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)

if is_feature_enabled("THUMBNAILS_SQLA_LISTENERS"):
update_thumbnail: OnDashboardChange = lambda _, __, dash: dash.update_thumbnail()
sqla.event.listen(Dashboard, "after_insert", update_thumbnail)
Expand Down
8 changes: 0 additions & 8 deletions superset/models/slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
from superset import db, is_feature_enabled, security_manager
from superset.legacy import update_time_range
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
from superset.models.tags import ChartUpdater
from superset.tasks.thumbnails import cache_chart_thumbnail
from superset.utils import core as utils
from superset.utils.hashing import md5_sha_from_str
Expand Down Expand Up @@ -367,13 +366,6 @@ def event_after_chart_changed(
sqla.event.listen(Slice, "before_insert", set_related_perm)
sqla.event.listen(Slice, "before_update", set_related_perm)

# events for updating tags
if is_feature_enabled("TAGGING_SYSTEM"):
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)

# events for updating tags
if is_feature_enabled("THUMBNAILS_SQLA_LISTENERS"):
sqla.event.listen(Slice, "after_insert", event_after_chart_changed)
sqla.event.listen(Slice, "after_update", event_after_chart_changed)
7 changes: 0 additions & 7 deletions superset/models/sql_lab.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
ExtraJSONMixin,
ImportExportMixin,
)
from superset.models.tags import QueryUpdater
from superset.sql_parse import CtasMethod, ParsedQuery, Table
from superset.sqllab.limiting_factor import LimitingFactor
from superset.superset_typing import ResultSetColumnType
Expand Down Expand Up @@ -509,9 +508,3 @@ def to_dict(self) -> Dict[str, Any]:
"description": description,
"expanded": self.expanded,
}


# 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)
88 changes: 88 additions & 0 deletions superset/tags/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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.


def register_sqla_event_listeners() -> None:
import sqlalchemy as sqla

from superset.connectors.sqla.models import SqlaTable
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.tags.models import (
ChartUpdater,
DashboardUpdater,
DatasetUpdater,
FavStarUpdater,
QueryUpdater,
)

sqla.event.listen(SqlaTable, "after_insert", DatasetUpdater.after_insert)
sqla.event.listen(SqlaTable, "after_update", DatasetUpdater.after_update)
sqla.event.listen(SqlaTable, "after_delete", DatasetUpdater.after_delete)

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)

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)


def clear_sqla_event_listeners() -> None:
import sqlalchemy as sqla

from superset.connectors.sqla.models import SqlaTable
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
from superset.tags.models import (
ChartUpdater,
DashboardUpdater,
DatasetUpdater,
FavStarUpdater,
QueryUpdater,
)

sqla.event.remove(SqlaTable, "after_insert", DatasetUpdater.after_insert)
sqla.event.remove(SqlaTable, "after_update", DatasetUpdater.after_update)
sqla.event.remove(SqlaTable, "after_delete", DatasetUpdater.after_delete)

sqla.event.remove(Slice, "after_insert", ChartUpdater.after_insert)
sqla.event.remove(Slice, "after_update", ChartUpdater.after_update)
sqla.event.remove(Slice, "after_delete", ChartUpdater.after_delete)

sqla.event.remove(Dashboard, "after_insert", DashboardUpdater.after_insert)
sqla.event.remove(Dashboard, "after_update", DashboardUpdater.after_update)
sqla.event.remove(Dashboard, "after_delete", DashboardUpdater.after_delete)

sqla.event.remove(FavStar, "after_insert", FavStarUpdater.after_insert)
sqla.event.remove(FavStar, "after_delete", FavStarUpdater.after_delete)

sqla.event.remove(SavedQuery, "after_insert", QueryUpdater.after_insert)
sqla.event.remove(SavedQuery, "after_update", QueryUpdater.after_update)
sqla.event.remove(SavedQuery, "after_delete", QueryUpdater.after_delete)
46 changes: 33 additions & 13 deletions superset/models/tags.py → superset/tags/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import, division, print_function, unicode_literals
from __future__ import (
absolute_import,
annotations,
division,
print_function,
unicode_literals,
)

import enum
from typing import List, Optional, TYPE_CHECKING, Union
Expand All @@ -28,6 +34,7 @@
from superset.models.helpers import AuditMixinNullable

if TYPE_CHECKING:
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import FavStar
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
Expand All @@ -41,7 +48,7 @@ class TagTypes(enum.Enum):
"""
Types for tags.

Objects (queries, charts and dashboards) will have with implicit tags based
Objects (queries, charts, dashboards, and datasets) 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`.
"""
Expand All @@ -64,11 +71,12 @@ class ObjectTypes(enum.Enum):
query = 1
chart = 2
dashboard = 3
dataset = 4


class Tag(Model, AuditMixinNullable):

"""A tag attached to an object (query, chart or dashboard)."""
"""A tag attached to an object (query, chart, dashboard, or dataset)."""

__tablename__ = "tag"
id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -103,6 +111,7 @@ def get_object_type(class_name: str) -> ObjectTypes:
"slice": ObjectTypes.chart,
"dashboard": ObjectTypes.dashboard,
"query": ObjectTypes.query,
"dataset": ObjectTypes.dataset,
}
try:
return mapping[class_name.lower()]
Expand All @@ -116,13 +125,15 @@ class ObjectUpdater:

@classmethod
def get_owners_ids(
cls, target: Union["Dashboard", "FavStar", "Slice"]
cls, target: Union[Dashboard, FavStar, Slice, Query, SqlaTable]
) -> List[int]:
raise NotImplementedError("Subclass should implement `get_owners_ids`")

@classmethod
def _add_owners(
cls, session: Session, target: Union["Dashboard", "FavStar", "Slice"]
cls,
session: Session,
target: Union[Dashboard, FavStar, Slice, Query, SqlaTable],
) -> None:
for owner_id in cls.get_owners_ids(target):
name = "owner:{0}".format(owner_id)
Expand All @@ -137,7 +148,7 @@ def after_insert(
cls,
_mapper: Mapper,
connection: Connection,
target: Union["Dashboard", "FavStar", "Slice"],
target: Union[Dashboard, FavStar, Slice, Query, SqlaTable],
) -> None:
session = Session(bind=connection)

Expand All @@ -158,7 +169,7 @@ def after_update(
cls,
_mapper: Mapper,
connection: Connection,
target: Union["Dashboard", "FavStar", "Slice"],
target: Union[Dashboard, FavStar, Slice, Query, SqlaTable],
) -> None:
session = Session(bind=connection)

Expand Down Expand Up @@ -187,7 +198,7 @@ def after_delete(
cls,
_mapper: Mapper,
connection: Connection,
target: Union["Dashboard", "FavStar", "Slice"],
target: Union[Dashboard, FavStar, Slice, Query, SqlaTable],
) -> None:
session = Session(bind=connection)

Expand All @@ -205,7 +216,7 @@ class ChartUpdater(ObjectUpdater):
object_type = "chart"

@classmethod
def get_owners_ids(cls, target: "Slice") -> List[int]:
def get_owners_ids(cls, target: Slice) -> List[int]:
return [owner.id for owner in target.owners]


Expand All @@ -214,7 +225,7 @@ class DashboardUpdater(ObjectUpdater):
object_type = "dashboard"

@classmethod
def get_owners_ids(cls, target: "Dashboard") -> List[int]:
def get_owners_ids(cls, target: Dashboard) -> List[int]:
return [owner.id for owner in target.owners]


Expand All @@ -223,14 +234,23 @@ class QueryUpdater(ObjectUpdater):
object_type = "query"

@classmethod
def get_owners_ids(cls, target: "Query") -> List[int]:
def get_owners_ids(cls, target: Query) -> List[int]:
return [target.user_id]


class DatasetUpdater(ObjectUpdater):

object_type = "dataset"

@classmethod
def get_owners_ids(cls, target: SqlaTable) -> List[int]:
return [owner.id for owner in target.owners]


class FavStarUpdater:
@classmethod
def after_insert(
cls, _mapper: Mapper, connection: Connection, target: "FavStar"
cls, _mapper: Mapper, connection: Connection, target: FavStar
) -> None:
session = Session(bind=connection)
name = "favorited_by:{0}".format(target.user_id)
Expand All @@ -246,7 +266,7 @@ def after_insert(

@classmethod
def after_delete(
cls, _mapper: Mapper, connection: Connection, target: "FavStar"
cls, _mapper: Mapper, connection: Connection, target: FavStar
) -> None:
session = Session(bind=connection)
name = "favorited_by:{0}".format(target.user_id)
Expand Down
2 changes: 1 addition & 1 deletion superset/tasks/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from superset.models.core import Log
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.tags import Tag, TaggedObject
from superset.tags.models import Tag, TaggedObject
from superset.utils.date_parser import parse_human_datetime
from superset.utils.machine_auth import MachineAuthProvider

Expand Down
2 changes: 1 addition & 1 deletion superset/utils/url_map_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from werkzeug.routing import BaseConverter, Map

from superset.models.tags import ObjectTypes
from superset.tags.models import ObjectTypes


class RegexConverter(BaseConverter):
Expand Down
Loading