This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 7b65d222050 chore(tags): TAGGING_SYSTEM to True by default (#39888)
7b65d222050 is described below
commit 7b65d222050104bf3df183ddbdb59bc0b0cab917
Author: Sam Firke <[email protected]>
AuthorDate: Tue Sep 8 19:27:37 2026 -0400
chore(tags): TAGGING_SYSTEM to True by default (#39888)
Co-authored-by: rusackas <[email protected]>
---
UPDATING.md | 28 +++++++++++++++++++
docs/static/feature-flags.json | 12 ++++-----
superset/app.py | 13 +++++----
superset/config.py | 6 ++---
superset/tags/models.py | 31 ++++++++++++++++++++++
tests/integration_tests/base_tests.py | 19 +++++++++++++
tests/integration_tests/charts/api_tests.py | 8 +++++-
tests/integration_tests/charts/commands_tests.py | 14 +++++++---
.../integration_tests/dashboards/commands_tests.py | 3 +++
tests/integration_tests/fixtures/tags.py | 21 ++++++++++-----
tests/integration_tests/tags/api_tests.py | 8 +++++-
.../commands/importers/v1/assets_test.py | 3 +++
tests/unit_tests/initialization_test.py | 9 ++-----
tests/unit_tests/tags/commands/create_test.py | 19 +++++++++----
tests/unit_tests/tags/commands/update_test.py | 10 +++++--
15 files changed, 162 insertions(+), 42 deletions(-)
diff --git a/UPDATING.md b/UPDATING.md
index 786f4add82b..389d6b90370 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -24,6 +24,34 @@ assists people when migrating to a new version.
## Next
+### Tagging is on by default
+
+`TAGGING_SYSTEM` now ships **on**. The Tags menu entry, the tag columns and
+filters on the chart, dashboard and saved-query lists, and the Tags field in
the
+chart and dashboard property modals are all visible without configuration, and
+tags are included in asset export and import.
+
+**What operators should expect:**
+
+- **Implicit tags accrue.** Saving a chart, dashboard, dataset or saved query,
+ and favoriting an asset, write rows to `tag` and `tagged_object`
(`type:chart`,
+ `editor:<user id>`, `favorited_by:<user id>`). These have always been created
+ when the flag was on; they are simply no longer opt-in.
+- **Exports gain a `tags` key and a `tags.yaml` file.** Chart and dashboard
+ export bundles carry custom tags. Importers on 6.0 and later understand both;
+ older importers skip the unrecognized `tags.yaml` file but reject chart and
+ dashboard YAML that contains a `tags` key, so strip that key before importing
+ a bundle into Superset 5.x or earlier.
+- **The flag is honored at write time.** The tagging SQLA event listeners are
+ always attached at startup; the ones that create tags check `TAGGING_SYSTEM`
+ when they fire, so the flag, including a runtime override through
+ `GET_FEATURE_FLAGS_FUNC` or `IS_FEATURE_ENABLED_FUNC`, takes effect without a
+ restart. The cleanup listeners run regardless of the flag, so deleting an
+ asset never leaves orphaned `tagged_object` rows behind.
+
+Set `FEATURE_FLAGS = {"TAGGING_SYSTEM": False}` to restore the previous
+behavior. Existing tag rows are left untouched.
+
### Global Async Queries re-platformed onto the Global Task Framework
(breaking)
Global Async Queries (GAQ) no longer runs on its own bespoke async-events
diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json
index a34590d95ea..7a679395464 100644
--- a/docs/static/feature-flags.json
+++ b/docs/static/feature-flags.json
@@ -98,12 +98,6 @@
"default": false,
"lifecycle": "development",
"description": "Enable Table V2 time comparison feature"
- },
- {
- "name": "TAGGING_SYSTEM",
- "default": false,
- "lifecycle": "development",
- "description": "Enables the tagging system for organizing assets"
}
],
"testing": [
@@ -240,6 +234,12 @@
"description": "Allow users to enable SSH tunneling when creating a DB
connection. DB engine must support SSH Tunnels.",
"docs":
"https://superset.apache.org/docs/configuration/setup-ssh-tunneling"
},
+ {
+ "name": "TAGGING_SYSTEM",
+ "default": true,
+ "lifecycle": "testing",
+ "description": "Enables the tagging system for organizing assets"
+ },
{
"name": "USE_ANALOGOUS_COLORS",
"default": false,
diff --git a/superset/app.py b/superset/app.py
index fb7ba012b7a..2ce5bbf0484 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -184,9 +184,6 @@ class SupersetApp(Flask):
app startup.
"""
try:
- # Import here to avoid circular import issues
- from superset.extensions import feature_flag_manager
-
# Check if database is up-to-date with migrations
if not self._is_database_up_to_date():
logger.info("Pending database migrations: run 'superset db
upgrade'")
@@ -194,11 +191,13 @@ class SupersetApp(Flask):
logger.info("Syncing configuration to database...")
- # Register SQLA event listeners for tagging system
- if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
- from superset.tags.core import register_sqla_event_listeners
+ # Register SQLA event listeners for the tagging system. The
+ # listeners that create tags check TAGGING_SYSTEM when they fire,
+ # and the cleanup listeners must run regardless of the flag so a
+ # deleted object never leaves orphaned `tagged_object` rows behind.
+ from superset.tags.core import register_sqla_event_listeners
- register_sqla_event_listeners()
+ register_sqla_event_listeners()
# Seed system themes from configuration
from superset.commands.theme.seed import SeedSystemThemesCommand
diff --git a/superset/config.py b/superset/config.py
index 3a32ae85b81..eb20815eb56 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -740,9 +740,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# Enable Table V2 time comparison feature
# @lifecycle: development
"TABLE_V2_TIME_COMPARISON_ENABLED": False,
- # Enables the tagging system for organizing assets
- # @lifecycle: development
- "TAGGING_SYSTEM": False,
# Enables the version history panel on Explore and Dashboard pages.
# History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on;
# with capture off the panel renders empty or stale history, so the two
@@ -826,6 +823,9 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
# @lifecycle: testing
# @docs: https://superset.apache.org/docs/configuration/setup-ssh-tunneling
"SSH_TUNNELING": False,
+ # Enables the tagging system for organizing assets
+ # @lifecycle: testing
+ "TAGGING_SYSTEM": True,
# Enable AWS IAM authentication for database connections (Aurora,
Redshift).
# Allows cross-account role assumption via STS AssumeRole.
# Security note: When enabled, ensure Superset's IAM role has restricted
diff --git a/superset/tags/models.py b/superset/tags/models.py
index c60e660b23d..68425d9d917 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -170,6 +170,28 @@ def get_object_type(class_name: str) -> ObjectType:
) from ex
+def tagging_enabled() -> bool:
+ """
+ Whether the tagging system is enabled.
+
+ The SQLA event listeners below are attached unconditionally at app startup
+ (see ``superset.app.SupersetApp.sync_config_to_db``). Listeners that
+ *create* tags check the flag when they fire, so the flag, including a
+ runtime override, is honored on the write path the same way it is on the
+ UI, export and import paths. The ``after_delete`` listeners deliberately
+ skip this check: they only remove ``tagged_object`` rows, and skipping that
+ cleanup would orphan rows pointing at a deleted object whose id may later
+ be reused.
+ """
+ # Resolved through the manager on every call rather than bound at import
+ # time, so that patching the manager (as the tests do) takes effect.
+ from superset.extensions import ( # pylint:
disable=import-outside-toplevel
+ feature_flag_manager,
+ )
+
+ return feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM")
+
+
class ObjectUpdater:
object_type: str = "default"
@@ -235,6 +257,9 @@ class ObjectUpdater:
connection: Connection,
target: Dashboard | FavStar | Slice | Query | SqlaTable,
) -> None:
+ if not tagging_enabled():
+ return
+
with Session(bind=connection) as session: # pylint:
disable=disallowed-name
# add `editor:` tags
cls._add_editors(session, target)
@@ -253,6 +278,9 @@ class ObjectUpdater:
connection: Connection,
target: Dashboard | FavStar | Slice | Query | SqlaTable,
) -> None:
+ if not tagging_enabled():
+ return
+
with Session(bind=connection) as session: # pylint:
disable=disallowed-name
# Fetch current editor tags
existing_tags = (
@@ -345,6 +373,9 @@ class FavStarUpdater:
def after_insert(
cls, _mapper: Mapper, connection: Connection, target: FavStar
) -> None:
+ if not tagging_enabled():
+ return
+
with Session(bind=connection) as session: # pylint:
disable=disallowed-name
name = f"favorited_by:{target.user_id}"
tag = get_tag(name, session, TagType.favorited_by)
diff --git a/tests/integration_tests/base_tests.py
b/tests/integration_tests/base_tests.py
index aaf82ff1449..8bef7da4dff 100644
--- a/tests/integration_tests/base_tests.py
+++ b/tests/integration_tests/base_tests.py
@@ -45,6 +45,7 @@ from superset.models.slice import Slice
from superset.sql.parse import CTASMethod
from superset.subjects.models import Subject
from superset.subjects.types import SubjectType
+from superset.tags.models import Tag, TaggedObject
from superset.utils import json
from superset.utils.core import get_example_default_schema, shortid
from superset.utils.database import get_example_database
@@ -271,10 +272,28 @@ class SupersetTestCase(TestCase):
db.session.delete(temp_role)
if login:
self.logout()
+ self._release_tag_references(temp_user.id)
db.session.delete(temp_user)
db.session.commit()
g.user = previous_g_user
+ @staticmethod
+ def _release_tag_references(user_id: int) -> None:
+ """
+ Drop `ab_user` references held by rows the tagging system wrote.
+
+ Anything a user saves while `TAGGING_SYSTEM` is on stamps the audit
+ columns of the `tag` and `tagged_object` rows it creates, and those are
+ foreign keys. Deleting the user without clearing them fails with a
+ foreign key violation on backends that enforce them.
+ """
+ for model in (Tag, TaggedObject):
+ for column in ("created_by_fk", "changed_by_fk"):
+ db.session.query(model).filter(
+ getattr(model, column) == user_id
+ ).update({column: None}, synchronize_session=False)
+ db.session.commit()
+
@staticmethod
def create_user(
username: str,
diff --git a/tests/integration_tests/charts/api_tests.py
b/tests/integration_tests/charts/api_tests.py
index 71a0aefaa3d..048d5aa75ee 100644
--- a/tests/integration_tests/charts/api_tests.py
+++ b/tests/integration_tests/charts/api_tests.py
@@ -1103,7 +1103,6 @@ class TestChartApi(ApiEditorsTestCaseMixin,
InsertChartMixin, SupersetTestCase):
"viewers": [],
"params": None,
"slice_name": "title",
- "tags": [],
"viz_type": None,
"query_context": None,
"is_managed_externally": False,
@@ -1113,6 +1112,13 @@ class TestChartApi(ApiEditorsTestCaseMixin,
InsertChartMixin, SupersetTestCase):
assert "id" in data["result"]
assert "thumbnail_url" in data["result"]
assert "url" in data["result"]
+ # implicit tags created by the tagging system's SQLA event listeners
+ tags = data["result"].pop("tags")
+ assert len(tags) == 2
+ assert {(tag["name"], tag["type"]) for tag in tags} == {
+ ("type:chart", TagType.type.value),
+ (f"editor:{admin.id}", TagType.editor.value),
+ }
for key, value in data["result"].items():
# We can't assert timestamp values or id/urls
if key not in (
diff --git a/tests/integration_tests/charts/commands_tests.py
b/tests/integration_tests/charts/commands_tests.py
index 7a5e76b156d..40e7fbc32ea 100644
--- a/tests/integration_tests/charts/commands_tests.py
+++ b/tests/integration_tests/charts/commands_tests.py
@@ -83,6 +83,7 @@ class TestExportChartsCommand(SupersetTestCase):
f"charts/Energy_Sankey_{example_chart.id}.yaml",
f"datasets/examples/energy_usage_{example_chart.table.id}.yaml",
"databases/examples.yaml",
+ "tags.yaml",
]
assert expected == list(contents.keys())
@@ -109,6 +110,7 @@ class TestExportChartsCommand(SupersetTestCase):
"uuid": str(example_chart.uuid),
"version": "1.0.0",
"query_context": None,
+ "tags": [],
}
@patch("superset.utils.core.g")
@@ -161,6 +163,7 @@ class TestExportChartsCommand(SupersetTestCase):
"uuid",
"version",
"dataset_uuid",
+ "tags",
]
@patch("superset.security.manager.g")
@@ -548,6 +551,14 @@ class TestChartsUpdateCommand(SupersetTestCase):
chart = db.session.query(Slice).filter_by(slice_name="Energy
Sankey").one()
pk = chart.id
admin = security_manager.find_user(username="admin")
+
+ # gamma has no access to the energy datasource and cannot edit the
chart.
+ # Bind the patched `g` to a real user before the setup commit below:
+ # that commit fires the tagging listeners, whose audit columns resolve
+ # through `g.user.id` and cannot be bound as a mock.
+ gamma = security_manager.find_user(username="gamma")
+ mock_core_g.user = mock_sm_g.user = mock_update_g.user = gamma
+
chart.editors = subjects_from_users([admin])
db.session.commit()
@@ -555,9 +566,6 @@ class TestChartsUpdateCommand(SupersetTestCase):
# own raise_for_access gate is what denies the request.
mock_find_by_id.return_value = chart
- # gamma has no access to the energy datasource and cannot edit the
chart
- gamma = security_manager.find_user(username="gamma")
- mock_core_g.user = mock_sm_g.user = mock_update_g.user = gamma
json_obj = {
"query_context_generation": True,
"query_context": json.dumps({"foo": "bar"}),
diff --git a/tests/integration_tests/dashboards/commands_tests.py
b/tests/integration_tests/dashboards/commands_tests.py
index 2ed70e1e4c9..17503039034 100644
--- a/tests/integration_tests/dashboards/commands_tests.py
+++ b/tests/integration_tests/dashboards/commands_tests.py
@@ -80,6 +80,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
"metadata.yaml",
f"dashboards/World_Banks_Data_{example_dashboard.id}.yaml",
"databases/examples.yaml",
+ "tags.yaml",
}
for chart in example_dashboard.slices:
chart_slug = secure_filename(chart.slice_name)
@@ -238,6 +239,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
"metadata": {"mock_key": "mock_value"},
"version": "1.0.0",
"theme_uuid": None,
+ "tags": [],
}
# @pytest.mark.usefixtures("load_covid_dashboard")
@@ -343,6 +345,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
"position",
"metadata",
"version",
+ "tags",
}
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
diff --git a/tests/integration_tests/fixtures/tags.py
b/tests/integration_tests/fixtures/tags.py
index 8fb42701509..2158cef5456 100644
--- a/tests/integration_tests/fixtures/tags.py
+++ b/tests/integration_tests/fixtures/tags.py
@@ -18,20 +18,27 @@
import pytest
from superset import db
-from superset.tags.core import clear_sqla_event_listeners,
register_sqla_event_listeners
+from superset.tags.core import register_sqla_event_listeners
from superset.tags.models import Tag
from tests.integration_tests.test_app import app
@pytest.fixture
def with_tagging_system_feature():
- is_enabled = app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"]
- if not is_enabled:
- app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = True
- register_sqla_event_listeners()
+ was_enabled = app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"]
+ app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = True
+ # Idempotent: SQLAlchemy keys listeners by (target, event, fn), so
+ # re-registering on top of the app-startup registration is a no-op. Calling
+ # it keeps these tests independent of whether startup registration ran (it
+ # is skipped when the metadata DB is behind on migrations).
+ register_sqla_event_listeners()
+ try:
yield
- app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = False
- clear_sqla_event_listeners()
+ finally:
+ # Only the flag is restored. The listeners are shared app-level state
+ # that startup registered unconditionally, and they check the flag when
+ # they fire, so unregistering here would break every later test.
+ app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = was_enabled
@pytest.fixture
diff --git a/tests/integration_tests/tags/api_tests.py
b/tests/integration_tests/tags/api_tests.py
index 15d0ec2ec0b..d19f16d2926 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -697,7 +697,13 @@ class TestTagApi(InsertChartMixin, SupersetTestCase):
@pytest.mark.usefixtures("create_tags")
def test_add_tag_not_found(self):
self.login(ADMIN_USERNAME)
- uri = "api/v1/tag/123/favorites/" # noqa: F541
+
+ # Pick an id that is genuinely free: the tagging system mints implicit
+ # tags as assets are saved, so no fixed id stays unused.
+ existing_ids = [tag_id for (tag_id,) in db.session.query(Tag.id).all()]
+ non_existent_id = max(existing_ids, default=0) + 1
+
+ uri = f"api/v1/tag/{non_existent_id}/favorites/"
rv = self.client.post(uri, follow_redirects=True)
assert rv.status_code == 404
diff --git a/tests/unit_tests/commands/importers/v1/assets_test.py
b/tests/unit_tests/commands/importers/v1/assets_test.py
index 307525d1a1a..8da939dff67 100644
--- a/tests/unit_tests/commands/importers/v1/assets_test.py
+++ b/tests/unit_tests/commands/importers/v1/assets_test.py
@@ -285,6 +285,9 @@ def test_import_threads_overwrite_flag(mocker:
MockerFixture, session: Session)
mocker.patch.object(assets_module, "find_chart_uuids", return_value=[])
mocker.patch.object(assets_module, "update_id_refs", side_effect=lambda c,
*_: c)
mocker.patch.object(assets_module, "migrate_dashboard")
+ # tag import isn't under test here; mock it out so this test doesn't
+ # depend on the TAGGING_SYSTEM feature flag default or a real tag table
+ mocker.patch.object(assets_module, "import_tag")
mocker.patch("superset.db.session.execute")
configs = {
diff --git a/tests/unit_tests/initialization_test.py
b/tests/unit_tests/initialization_test.py
index fea1d86b32f..c995d11f6a6 100644
--- a/tests/unit_tests/initialization_test.py
+++ b/tests/unit_tests/initialization_test.py
@@ -78,20 +78,17 @@ class TestSupersetApp:
"Pending database migrations: run 'superset db upgrade'"
)
- @patch("superset.extensions.feature_flag_manager")
@patch("superset.app.logger")
@patch("superset.commands.theme.seed.SeedSystemThemesCommand")
def test_sync_config_to_db_initializes_when_tables_exist(
self,
mock_seed_themes_command,
mock_logger,
- mock_feature_flag_manager,
):
"""Test that features are initialized when database is up-to-date."""
# Setup
app = SupersetApp(__name__)
app.config = {"SQLALCHEMY_DATABASE_URI":
"postgresql://user:pass@host:5432/db"}
- mock_feature_flag_manager.is_feature_enabled.return_value = True
mock_seed_themes = MagicMock()
mock_seed_themes_command.return_value = mock_seed_themes
@@ -105,10 +102,8 @@ class TestSupersetApp:
# Execute
app.sync_config_to_db()
- # Assert
- mock_feature_flag_manager.is_feature_enabled.assert_called_with(
- "TAGGING_SYSTEM"
- )
+ # Assert: tagging listeners are registered regardless of the flag; the
+ # write listeners check TAGGING_SYSTEM themselves when they fire
mock_register_listeners.assert_called_once()
# Should seed themes
mock_seed_themes_command.assert_called_once()
diff --git a/tests/unit_tests/tags/commands/create_test.py
b/tests/unit_tests/tags/commands/create_test.py
index 292e343f3fb..72509498043 100644
--- a/tests/unit_tests/tags/commands/create_test.py
+++ b/tests/unit_tests/tags/commands/create_test.py
@@ -72,7 +72,7 @@ def test_create_command_success(session_with_data: Session,
mocker: MockerFixtur
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
- from superset.tags.models import ObjectType, TaggedObject
+ from superset.tags.models import ObjectType, Tag, TaggedObject
# Define a list of objects to tag
query = db.session.query(SavedQuery).first()
@@ -95,11 +95,17 @@ def test_create_command_success(session_with_data: Session,
mocker: MockerFixtur
data={"name": "test_tag", "objects_to_tag": objects_to_tag}
).run()
- assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+ # Scope to the tag under test: with TAGGING_SYSTEM on, saving the fixture's
+ # assets also mints implicit `type:` tags, so a global count is unstable.
+ tag = db.session.query(Tag).filter_by(name="test_tag").one()
+ assert len(db.session.query(TaggedObject).filter_by(tag_id=tag.id).all())
== len(
+ objects_to_tag
+ )
for object_type, object_id in objects_to_tag:
assert (
db.session.query(TaggedObject)
.filter(
+ TaggedObject.tag_id == tag.id,
TaggedObject.object_type == object_type,
TaggedObject.object_id == object_id,
)
@@ -184,7 +190,7 @@ def test_create_command_success_clear(
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import SavedQuery
- from superset.tags.models import ObjectType, TaggedObject
+ from superset.tags.models import ObjectType, Tag, TaggedObject
# Define a list of objects to tag
query = db.session.query(SavedQuery).first()
@@ -206,10 +212,13 @@ def test_create_command_success_clear(
CreateCustomTagWithRelationshipsCommand(
data={"name": "test_tag", "objects_to_tag": objects_to_tag}
).run()
- assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+ tag = db.session.query(Tag).filter_by(name="test_tag").one()
+ assert len(db.session.query(TaggedObject).filter_by(tag_id=tag.id).all())
== len(
+ objects_to_tag
+ )
CreateCustomTagWithRelationshipsCommand(
data={"name": "test_tag", "objects_to_tag": []}
).run()
- assert len(db.session.query(TaggedObject).all()) == 0
+ assert len(db.session.query(TaggedObject).filter_by(tag_id=tag.id).all())
== 0
diff --git a/tests/unit_tests/tags/commands/update_test.py
b/tests/unit_tests/tags/commands/update_test.py
index 0263968dd9f..de425ddfdf5 100644
--- a/tests/unit_tests/tags/commands/update_test.py
+++ b/tests/unit_tests/tags/commands/update_test.py
@@ -107,7 +107,11 @@ def test_update_command_success(session_with_data:
Session, mocker: MockerFixtur
updated_tag = TagDAO.find_by_name("new_name")
assert updated_tag is not None
assert updated_tag.description == "new_description"
- assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+ # Scope to the tag under test: with TAGGING_SYSTEM on, saving the fixture's
+ # assets also mints implicit `type:` tags, so a global count is unstable.
+ assert len(
+ db.session.query(TaggedObject).filter_by(tag_id=updated_tag.id).all()
+ ) == len(objects_to_tag)
def test_update_command_success_duplicates(
@@ -156,7 +160,9 @@ def test_update_command_success_duplicates(
updated_tag = TagDAO.find_by_name("new_name")
assert updated_tag is not None
assert updated_tag.description == "new_description"
- assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+ assert len(
+ db.session.query(TaggedObject).filter_by(tag_id=updated_tag.id).all()
+ ) == len(objects_to_tag)
assert changed_model.objects[0].object_id == chart.id