You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by kg...@apache.org on 2026/05/29 16:26:27 UTC

(superset) 20/23: refactor(activity-view): JSON entity_kind uses user-facing labels

This is an automated email from the ASF dual-hosted git repository.

kgabryje pushed a commit to branch sc-107283-versioning-activity-view
in repository https://gitbox.apache.org/repos/asf/superset.git

commit ec851849fba5bb3799fdc911ae03fa2ca2e8bb27
Author: Mike Bridge <mi...@ext.preset.io>
AuthorDate: Wed May 27 10:29:07 2026 -0600

    refactor(activity-view): JSON entity_kind uses user-facing labels
    
    Breaking change to the (still-unmerged) activity-view JSON contract:
    ``entity_kind`` now emits ``"dashboard"`` / ``"chart"`` / ``"dataset"``
    instead of the Python class names ``"Dashboard"`` / ``"Slice"`` /
    ``"SqlaTable"``. The class names are developer-facing artifacts that
    leaked the model layer (e.g. ``"Slice"`` predates the UI rename to
    "chart"; ``"SqlaTable"`` predates "dataset"). User-facing JSON should
    speak user language.
    
    Implementation: a new ``_USER_FACING_KIND`` map translates at JSON
    serialization time only (in ``_decorate_records``). Internal code keeps
    the Python class-name form (``model_cls.__name__``) for dispatch — the
    existing ``_NAME_COLUMN``, ``_NOT_FOUND_EXC``, ``_API_KIND_LABEL``,
    ``_can_read``, ``_compute_impact``, etc. all key off class names and
    are unchanged. The translation happens at the single ``record["entity_kind"]
    = ...`` assignment.
    
    Schema validator ``ACTIVITY_ENTITY_KINDS`` updated to the new tuple.
    Integration tests' response-shape assertions renamed via bulk sed; unit
    tests testing internal helpers are unchanged (they operate on internal
    api_kind / class names).
    
    UPDATING.md example payload updated. Spec updates (spec.md, data-model.md,
    contracts/activity-view.yaml) committed separately to the spec repo.
    
    Full suite: 66 unit + 35 integration + 1 xfailed.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
---
 UPDATING.md                                        |  2 +-
 superset/versioning/activity.py                    | 20 ++++++++++++++++++--
 superset/versioning/schemas.py                     | 16 ++++++++++++----
 .../versioning/activity_view_tests.py              | 22 +++++++++++-----------
 4 files changed, 42 insertions(+), 18 deletions(-)

diff --git a/UPDATING.md b/UPDATING.md
index 61d6f43fa23..31ccc9d9143 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -103,7 +103,7 @@ A read-only companion to the version-history endpoints (above). Each entity type
   "result": [
     {
       "version_uuid": "...",
-      "entity_kind": "Slice",
+      "entity_kind": "chart",
       "entity_uuid": "...",
       "entity_name": "Top 10 Girls",
       "entity_deleted": false,
diff --git a/superset/versioning/activity.py b/superset/versioning/activity.py
index 9b171f995a1..d91cef4d244 100644
--- a/superset/versioning/activity.py
+++ b/superset/versioning/activity.py
@@ -88,13 +88,26 @@ _TABLE_KIND_TO_API: dict[str, str] = {
 _API_KIND_TO_TABLE: dict[str, str] = dict(_ENTITY_KIND_BY_CLASS_NAME)
 
 # Human-readable label for AV-012 summary headlines
-# ("Dataset updated: Sales Transactions"). Keyed by the API kind.
+# ("Dataset updated: Sales Transactions"). Keyed by the internal API kind
+# (Python class name; matches ``model_cls.__name__``).
 _API_KIND_LABEL: dict[str, str] = {
     "Dashboard": "Dashboard",
     "Slice": "Chart",
     "SqlaTable": "Dataset",
 }
 
+# User-facing lowercase rendering of the kind. This is what appears in
+# the JSON response's ``entity_kind`` field and the
+# ``ActivityRecordSchema.entity_kind`` enum. Internal code keeps the
+# Python class-name form because it matches ``model_cls.__name__`` and is
+# convenient for dispatch — translation happens at serialization time
+# only, in :func:`_decorate_records`.
+_USER_FACING_KIND: dict[str, str] = {
+    "Dashboard": "dashboard",
+    "Slice": "chart",
+    "SqlaTable": "dataset",
+}
+
 # 404 exception class per API kind. Each accepts a string positional arg
 # (the path-entity UUID) that gets formatted into the exception message.
 _NOT_FOUND_EXC: dict[str, type[Exception]] = {
@@ -862,7 +875,10 @@ def _decorate_records(
         entity_uuid = uuids.get((api_kind, entity_id))
         is_self = api_kind == path_kind and entity_id == path_id
 
-        record["entity_kind"] = api_kind
+        # Emit the user-facing form ("dashboard"/"chart"/"dataset") on the
+        # wire; the internal class-name (api_kind) is kept above for the
+        # remaining decoration steps that key off model_cls.__name__.
+        record["entity_kind"] = _USER_FACING_KIND.get(api_kind, api_kind)
         record["entity_uuid"] = str(entity_uuid) if entity_uuid else None
         record["entity_deleted"] = tombstone["deleted"]
         record["entity_deletion_state"] = tombstone["deletion_state"]
diff --git a/superset/versioning/schemas.py b/superset/versioning/schemas.py
index e1d7ce7719e..38acc48014b 100644
--- a/superset/versioning/schemas.py
+++ b/superset/versioning/schemas.py
@@ -130,9 +130,12 @@ class VersionListResponseSchema(Schema):
 
 # ---- Cross-entity activity view (sc-107283) -------------------------------
 
-#: Allowed values for ``ActivityRecordSchema.entity_kind``. Mirrors the model
-#: classes registered with Continuum versioning (sc-103156).
-ACTIVITY_ENTITY_KINDS: tuple[str, ...] = ("Dashboard", "Slice", "SqlaTable")
+#: Allowed values for ``ActivityRecordSchema.entity_kind``. User-facing
+#: lowercase strings; the activity layer's internal kind dispatch keys off
+#: ``model_cls.__name__`` (``Dashboard`` / ``Slice`` / ``SqlaTable``) and
+#: translates to these labels at the JSON boundary in
+#: :func:`superset.versioning.activity._decorate_records`.
+ACTIVITY_ENTITY_KINDS: tuple[str, ...] = ("dashboard", "chart", "dataset")
 
 #: Allowed values for ``ActivityRecordSchema.source`` (spec AV-013).
 ACTIVITY_SOURCES: tuple[str, ...] = ("self", "related")
@@ -212,7 +215,12 @@ class ActivityRecordSchema(Schema):
     )
     entity_kind = fields.String(
         validate=validate.OneOf(ACTIVITY_ENTITY_KINDS),
-        metadata={"description": "Model class of the source entity."},
+        metadata={
+            "description": (
+                "User-facing kind of the source entity: one of "
+                '``"dashboard"`` / ``"chart"`` / ``"dataset"``.'
+            )
+        },
     )
     entity_uuid = fields.String(
         allow_none=True,
diff --git a/tests/integration_tests/versioning/activity_view_tests.py b/tests/integration_tests/versioning/activity_view_tests.py
index 118796f122c..1b15279fe72 100644
--- a/tests/integration_tests/versioning/activity_view_tests.py
+++ b/tests/integration_tests/versioning/activity_view_tests.py
@@ -172,7 +172,7 @@ class TestDashboardActivityView(SupersetTestCase):
             related = [
                 r
                 for r in body["result"]
-                if r["entity_kind"] == "Slice" and r["source"] == "related"
+                if r["entity_kind"] == "chart" and r["source"] == "related"
             ]
             assert related, (
                 "Expected at least one Slice/related record from the chart "
@@ -213,7 +213,7 @@ class TestDashboardActivityView(SupersetTestCase):
                 assert record["source"] == "self", (
                     f"include=self leaked a non-self record: {record}"
                 )
-                assert record["entity_kind"] == "Dashboard"
+                assert record["entity_kind"] == "dashboard"
         finally:
             db.session.rollback()
             chart = db.session.query(Slice).filter(Slice.id == chart_id).one()
@@ -250,7 +250,7 @@ class TestDashboardActivityView(SupersetTestCase):
                 assert record["source"] == "related", (
                     f"include=related leaked a self record: {record}"
                 )
-                assert record["entity_kind"] != "Dashboard"
+                assert record["entity_kind"] != "dashboard"
         finally:
             db.session.rollback()
             dashboard = (
@@ -361,7 +361,7 @@ class TestDashboardActivityView(SupersetTestCase):
             tombstoned = [
                 r
                 for r in body["result"]
-                if r["entity_kind"] == "Slice" and r["entity_deleted"] is True
+                if r["entity_kind"] == "chart" and r["entity_deleted"] is True
             ]
             seen = [
                 (r["entity_kind"], r["entity_deleted"]) for r in body["result"][:10]
@@ -584,7 +584,7 @@ class TestDashboardActivityView(SupersetTestCase):
             restore_records = [
                 r
                 for r in body["result"]
-                if r["kind"] == "restore" and r["entity_kind"] == "Dashboard"
+                if r["kind"] == "restore" and r["entity_kind"] == "dashboard"
             ]
             assert restore_records, (
                 "Expected at least one kind='restore' Dashboard record; "
@@ -685,7 +685,7 @@ class TestChartActivityView(SupersetTestCase):
             self_records = [
                 r
                 for r in body["result"]
-                if r["entity_kind"] == "Slice" and r["source"] == "self"
+                if r["entity_kind"] == "chart" and r["source"] == "self"
             ]
             got = [(r["entity_kind"], r["source"]) for r in body["result"]]
             assert self_records, (
@@ -720,7 +720,7 @@ class TestChartActivityView(SupersetTestCase):
             related = [
                 r
                 for r in body["result"]
-                if r["entity_kind"] == "SqlaTable" and r["source"] == "related"
+                if r["entity_kind"] == "dataset" and r["source"] == "related"
             ]
             assert related, (
                 "Expected at least one SqlaTable/related record from the "
@@ -760,7 +760,7 @@ class TestChartActivityView(SupersetTestCase):
             assert rv.status_code == 200
             body = _json.loads(rv.data.decode("utf-8"))
             for record in body["result"]:
-                assert record["entity_kind"] != "Dashboard", (
+                assert record["entity_kind"] != "dashboard", (
                     f"Dashboard edit leaked into chart's activity stream: {record}"
                 )
         finally:
@@ -792,7 +792,7 @@ class TestChartActivityView(SupersetTestCase):
             body = _json.loads(rv.data.decode("utf-8"))
             for record in body["result"]:
                 assert record["source"] == "self"
-                assert record["entity_kind"] == "Slice"
+                assert record["entity_kind"] == "chart"
         finally:
             db.session.rollback()
             dataset = (
@@ -883,7 +883,7 @@ class TestDatasetActivityView(SupersetTestCase):
             self_records = [
                 r
                 for r in body["result"]
-                if r["entity_kind"] == "SqlaTable" and r["source"] == "self"
+                if r["entity_kind"] == "dataset" and r["source"] == "self"
             ]
             got = [(r["entity_kind"], r["source"]) for r in body["result"]]
             assert self_records, (
@@ -921,7 +921,7 @@ class TestDatasetActivityView(SupersetTestCase):
             assert rv.status_code == 200
             body = _json.loads(rv.data.decode("utf-8"))
             for record in body["result"]:
-                assert record["entity_kind"] == "SqlaTable", (
+                assert record["entity_kind"] == "dataset", (
                     "Non-dataset record leaked into dataset's activity "
                     f"stream: {record}"
                 )