You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/04/23 19:22:11 UTC
(superset) branch 5.0-pulse updated (e8150757100 -> ebcdf727baa)
This is an automated email from the ASF dual-hosted git repository.
michael-s-molina pushed a change to branch 5.0-pulse
in repository https://gitbox.apache.org/repos/asf/superset.git
discard e8150757100 fix(jinja): drill-to-detail respects remove_filter=True in Jinja templates (#39594)
new ebcdf727baa fix(jinja): drill-to-detail respects remove_filter=True in Jinja templates (#39594)
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (e8150757100)
\
N -- N -- N refs/heads/5.0-pulse (ebcdf727baa)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
tests/unit_tests/jinja_context_test.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(superset) 01/01: fix(jinja): drill-to-detail respects remove_filter=True in Jinja templates (#39594)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michael-s-molina pushed a commit to branch 5.0-pulse
in repository https://gitbox.apache.org/repos/asf/superset.git
commit ebcdf727baae6e74d5e0d5f611a571ddc25a410a
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Thu Apr 23 16:11:13 2026 -0300
fix(jinja): drill-to-detail respects remove_filter=True in Jinja templates (#39594)
Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
(cherry picked from commit 362e5bf45e9622abedea4550411c81396659d7dc)
---
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 2baece8941f..b4509f96815 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -122,6 +122,7 @@ class ExtraCache:
database: Optional[Database] = None,
dialect: Optional[Dialect] = None,
table: Optional[SqlaTable] = None,
+ query_context_filters: Optional[list[Any]] = None,
):
self.extra_cache_keys = extra_cache_keys
self.applied_filters = applied_filters if applied_filters is not None else []
@@ -129,6 +130,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) -> Optional[int]:
"""
@@ -381,6 +383,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
@@ -646,6 +682,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 8a57f2e2374..e9f9b1a3141 100644
--- a/tests/unit_tests/jinja_context_test.py
+++ b/tests/unit_tests/jinja_context_test.py
@@ -250,6 +250,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 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.