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

eschutho pushed a commit to branch 
fix-dashboard-position-non-dict-filter-scope-2
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 24a244589bdc740c2344ed065f19d12001a6c5c9
Author: Elizabeth Thompson <[email protected]>
AuthorDate: Wed Sep 23 15:12:32 2026 +0000

    fix(dashboard): don't 500 deriving filter scopes from a non-mapping layout 
(SC-121722)
    
    Dashboard.position returns whatever position_json parses to, so a layout
    stored as a JSON string or array reaches build_chart_layout_items, which
    called .values() on it and made GET /api/v1/dashboard/<id> raise
    AttributeError. Treat a non-mapping layout as placing no charts, the same
    tolerance Dashboard.tabs already applies to this case.
    
    Fixes SUPERSET-PYTHON-17F0
    
    Co-Authored-By: Claude <[email protected]>
---
 superset/dashboards/filter_scope.py              | 12 ++++-
 tests/unit_tests/dashboards/filter_scope_test.py | 58 ++++++++++++++++++++++++
 2 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/superset/dashboards/filter_scope.py 
b/superset/dashboards/filter_scope.py
index 9454fc7d35f..eedcca28974 100644
--- a/superset/dashboards/filter_scope.py
+++ b/superset/dashboards/filter_scope.py
@@ -36,6 +36,7 @@ and ``getCrossFiltersConfiguration``.
 
 from __future__ import annotations
 
+import logging
 import re
 from typing import Any, TYPE_CHECKING
 
@@ -44,6 +45,8 @@ from superset.utils import json
 if TYPE_CHECKING:
     from superset.models.dashboard import Dashboard
 
+logger = logging.getLogger(__name__)
+
 CHART_TYPE = "CHART"
 TAB_TYPE = "TAB"
 NATIVE_FILTER_DIVIDER_PREFIX = "NATIVE_FILTER_DIVIDER-"
@@ -57,8 +60,15 @@ ChartLayoutItems = dict[int, list[dict[str, Any]]]
 
 
 def build_chart_layout_items(position_data: dict[str, Any]) -> 
ChartLayoutItems:
-    """Map each chart id in the layout to the layout items that render it."""
+    """Map each chart id in the layout to the layout items that render it.
+
+    A layout that is not a mapping (``position_json`` holding a JSON string or
+    array) places no charts, so every derived scope comes out empty.
+    """
     chart_layout_items: ChartLayoutItems = {}
+    if not isinstance(position_data, dict):
+        logger.warning("Dashboard layout is not a mapping; no charts are in 
scope")
+        return chart_layout_items
     for item in position_data.values():
         if not isinstance(item, dict) or item.get("type") != CHART_TYPE:
             continue
diff --git a/tests/unit_tests/dashboards/filter_scope_test.py 
b/tests/unit_tests/dashboards/filter_scope_test.py
index 3e95cf28d19..f523e9102f3 100644
--- a/tests/unit_tests/dashboards/filter_scope_test.py
+++ b/tests/unit_tests/dashboards/filter_scope_test.py
@@ -15,10 +15,14 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import logging
 from types import SimpleNamespace
 from typing import Any
 
+import pytest
+
 from superset.dashboards.filter_scope import (
+    build_chart_layout_items,
     derive_json_metadata,
     derive_metadata_scopes,
     derive_scopes,
@@ -231,3 +235,57 @@ def 
test_derive_json_metadata_passes_through_unparsable_metadata() -> None:
 
     assert derive_json_metadata(dashboard, "not json") == "not json"  # type: 
ignore[arg-type]
     assert derive_json_metadata(dashboard, "[]") == "[]"  # type: 
ignore[arg-type]
+
+
+def test_derive_scopes_when_layout_is_not_a_mapping(
+    caplog: pytest.LogCaptureFixture,
+) -> None:
+    """Valid JSON that is not an object yields empty scopes instead of raising.
+
+    ``Dashboard.position`` hands back whatever ``position_json`` parses to, so 
a
+    layout stored as a JSON string or array reaches the derivation as-is.
+    """
+    caplog.set_level(logging.WARNING, 
logger="superset.dashboards.filter_scope")
+    metadata = {
+        "native_filter_configuration": [
+            {
+                "id": "NATIVE_FILTER-1",
+                "scope": {"rootPath": ["ROOT_ID"], "excluded": []},
+                "chartsInScope": [1, 2],
+                "tabsInScope": ["TAB-1"],
+            }
+        ],
+    }
+
+    position_data: Any
+    for position_data in ("not a layout", [], None):
+        assert build_chart_layout_items(position_data) == {}
+        derived = derive_scopes(metadata, position_data, CHART_IDS)
+        native_filter = derived["native_filter_configuration"][0]
+        assert native_filter["chartsInScope"] == []
+        assert native_filter["tabsInScope"] == []
+
+    assert caplog.text.count("layout is not a mapping") == 6
+
+
+def test_derive_json_metadata_when_layout_is_not_a_mapping() -> None:
+    """The ``GET /api/v1/dashboard/{id}`` path for a string 
``position_json``."""
+    dashboard = SimpleNamespace(
+        position=json.loads(json.dumps("not a layout")),
+        slices=[SimpleNamespace(id=1)],
+    )
+    stored = json.dumps(
+        {
+            "native_filter_configuration": [
+                {
+                    "id": "NATIVE_FILTER-1",
+                    "scope": {"rootPath": ["ROOT_ID"], "excluded": []},
+                    "chartsInScope": [1],
+                }
+            ]
+        }
+    )
+
+    derived = json.loads(derive_json_metadata(dashboard, stored))  # type: 
ignore[arg-type]
+
+    assert derived["native_filter_configuration"][0]["chartsInScope"] == []

Reply via email to