diff --git a/requirements/base.txt b/requirements/base.txt
index 30dc9c0a9187c..1e9183f479379 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -278,6 +278,8 @@ wtforms==2.3.3
# wtforms-json
wtforms-json==0.3.3
# via apache-superset
+xlsxwriter==3.0.3
+ # via apache-superset
# The following packages are considered to be unsafe in a requirements file:
# setuptools
diff --git a/setup.py b/setup.py
index 0e45eb5fdb14f..0fddbfb1b0ce2 100644
--- a/setup.py
+++ b/setup.py
@@ -121,6 +121,7 @@ def get_git_sha() -> str:
"tabulate>=0.8.9, <0.9",
"typing-extensions>=4, <5",
"wtforms-json",
+ "xlsxwriter",
],
extras_require={
"athena": ["pyathena[pandas]>=2, <3"],
@@ -173,8 +174,6 @@ def get_git_sha() -> str:
"thumbnails": ["Pillow>=9.1.1, <10.0.0"],
"vertica": ["sqlalchemy-vertica-python>=0.5.9, < 0.6"],
"netezza": ["nzalchemy>=11.0.2"],
- "xls": ["xlwt>=1.3.0, < 1.4"],
- "xlsx": ["xlsxwriter>=3.0.0, < 3.1"],
},
python_requires="~=3.8",
author="Apache Software Foundation",
diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
index f75f102459369..f448abdeda3b7 100644
--- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
+++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
@@ -41,7 +41,6 @@ const MENU_KEYS = {
EXPORT_TO_CSV: 'export_to_csv',
EXPORT_TO_CSV_PIVOTED: 'export_to_csv_pivoted',
EXPORT_TO_JSON: 'export_to_json',
- EXPORT_TO_XLS: 'export_to_xls',
EXPORT_TO_XLSX: 'export_to_xlsx',
DOWNLOAD_AS_IMAGE: 'download_as_image',
SHARE_SUBMENU: 'share_submenu',
@@ -159,29 +158,15 @@ export const useExploreAdditionalActionsMenu = (
}),
[latestQueryFormData],
);
- const exportXLS = useCallback(
- () =>
- canDownloadCSV
- ? exportChart({
- formData: latestQueryFormData,
- ownState,
- resultType: 'full',
- resultFormat: 'xls',
- })
- : null,
- [canDownloadCSV, latestQueryFormData],
- );
- const exportXLSX = useCallback(
+
+ const exportExcel = useCallback(
() =>
- canDownloadCSV
- ? exportChart({
- formData: latestQueryFormData,
- ownState,
- resultType: 'full',
- resultFormat: 'xlsx',
- })
- : null,
- [canDownloadCSV, latestQueryFormData],
+ exportChart({
+ formData: latestQueryFormData,
+ resultType: 'results',
+ resultFormat: 'xlsx',
+ }),
+ [latestQueryFormData],
);
const copyLink = useCallback(async () => {
@@ -218,14 +203,9 @@ export const useExploreAdditionalActionsMenu = (
setIsDropdownVisible(false);
setOpenSubmenus([]);
- break;
- case MENU_KEYS.EXPORT_TO_XLS:
- exportXLS();
- setIsDropdownVisible(false);
- setOpenSubmenus([]);
break;
case MENU_KEYS.EXPORT_TO_XLSX:
- exportXLSX();
+ exportExcel();
setIsDropdownVisible(false);
setOpenSubmenus([]);
break;
@@ -332,18 +312,15 @@ export const useExploreAdditionalActionsMenu = (
}>
{t('Export to .JSON')}
- }>
- {t('Export to .XLS')}
-
- }>
- {t('Export to .XLSX')}
-
}
>
{t('Download as image')}
+ }>
+ {t('Export to Excel')}
+
diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py
index fc0f3f3a16643..0418472982fdf 100644
--- a/superset/charts/data/api.py
+++ b/superset/charts/data/api.py
@@ -45,12 +45,7 @@
from superset.extensions import event_logger
from superset.utils.async_query_manager import AsyncQueryTokenException
from superset.utils.core import create_zip, get_user_id, json_int_dttm_ser
-from superset.views.base import (
- CsvResponse,
- generate_download_headers,
- XlsResponse,
- XlsxResponse,
-)
+from superset.views.base import CsvResponse, generate_download_headers, XlsxResponse
from superset.views.base_api import statsd_metrics
if TYPE_CHECKING:
@@ -355,27 +350,26 @@ def _send_chart_response(
if not result["queries"]:
return self.response_400(_("Empty query result"))
+ is_csv_format = result_format == ChartDataResultFormat.CSV
+
if len(result["queries"]) == 1:
# return single query results
data = result["queries"][0]["data"]
- if result_format == ChartDataResultFormat.CSV:
+ if is_csv_format:
return CsvResponse(data, headers=generate_download_headers("csv"))
- elif result_format == ChartDataResultFormat.XLS:
- return XlsResponse(data, headers=generate_download_headers("xls"))
- elif result_format == ChartDataResultFormat.XLSX:
- return XlsxResponse(data, headers=generate_download_headers("xlsx"))
- # return multi-query results bundled as a zip file
+ return XlsxResponse(data, headers=generate_download_headers("xlsx"))
- def _process_data(d: Any) -> Any:
+ # return multi-query results bundled as a zip file
+ def _process_data(query_data: Any) -> Any:
if result_format == ChartDataResultFormat.CSV:
encoding = current_app.config["CSV_EXPORT"].get("encoding", "utf-8")
- return d.encode(encoding)
- return d
+ return query_data.encode(encoding)
+ return query_data
files = {
- f"query_{idx + 1}.{result_format}": _process_data(result["data"])
- for idx, result in enumerate(result["queries"])
+ f"query_{idx + 1}.{result_format}": _process_data(query["data"])
+ for idx, query in enumerate(result["queries"])
}
return Response(
create_zip(files),
diff --git a/superset/common/chart_data.py b/superset/common/chart_data.py
index 1cbe982b07c2a..659a640159378 100644
--- a/superset/common/chart_data.py
+++ b/superset/common/chart_data.py
@@ -25,16 +25,11 @@ class ChartDataResultFormat(str, Enum):
CSV = "csv"
JSON = "json"
- XLS = "xls"
XLSX = "xlsx"
- @classmethod
- def excel(cls) -> Set["ChartDataResultFormat"]:
- return {cls.XLS, cls.XLSX}
-
@classmethod
def table_like(cls) -> Set["ChartDataResultFormat"]:
- return {cls.CSV} | {cls.XLS, cls.XLSX}
+ return {cls.CSV} | {cls.XLSX}
class ChartDataResultType(str, Enum):
diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py
index f0fba43e02dc8..747b1aebcd3f1 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -452,16 +452,14 @@ def get_data(self, df: pd.DataFrame) -> Union[str, List[Dict[str, Any]]]:
verbose_map = self._qc_datasource.data.get("verbose_map", {})
if verbose_map:
df.columns = [verbose_map.get(column, column) for column in columns]
- if self._query_context.result_type == ChartDataResultFormat.CSV:
+
+ result = None
+ if self._query_context.result_format == ChartDataResultFormat.CSV:
result = csv.df_to_escaped_csv(
df, index=include_index, **config["CSV_EXPORT"]
)
- else:
- result = excel.df_to_excel(
- df,
- excel_format=self._query_context.result_format,
- **config["EXCEL_EXPORT"],
- )
+ elif self._query_context.result_format == ChartDataResultFormat.XLSX:
+ result = excel.df_to_excel(df, **config["EXCEL_EXPORT"])
return result or ""
return df.to_dict(orient="records")
diff --git a/superset/utils/excel.py b/superset/utils/excel.py
index 595e3276f0c77..1f68031b6497b 100644
--- a/superset/utils/excel.py
+++ b/superset/utils/excel.py
@@ -1,18 +1,29 @@
+# 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.
import io
from typing import Any
import pandas as pd
-from superset.common.chart_data import ChartDataResultFormat
-
-def df_to_excel(
- df: pd.DataFrame,
- excel_format: ChartDataResultFormat = ChartDataResultFormat.XLSX,
- **kwargs: Any
-) -> bytes:
+def df_to_excel(df: pd.DataFrame, **kwargs: Any) -> Any:
output = io.BytesIO()
- engine = "xlwt" if excel_format == ChartDataResultFormat.XLS else "xlsxwriter"
- with pd.ExcelWriter(output, engine=engine) as writer:
+ # pylint: disable=abstract-class-instantiated
+ with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
df.to_excel(writer, **kwargs)
+
return output.getvalue()
diff --git a/superset/views/base.py b/superset/views/base.py
index eb01c50c0283a..8eb7e58693938 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -666,15 +666,6 @@ class CsvResponse(Response):
default_mimetype = "text/csv"
-class XlsResponse(Response):
- """
- Override Response to use xls mimetype
- """
-
- charset = "utf-8"
- default_mimetype = "application/vnd.ms-excel"
-
-
class XlsxResponse(Response):
"""
Override Response to use xlsx mimetype
diff --git a/superset/views/core.py b/superset/views/core.py
index cfa5ab2218503..dfe57c7e50f24 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -152,7 +152,6 @@
json_errors_response,
json_success,
validate_sqlatable,
- XlsResponse,
XlsxResponse,
)
from superset.views.sql_lab.schemas import SqlJsonPayloadSchema
@@ -497,12 +496,6 @@ def generate_json(
viz_obj.get_csv(), headers=generate_download_headers("csv")
)
- if response_type == ChartDataResultFormat.XLS:
- return XlsResponse(
- viz_obj.get_excel(ChartDataResultFormat(response_type)),
- headers=generate_download_headers("xls"),
- )
-
if response_type == ChartDataResultFormat.XLSX:
return XlsxResponse(
viz_obj.get_excel(ChartDataResultFormat(response_type)),
diff --git a/tests/integration_tests/charts/data/api_tests.py b/tests/integration_tests/charts/data/api_tests.py
index e9b4f4d1c617d..48ce6cc91d2ab 100644
--- a/tests/integration_tests/charts/data/api_tests.py
+++ b/tests/integration_tests/charts/data/api_tests.py
@@ -246,44 +246,52 @@ def test_with_query_result_type__200(self):
assert rv.status_code == 200
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
- @pytest.mark.parametrize("result_format", ["csv", "xls", "xlsx"])
- def test_empty_request_with_table_like_result_format(self, result_format):
+ def test_empty_request_with_csv_result_format(self):
"""
- Chart data API: Test empty chart data with table like result format
+ Chart data API: Test empty chart data with CSV result format
"""
- self.query_context_payload["result_format"] = result_format
+ self.query_context_payload["result_format"] = "csv"
self.query_context_payload["queries"] = []
rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
assert rv.status_code == 400
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
- @pytest.mark.parametrize(
- "result_format,mimetype",
- [
- ("csv", "text/csv"),
- ("xls", "application/vnd.ms-excel"),
- (
- "xlsx",
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- )
- ]
- )
- def test_with_table_like_result_format(self, result_format, mimetype):
+ def test_empty_request_with_excel_result_format(self):
+ """
+ Chart data API: Test empty chart data with Excel result format
+ """
+ self.query_context_payload["result_format"] = "xlsx"
+ self.query_context_payload["queries"] = []
+ rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
+ assert rv.status_code == 400
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_with_csv_result_format(self):
"""
- Chart data API: Test chart data with table like result format
+ Chart data API: Test chart data with CSV result format
"""
- self.query_context_payload["result_format"] = result_format
+ self.query_context_payload["result_format"] = "csv"
+ rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
+ assert rv.status_code == 200
+ assert rv.mimetype == "text/csv"
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_with_excel_result_format(self):
+ """
+ Chart data API: Test chart data with Excel result format
+ """
+ self.query_context_payload["result_format"] = "xlsx"
+ mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
assert rv.status_code == 200
assert rv.mimetype == mimetype
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
- @pytest.mark.parametrize("result_format", ["csv", "xls", "xlsx"])
- def test_with_multi_query_table_like_result_format(self, result_format):
+ def test_with_multi_query_csv_result_format(self):
"""
- Chart data API: Test chart data with multi-query table like result format
+ Chart data API: Test chart data with multi-query CSV result format
"""
- self.query_context_payload["result_format"] = result_format
+ self.query_context_payload["result_format"] = "csv"
self.query_context_payload["queries"].append(
self.query_context_payload["queries"][0]
)
@@ -291,22 +299,43 @@ def test_with_multi_query_table_like_result_format(self, result_format):
assert rv.status_code == 200
assert rv.mimetype == "application/zip"
zipfile = ZipFile(BytesIO(rv.data), "r")
- assert zipfile.namelist() == [
- f"query_1.{result_format}",
- f"query_2.{result_format}",
- ]
+ assert zipfile.namelist() == ["query_1.csv", "query_2.csv"]
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
- @pytest.mark.parametrize("result_format", ["csv", "xls", "xlsx"])
- def test_with_table_like_result_format_when_actor_not_permitted_for__403(
- self, result_format
- ):
+ def test_with_multi_query_excel_result_format(self):
+ """
+ Chart data API: Test chart data with multi-query Excel result format
+ """
+ self.query_context_payload["result_format"] = "xlsx"
+ self.query_context_payload["queries"].append(
+ self.query_context_payload["queries"][0]
+ )
+ rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
+ assert rv.status_code == 200
+ assert rv.mimetype == "application/zip"
+ zipfile = ZipFile(BytesIO(rv.data), "r")
+ assert zipfile.namelist() == ["query_1.xlsx", "query_2.xlsx"]
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_with_csv_result_format_when_actor_not_permitted_for_csv__403(self):
+ """
+ Chart data API: Test chart data with CSV result format
+ """
+ self.logout()
+ self.login(username="gamma_no_csv")
+ self.query_context_payload["result_format"] = "csv"
+
+ rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
+ assert rv.status_code == 403
+
+ @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+ def test_with_excel_result_format_when_actor_not_permitted_for_excel__403(self):
"""
- Chart data API: Test chart data with table like result format
+ Chart data API: Test chart data with Excel result format
"""
self.logout()
self.login(username="gamma_no_csv")
- self.query_context_payload["result_format"] = result_format
+ self.query_context_payload["result_format"] = "xlsx"
rv = self.post_assert_metric(CHART_DATA_URI, self.query_context_payload, "data")
assert rv.status_code == 403