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.
