mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
feat: Dynamic currency (#36416)
This commit is contained in:
committed by
GitHub
parent
896947c787
commit
f4474b2e3e
290
tests/unit_tests/common/test_query_actions_currency.py
Normal file
290
tests/unit_tests/common/test_query_actions_currency.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from superset.common.query_actions import _detect_currency
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_query_context() -> MagicMock:
|
||||
"""Create a mock QueryContext with AUTO currency format."""
|
||||
context = MagicMock()
|
||||
context.form_data = {"currency_format": {"symbol": "AUTO"}}
|
||||
return context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_query_obj() -> MagicMock:
|
||||
"""Create a mock QueryObject with filter attributes."""
|
||||
obj = MagicMock()
|
||||
obj.filter = []
|
||||
obj.granularity = None
|
||||
obj.from_dttm = None
|
||||
obj.to_dttm = None
|
||||
obj.extras = {}
|
||||
return obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_datasource() -> MagicMock:
|
||||
"""Create a mock datasource with currency column."""
|
||||
ds = MagicMock()
|
||||
ds.currency_code_column = "currency_code"
|
||||
return ds
|
||||
|
||||
|
||||
def test_detect_currency_returns_none_when_form_data_is_none(
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Returns None when query context has no form_data."""
|
||||
context = MagicMock()
|
||||
context.form_data = None
|
||||
|
||||
result = _detect_currency(context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_detect_currency_returns_none_when_currency_format_not_dict(
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Returns None when currency_format is not a dict."""
|
||||
context = MagicMock()
|
||||
context.form_data = {"currency_format": "invalid"}
|
||||
|
||||
result = _detect_currency(context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_detect_currency_returns_none_when_symbol_not_auto(
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Returns None when currency_format.symbol is not AUTO."""
|
||||
context = MagicMock()
|
||||
context.form_data = {"currency_format": {"symbol": "USD"}}
|
||||
|
||||
result = _detect_currency(context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_detect_currency_returns_none_when_no_currency_column(
|
||||
mock_query_context: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
) -> None:
|
||||
"""Returns None when datasource has no currency_code_column."""
|
||||
datasource = MagicMock()
|
||||
datasource.currency_code_column = None
|
||||
|
||||
result = _detect_currency(mock_query_context, mock_query_obj, datasource)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency_from_df")
|
||||
def test_detect_currency_uses_dataframe_when_column_present(
|
||||
mock_detect_from_df: MagicMock,
|
||||
mock_query_context: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Uses detect_currency_from_df when df contains currency column."""
|
||||
df = pd.DataFrame({"currency_code": ["USD", "USD"]})
|
||||
mock_detect_from_df.return_value = "USD"
|
||||
|
||||
result = _detect_currency(mock_query_context, mock_query_obj, mock_datasource, df)
|
||||
|
||||
assert result == "USD"
|
||||
mock_detect_from_df.assert_called_once_with(df, "currency_code")
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency")
|
||||
def test_detect_currency_queries_datasource_when_no_df(
|
||||
mock_detect: MagicMock,
|
||||
mock_query_context: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Queries datasource when df is None."""
|
||||
mock_detect.return_value = "EUR"
|
||||
|
||||
result = _detect_currency(mock_query_context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result == "EUR"
|
||||
mock_detect.assert_called_once_with(
|
||||
datasource=mock_datasource,
|
||||
filters=mock_query_obj.filter,
|
||||
granularity=mock_query_obj.granularity,
|
||||
from_dttm=mock_query_obj.from_dttm,
|
||||
to_dttm=mock_query_obj.to_dttm,
|
||||
extras=mock_query_obj.extras,
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency")
|
||||
def test_detect_currency_queries_datasource_when_column_not_in_df(
|
||||
mock_detect: MagicMock,
|
||||
mock_query_context: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Falls back to query when df doesn't have currency column."""
|
||||
df = pd.DataFrame({"other_column": ["value"]})
|
||||
mock_detect.return_value = "GBP"
|
||||
|
||||
result = _detect_currency(mock_query_context, mock_query_obj, mock_datasource, df)
|
||||
|
||||
assert result == "GBP"
|
||||
mock_detect.assert_called_once()
|
||||
|
||||
|
||||
# Tests for column_config AUTO detection (Table charts)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_query_context_with_column_config() -> MagicMock:
|
||||
"""Create a mock QueryContext with column_config AUTO currency (Table charts)."""
|
||||
context = MagicMock()
|
||||
context.form_data = {
|
||||
"column_config": {
|
||||
"cost": {"currencyFormat": {"symbol": "AUTO", "symbolPosition": "prefix"}}
|
||||
}
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency")
|
||||
def test_detect_currency_checks_column_config_for_auto(
|
||||
mock_detect: MagicMock,
|
||||
mock_query_context_with_column_config: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Runs detection when column_config has AUTO currency (Table charts)."""
|
||||
mock_detect.return_value = "USD"
|
||||
|
||||
result = _detect_currency(
|
||||
mock_query_context_with_column_config,
|
||||
mock_query_obj,
|
||||
mock_datasource,
|
||||
)
|
||||
|
||||
assert result == "USD"
|
||||
mock_detect.assert_called_once()
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency_from_df")
|
||||
def test_detect_currency_column_config_uses_dataframe(
|
||||
mock_detect_from_df: MagicMock,
|
||||
mock_query_context_with_column_config: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Uses dataframe detection when column_config has AUTO and df has currency."""
|
||||
df = pd.DataFrame({"currency_code": ["EUR", "EUR"]})
|
||||
mock_detect_from_df.return_value = "EUR"
|
||||
|
||||
result = _detect_currency(
|
||||
mock_query_context_with_column_config,
|
||||
mock_query_obj,
|
||||
mock_datasource,
|
||||
df,
|
||||
)
|
||||
|
||||
assert result == "EUR"
|
||||
mock_detect_from_df.assert_called_once_with(df, "currency_code")
|
||||
|
||||
|
||||
def test_detect_currency_skips_when_no_auto_in_column_config(
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Returns None when column_config has explicit currency (not AUTO)."""
|
||||
context = MagicMock()
|
||||
context.form_data = {
|
||||
"column_config": {"cost": {"currencyFormat": {"symbol": "USD"}}}
|
||||
}
|
||||
|
||||
result = _detect_currency(context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency")
|
||||
def test_detect_currency_works_with_both_top_level_and_column_config(
|
||||
mock_detect: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Detects when both top-level and column_config have AUTO."""
|
||||
context = MagicMock()
|
||||
context.form_data = {
|
||||
"currency_format": {"symbol": "AUTO"},
|
||||
"column_config": {"cost": {"currencyFormat": {"symbol": "AUTO"}}},
|
||||
}
|
||||
mock_detect.return_value = "JPY"
|
||||
|
||||
result = _detect_currency(context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result == "JPY"
|
||||
mock_detect.assert_called_once()
|
||||
|
||||
|
||||
@patch("superset.common.query_actions.detect_currency")
|
||||
def test_detect_currency_top_level_auto_triggers_detection(
|
||||
mock_detect: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
mock_datasource: MagicMock,
|
||||
) -> None:
|
||||
"""Detects when only top-level currency_format has AUTO."""
|
||||
context = MagicMock()
|
||||
context.form_data = {
|
||||
"currency_format": {"symbol": "AUTO"},
|
||||
"column_config": {
|
||||
"cost": {"currencyFormat": {"symbol": "USD"}} # explicit, not AUTO
|
||||
},
|
||||
}
|
||||
mock_detect.return_value = "CAD"
|
||||
|
||||
result = _detect_currency(context, mock_query_obj, mock_datasource)
|
||||
|
||||
assert result == "CAD"
|
||||
mock_detect.assert_called_once()
|
||||
|
||||
|
||||
def test_detect_currency_column_config_no_currency_column_returns_none(
|
||||
mock_query_context_with_column_config: MagicMock,
|
||||
mock_query_obj: MagicMock,
|
||||
) -> None:
|
||||
"""Returns None when column_config has AUTO but datasource lacks currency column."""
|
||||
datasource = MagicMock()
|
||||
datasource.currency_code_column = None
|
||||
|
||||
result = _detect_currency(
|
||||
mock_query_context_with_column_config,
|
||||
mock_query_obj,
|
||||
datasource,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
@@ -436,3 +436,109 @@ class TestQueryContextFactory:
|
||||
self.factory._apply_filters(query_object)
|
||||
|
||||
assert query_object.filter[0]["val"] == "value"
|
||||
|
||||
def test_add_currency_column_no_form_data(self):
|
||||
"""Test _add_currency_column when form_data is None."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1"]
|
||||
datasource = Mock()
|
||||
|
||||
self.factory._add_currency_column(query_object, None, datasource)
|
||||
|
||||
assert query_object.columns == ["col1"]
|
||||
|
||||
def test_add_currency_column_no_columns(self):
|
||||
"""Test _add_currency_column when query_object has no columns."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = []
|
||||
form_data = {
|
||||
"viz_type": "pivot_table_v2",
|
||||
"currency_format": {"symbol": "AUTO"},
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = "currency_code"
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == []
|
||||
|
||||
def test_add_currency_column_unsupported_viz_type(self):
|
||||
"""Test _add_currency_column with unsupported viz type."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1"]
|
||||
form_data = {"viz_type": "pie", "currency_format": {"symbol": "AUTO"}}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = "currency_code"
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == ["col1"]
|
||||
|
||||
def test_add_currency_column_symbol_not_auto(self):
|
||||
"""Test _add_currency_column when symbol is not AUTO."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1"]
|
||||
form_data = {"viz_type": "pivot_table_v2", "currency_format": {"symbol": "USD"}}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = "currency_code"
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == ["col1"]
|
||||
|
||||
def test_add_currency_column_no_currency_column_on_datasource(self):
|
||||
"""Test _add_currency_column when datasource has no currency column."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1"]
|
||||
form_data = {
|
||||
"viz_type": "pivot_table_v2",
|
||||
"currency_format": {"symbol": "AUTO"},
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = None
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == ["col1"]
|
||||
|
||||
def test_add_currency_column_already_in_query(self):
|
||||
"""Test _add_currency_column when currency column already exists."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1", "currency_code"]
|
||||
form_data = {
|
||||
"viz_type": "pivot_table_v2",
|
||||
"currency_format": {"symbol": "AUTO"},
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = "currency_code"
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == ["col1", "currency_code"]
|
||||
|
||||
def test_add_currency_column_adds_column_for_pivot_table(self):
|
||||
"""Test _add_currency_column adds column for pivot_table_v2 viz type"""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1"]
|
||||
form_data = {
|
||||
"viz_type": "pivot_table_v2",
|
||||
"currency_format": {"symbol": "AUTO"},
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = "currency_code"
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == ["col1", "currency_code"]
|
||||
|
||||
def test_add_currency_column_skips_table_viz_type(self):
|
||||
"""Test _add_currency_column does not add column for table viz type."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.columns = ["col1"]
|
||||
form_data = {"viz_type": "table", "currency_format": {"symbol": "AUTO"}}
|
||||
datasource = Mock()
|
||||
datasource.currency_code_column = "currency_code"
|
||||
|
||||
self.factory._add_currency_column(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.columns == ["col1"]
|
||||
|
||||
Reference in New Issue
Block a user