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

michael-s-molina pushed a commit to branch 6.2
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 55d2e54c8eb85fc26a7d283e6183a002e81ae378
Author: Luiz Otavio <[email protected]>
AuthorDate: Fri Jul 10 16:38:21 2026 -0300

    fix(jinja): drill-to-detail respects remove_filter=True in Jinja templates 
(#41934)
    
    Co-authored-by: Michael S. Molina 
<[email protected]>
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
    (cherry picked from commit c898341a82cf180612318a3108eda2248226f156)
---
 superset/jinja_context.py              | 37 +++++++++++++
 tests/unit_tests/jinja_context_test.py | 95 ++++++++++++++++++++++++++++++++++
 2 files changed, 132 insertions(+)

diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 15b8d0495a1..c0b26333f61 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -141,6 +141,7 @@ class ExtraCache:
         database: Database | None = None,
         dialect: Dialect | None = None,
         table: SqlaTable | None = None,
+        query_context_filters: list[Any] | None = None,
     ):
         self.extra_cache_keys = extra_cache_keys
         self.applied_filters = applied_filters if applied_filters is not None 
else []
@@ -148,6 +149,7 @@ class ExtraCache:
         self.database = database
         self.dialect = dialect
         self.table = table
+        self.query_context_filters: list[Any] = query_context_filters or []
 
     def current_user_id(self, add_to_cache_keys: bool = True) -> int | None:
         """
@@ -444,6 +446,40 @@ class ExtraCache:
 
                 filters.append({"op": op, "col": column, "val": val})
 
+        # Drill-to-detail queries send filters in native {col, op, val} format
+        # rather than adhoc_filters, so get_form_data() above finds nothing.
+        # query_context_filters carries those native filters from
+        # template_kwargs["filter"], already available in the Jinja context.
+        # Only consult them when adhoc_filters produced no match to avoid
+        # duplicating entries for aggregated queries where both formats exist.
+        if not filters:
+            filters = self._get_filters_from_query_context(column, 
remove_filter)
+
+        return filters
+
+    def _get_filters_from_query_context(
+        self, column: str, remove_filter: bool
+    ) -> list[Filter]:
+        filters: list[Filter] = []
+        for flt in self.query_context_filters:
+            col = flt.get("col")
+            val = flt.get("val")
+            op = (flt.get("op") or FilterOperator.IN).upper()
+            if col != column or (
+                val is None
+                and op not in ("IS NULL", "IS NOT NULL", "IS_NULL", 
"IS_NOT_NULL")
+            ):
+                continue
+            if op in (
+                FilterOperator.IN,
+                FilterOperator.NOT_IN,
+            ) and not isinstance(val, list):
+                val = [val]
+            if remove_filter and column not in self.removed_filters:
+                self.removed_filters.append(column)
+            if column not in self.applied_filters:
+                self.applied_filters.append(column)
+            filters.append({"op": op, "col": column, "val": val})
         return filters
 
     # pylint: disable=too-many-arguments
@@ -808,6 +844,7 @@ class JinjaTemplateProcessor(BaseTemplateProcessor):
             database=self._database,
             dialect=self._database.get_dialect(),
             table=self._table,
+            query_context_filters=self._context.get("filter") or [],
         )
 
         from_dttm = (
diff --git a/tests/unit_tests/jinja_context_test.py 
b/tests/unit_tests/jinja_context_test.py
index 9ba84315fac..cfdbc2090d0 100644
--- a/tests/unit_tests/jinja_context_test.py
+++ b/tests/unit_tests/jinja_context_test.py
@@ -259,6 +259,101 @@ def test_get_filters_remove_not_present() -> None:
     assert cache.removed_filters == []
 
 
+def test_get_filters_query_context_filters() -> None:
+    """
+    Test that ``get_filters`` falls back to native query_context_filters when 
no
+    adhoc_filters are present — the drill-to-detail path sends filters in 
native
+    {col, op, val} format rather than adhoc_filters.
+    """
+    cache = ExtraCache(
+        query_context_filters=[{"col": "name", "op": "IN", "val": ["foo", 
"bar"]}]
+    )
+    assert cache.get_filters("name") == [
+        {"op": "IN", "col": "name", "val": ["foo", "bar"]}
+    ]
+    assert cache.applied_filters == ["name"]
+    assert cache.removed_filters == []
+
+
+def test_get_filters_query_context_filters_remove_filter() -> None:
+    """
+    Test that ``get_filters`` with ``remove_filter=True`` marks the column as 
removed
+    when matching via query_context_filters.
+    """
+    cache = ExtraCache(
+        query_context_filters=[{"col": "name", "op": "IN", "val": "foo"}]
+    )
+    assert cache.get_filters("name", remove_filter=True) == [
+        {"op": "IN", "col": "name", "val": ["foo"]}
+    ]
+    assert cache.removed_filters == ["name"]
+    assert cache.applied_filters == ["name"]
+
+
+def test_get_filters_query_context_filters_is_null() -> None:
+    """
+    Test that IS_NULL filters (which have no val) are returned correctly from
+    query_context_filters. Unary null operators legitimately have val=None.
+    """
+    cache = ExtraCache(query_context_filters=[{"col": "name", "op": 
"IS_NULL"}])
+    assert cache.get_filters("name") == [{"op": "IS_NULL", "col": "name", 
"val": None}]
+    assert cache.applied_filters == ["name"]
+    assert cache.removed_filters == []
+
+
+def test_get_filters_query_context_filters_is_not_null() -> None:
+    """
+    Test that IS_NOT_NULL filters (which have no val) are returned correctly 
from
+    query_context_filters. Unary null operators legitimately have val=None.
+    """
+    cache = ExtraCache(query_context_filters=[{"col": "name", "op": 
"IS_NOT_NULL"}])
+    assert cache.get_filters("name") == [
+        {"op": "IS_NOT_NULL", "col": "name", "val": None}
+    ]
+    assert cache.applied_filters == ["name"]
+    assert cache.removed_filters == []
+
+
+def 
test_get_filters_adhoc_filters_take_precedence_over_query_context_filters() -> 
None:
+    """
+    Test that adhoc_filters takes precedence over query_context_filters to 
avoid
+    duplicate filter entries for aggregated queries where both formats are 
present.
+    """
+    with current_app.test_request_context(
+        data={
+            "form_data": json.dumps(
+                {
+                    "adhoc_filters": [
+                        {
+                            "clause": "WHERE",
+                            "comparator": ["adhoc_val"],
+                            "expressionType": "SIMPLE",
+                            "operator": "in",
+                            "subject": "name",
+                        }
+                    ],
+                }
+            )
+        }
+    ):
+        cache = ExtraCache(
+            query_context_filters=[{"col": "name", "op": "IN", "val": 
["native_val"]}]
+        )
+        result = cache.get_filters("name")
+        assert result == [{"op": "IN", "col": "name", "val": ["adhoc_val"]}]
+
+
+def test_filter_values_query_context_filters() -> None:
+    """
+    Test that ``filter_values`` works via query_context_filters 
(drill-to-detail path).
+    """
+    cache = ExtraCache(
+        query_context_filters=[{"col": "name", "op": "IN", "val": ["foo", 
"bar"]}]
+    )
+    assert cache.filter_values("name") == ["foo", "bar"]
+    assert cache.applied_filters == ["name"]
+
+
 def test_url_param_query() -> None:
     """
     Test the ``url_param`` macro.

Reply via email to