Skip to content

Commit 776ab40

Browse files
committed
fix(currency): show auto currency on table charts
1 parent 594238e commit 776ab40

7 files changed

Lines changed: 376 additions & 12 deletions

File tree

‎superset-frontend/package-lock.json‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎superset/common/query_actions.py‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@
3333
get_column_name,
3434
get_time_filter_status,
3535
)
36-
from superset.utils.currency import detect_currency, detect_currency_from_df
36+
from superset.utils.currency import (
37+
detect_currency,
38+
detect_currency_from_df,
39+
has_auto_currency_in_column_config,
40+
)
3741

3842
if TYPE_CHECKING:
3943
from superset.common.query_context import QueryContext
@@ -105,18 +109,28 @@ def _detect_currency(
105109
First attempts to detect from the provided dataframe if the currency
106110
column is present. Falls back to a separate query only when needed.
107111
112+
Checks both top-level currency_format (used by Pie, Timeseries, etc.)
113+
and column_config (used by Table charts) for AUTO currency settings.
114+
108115
:param query_context: The query context with form_data containing currency_format
109116
:param query_obj: The original query object with filters
110117
:param datasource: The datasource being queried
111118
:param df: Optional dataframe to detect currency from (avoids extra query)
112119
:return: ISO 4217 currency code (e.g., "USD") or None
113120
"""
114-
# Only detect if currency_format.symbol is AUTO
115121
form_data = query_context.form_data or {}
122+
123+
# Check top-level currency_format (for Pie, Timeseries, etc.)
116124
currency_format = form_data.get("currency_format", {})
117-
if not isinstance(currency_format, dict):
118-
return None
119-
if currency_format.get("symbol") != "AUTO":
125+
top_level_auto = (
126+
isinstance(currency_format, dict) and currency_format.get("symbol") == "AUTO"
127+
)
128+
129+
# Check column_config (for Table charts)
130+
column_config_auto = has_auto_currency_in_column_config(form_data)
131+
132+
# Only detect if AUTO is configured somewhere
133+
if not top_level_auto and not column_config_auto:
120134
return None
121135

122136
currency_column = getattr(datasource, "currency_code_column", None)

‎superset/common/query_context_factory.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from superset.models.slice import Slice
3131
from superset.superset_typing import Column
3232
from superset.utils.core import DatasourceDict, DatasourceType, is_adhoc_column
33+
from superset.utils.currency import has_auto_currency_in_column_config
3334

3435
# Charts supporting per-cell currency detection
3536
CELL_LEVEL_CURRENCY_VIZ_TYPES = {"pivot_table_v2", "table"}
@@ -213,6 +214,9 @@ def _add_currency_column(
213214
queries (like pivot tables) to include currency codes per row,
214215
allowing per-cell currency formatting based on the data.
215216
217+
Checks both top-level currency_format (used by Pie, Timeseries, etc.)
218+
and column_config (used by Table charts) for AUTO currency settings.
219+
216220
Only applies to viz types that support cell-level detection (e.g., pivot tables)
217221
Other charts like pie charts use the dataset-level detected_currency instead.
218222
"""
@@ -224,8 +228,18 @@ def _add_currency_column(
224228
if viz_type not in CELL_LEVEL_CURRENCY_VIZ_TYPES:
225229
return
226230

231+
# Check top-level currency_format (for Pie, Timeseries, etc.)
227232
currency_format = form_data.get("currency_format", {})
228-
if currency_format.get("symbol") != "AUTO":
233+
top_level_auto = (
234+
isinstance(currency_format, dict)
235+
and currency_format.get("symbol") == "AUTO"
236+
)
237+
238+
# Check column_config (for Table charts)
239+
column_config_auto = has_auto_currency_in_column_config(form_data)
240+
241+
# Only inject if AUTO is configured somewhere
242+
if not top_level_auto and not column_config_auto:
229243
return
230244

231245
currency_column = getattr(datasource, "currency_code_column", None)

‎superset/utils/currency.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,33 @@
3232
logger = logging.getLogger(__name__)
3333

3434

35+
def has_auto_currency_in_column_config(form_data: dict[str, Any] | None) -> bool:
36+
"""
37+
Check if any column in column_config has AUTO currency format.
38+
39+
Used by Table charts which configure currency per-column via column_config,
40+
rather than using top-level currency_format like other chart types.
41+
42+
:param form_data: The form data containing column_config
43+
:return: True if any column has AUTO currency format
44+
"""
45+
if not form_data:
46+
return False
47+
column_config = form_data.get("column_config", {})
48+
if not isinstance(column_config, dict):
49+
return False
50+
for config in column_config.values():
51+
if not isinstance(config, dict):
52+
continue
53+
currency_format = config.get("currencyFormat", {})
54+
if (
55+
isinstance(currency_format, dict)
56+
and currency_format.get("symbol") == "AUTO"
57+
):
58+
return True
59+
return False
60+
61+
3562
def detect_currency_from_df(
3663
df: Any, # pd.DataFrame, but avoiding import for type checking
3764
currency_column: str,

‎superset/viz.py‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@
7777
merge_extra_filters,
7878
simple_filter_to_adhoc,
7979
)
80-
from superset.utils.currency import detect_currency, detect_currency_from_df
80+
from superset.utils.currency import (
81+
detect_currency,
82+
detect_currency_from_df,
83+
has_auto_currency_in_column_config,
84+
)
8185
from superset.utils.date_parser import get_since_until, parse_past_timedelta
8286
from superset.utils.hashing import hash_from_str
8387

@@ -488,15 +492,25 @@ def _detect_currency(
488492
First attempts to detect from the provided dataframe if the currency
489493
column is present. Falls back to a separate query only when needed.
490494
495+
Checks both top-level currency_format (used by Pie, Timeseries, etc.)
496+
and column_config (used by Table charts) for AUTO currency settings.
497+
491498
:param query_obj: The query object with filters
492499
:param df: Optional dataframe to detect currency from (avoids extra query)
493500
:return: ISO 4217 currency code (e.g., "USD") or None
494501
"""
495-
# Only detect if currency_format.symbol is AUTO
502+
# Check top-level currency_format (for Pie, Timeseries, etc.)
496503
currency_format = self.form_data.get("currency_format", {})
497-
if not isinstance(currency_format, dict):
498-
return None
499-
if currency_format.get("symbol") != "AUTO":
504+
top_level_auto = (
505+
isinstance(currency_format, dict)
506+
and currency_format.get("symbol") == "AUTO"
507+
)
508+
509+
# Check column_config (for Table charts)
510+
column_config_auto = has_auto_currency_in_column_config(self.form_data)
511+
512+
# Only detect if AUTO is configured somewhere
513+
if not top_level_auto and not column_config_auto:
500514
return None
501515

502516
currency_column = getattr(self.datasource, "currency_code_column", None)

‎tests/unit_tests/common/test_query_actions_currency.py‎

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
from typing import Any
1718
from unittest.mock import MagicMock, patch
1819

1920
import pandas as pd
2021
import pytest
2122

2223
from superset.common.query_actions import _detect_currency
24+
from superset.viz import BaseViz
2325

2426

2527
@pytest.fixture
@@ -50,6 +52,24 @@ def mock_datasource() -> MagicMock:
5052
return ds
5153

5254

55+
@pytest.fixture
56+
def base_query_obj() -> dict[str, Any]:
57+
return {
58+
"filter": [],
59+
"granularity": None,
60+
"from_dttm": None,
61+
"to_dttm": None,
62+
"extras": {},
63+
}
64+
65+
66+
def _build_viz(form_data: dict[str, Any], datasource: MagicMock) -> BaseViz:
67+
viz = BaseViz.__new__(BaseViz)
68+
viz.form_data = form_data
69+
viz.datasource = datasource
70+
return viz
71+
72+
5373
def test_detect_currency_returns_none_when_form_data_is_none(
5474
mock_query_obj: MagicMock,
5575
mock_datasource: MagicMock,
@@ -157,3 +177,174 @@ def test_detect_currency_queries_datasource_when_column_not_in_df(
157177

158178
assert result == "GBP"
159179
mock_detect.assert_called_once()
180+
181+
182+
# Tests for column_config AUTO detection (Table charts)
183+
184+
185+
@pytest.fixture
186+
def mock_query_context_with_column_config() -> MagicMock:
187+
"""Create a mock QueryContext with column_config AUTO currency (Table charts)."""
188+
context = MagicMock()
189+
context.form_data = {
190+
"column_config": {
191+
"cost": {"currencyFormat": {"symbol": "AUTO", "symbolPosition": "prefix"}}
192+
}
193+
}
194+
return context
195+
196+
197+
@patch("superset.common.query_actions.detect_currency")
198+
def test_detect_currency_checks_column_config_for_auto(
199+
mock_detect: MagicMock,
200+
mock_query_context_with_column_config: MagicMock,
201+
mock_query_obj: MagicMock,
202+
mock_datasource: MagicMock,
203+
) -> None:
204+
"""Runs detection when column_config has AUTO currency (Table charts)."""
205+
mock_detect.return_value = "USD"
206+
207+
result = _detect_currency(
208+
mock_query_context_with_column_config,
209+
mock_query_obj,
210+
mock_datasource,
211+
)
212+
213+
assert result == "USD"
214+
mock_detect.assert_called_once()
215+
216+
217+
@patch("superset.common.query_actions.detect_currency_from_df")
218+
def test_detect_currency_column_config_uses_dataframe(
219+
mock_detect_from_df: MagicMock,
220+
mock_query_context_with_column_config: MagicMock,
221+
mock_query_obj: MagicMock,
222+
mock_datasource: MagicMock,
223+
) -> None:
224+
"""Uses dataframe detection when column_config has AUTO and df has currency."""
225+
df = pd.DataFrame({"currency_code": ["EUR", "EUR"]})
226+
mock_detect_from_df.return_value = "EUR"
227+
228+
result = _detect_currency(
229+
mock_query_context_with_column_config,
230+
mock_query_obj,
231+
mock_datasource,
232+
df,
233+
)
234+
235+
assert result == "EUR"
236+
mock_detect_from_df.assert_called_once_with(df, "currency_code")
237+
238+
239+
def test_detect_currency_skips_when_no_auto_in_column_config(
240+
mock_query_obj: MagicMock,
241+
mock_datasource: MagicMock,
242+
) -> None:
243+
"""Returns None when column_config has explicit currency (not AUTO)."""
244+
context = MagicMock()
245+
context.form_data = {
246+
"column_config": {"cost": {"currencyFormat": {"symbol": "USD"}}}
247+
}
248+
249+
result = _detect_currency(context, mock_query_obj, mock_datasource)
250+
251+
assert result is None
252+
253+
254+
@patch("superset.common.query_actions.detect_currency")
255+
def test_detect_currency_works_with_both_top_level_and_column_config(
256+
mock_detect: MagicMock,
257+
mock_query_obj: MagicMock,
258+
mock_datasource: MagicMock,
259+
) -> None:
260+
"""Detects when both top-level and column_config have AUTO."""
261+
context = MagicMock()
262+
context.form_data = {
263+
"currency_format": {"symbol": "AUTO"},
264+
"column_config": {"cost": {"currencyFormat": {"symbol": "AUTO"}}},
265+
}
266+
mock_detect.return_value = "JPY"
267+
268+
result = _detect_currency(context, mock_query_obj, mock_datasource)
269+
270+
assert result == "JPY"
271+
mock_detect.assert_called_once()
272+
273+
274+
@patch("superset.common.query_actions.detect_currency")
275+
def test_detect_currency_top_level_auto_triggers_detection(
276+
mock_detect: MagicMock,
277+
mock_query_obj: MagicMock,
278+
mock_datasource: MagicMock,
279+
) -> None:
280+
"""Detects when only top-level currency_format has AUTO."""
281+
context = MagicMock()
282+
context.form_data = {
283+
"currency_format": {"symbol": "AUTO"},
284+
"column_config": {
285+
"cost": {"currencyFormat": {"symbol": "USD"}} # explicit, not AUTO
286+
},
287+
}
288+
mock_detect.return_value = "CAD"
289+
290+
result = _detect_currency(context, mock_query_obj, mock_datasource)
291+
292+
assert result == "CAD"
293+
mock_detect.assert_called_once()
294+
295+
296+
def test_detect_currency_column_config_no_currency_column_returns_none(
297+
mock_query_context_with_column_config: MagicMock,
298+
mock_query_obj: MagicMock,
299+
) -> None:
300+
"""Returns None when column_config has AUTO but datasource lacks currency column."""
301+
datasource = MagicMock()
302+
datasource.currency_code_column = None
303+
304+
result = _detect_currency(
305+
mock_query_context_with_column_config,
306+
mock_query_obj,
307+
datasource,
308+
)
309+
310+
assert result is None
311+
312+
313+
@patch("superset.viz.detect_currency")
314+
def test_viz_detect_currency_checks_column_config_for_auto(
315+
mock_detect: MagicMock,
316+
base_query_obj: dict[str, Any],
317+
mock_datasource: MagicMock,
318+
) -> None:
319+
"""Runs detection when column_config has AUTO currency (legacy API)."""
320+
mock_detect.return_value = "USD"
321+
form_data = {"column_config": {"cost": {"currencyFormat": {"symbol": "AUTO"}}}}
322+
viz = _build_viz(form_data, mock_datasource)
323+
324+
result = BaseViz._detect_currency(viz, base_query_obj)
325+
326+
assert result == "USD"
327+
mock_detect.assert_called_once_with(
328+
datasource=mock_datasource,
329+
filters=base_query_obj["filter"],
330+
granularity=base_query_obj["granularity"],
331+
from_dttm=base_query_obj["from_dttm"],
332+
to_dttm=base_query_obj["to_dttm"],
333+
extras=base_query_obj["extras"],
334+
)
335+
336+
337+
@patch("superset.viz.detect_currency")
338+
def test_viz_detect_currency_skips_without_auto_in_column_config(
339+
mock_detect: MagicMock,
340+
base_query_obj: dict[str, Any],
341+
mock_datasource: MagicMock,
342+
) -> None:
343+
"""Returns None when column_config has explicit currency (legacy API)."""
344+
form_data = {"column_config": {"cost": {"currencyFormat": {"symbol": "USD"}}}}
345+
viz = _build_viz(form_data, mock_datasource)
346+
347+
result = BaseViz._detect_currency(viz, base_query_obj)
348+
349+
assert result is None
350+
mock_detect.assert_not_called()

0 commit comments

Comments
 (0)