This is an automated email from the ASF dual-hosted git repository.
jli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new de1dd53186 fix(theme-crud): enable overwrite confirmation UI for theme
imports (#35558)
de1dd53186 is described below
commit de1dd5318675172fb0dc6c759dd5a2898e4d353e
Author: Gabriel Torres Ruiz <[email protected]>
AuthorDate: Wed Oct 15 21:15:57 2025 -0400
fix(theme-crud): enable overwrite confirmation UI for theme imports (#35558)
---
superset/themes/api.py | 12 +--
tests/integration_tests/themes/api_tests.py | 122 ++++++++++++++++++++++++++++
2 files changed, 125 insertions(+), 9 deletions(-)
diff --git a/superset/themes/api.py b/superset/themes/api.py
index 8314e4e15b..10b05d3a91 100644
--- a/superset/themes/api.py
+++ b/superset/themes/api.py
@@ -550,15 +550,9 @@ class ThemeRestApi(BaseSupersetModelRestApi):
overwrite = request.form.get("overwrite") == "true"
- try:
- ImportThemesCommand(contents, overwrite=overwrite).run()
- return self.response(200, message="Theme imported successfully")
- except ValidationError as err:
- logger.exception("Import themes validation error")
- return self.response_400(message=str(err))
- except Exception as ex:
- logger.exception("Unexpected error importing themes")
- return self.response_422(message=str(ex))
+ command = ImportThemesCommand(contents, overwrite=overwrite)
+ command.run()
+ return self.response(200, message="Theme imported successfully")
@expose("/<int:pk>/set_system_default", methods=("PUT",))
@protect()
diff --git a/tests/integration_tests/themes/api_tests.py
b/tests/integration_tests/themes/api_tests.py
index 7828101fcf..387b9edf86 100644
--- a/tests/integration_tests/themes/api_tests.py
+++ b/tests/integration_tests/themes/api_tests.py
@@ -19,9 +19,14 @@
import pytest
import prison
+import uuid
+import yaml
from datetime import datetime
from freezegun import freeze_time
+from io import BytesIO
from sqlalchemy.sql import func
+from typing import Any
+from zipfile import ZipFile
import tests.integration_tests.test_app # noqa: F401
from superset import db
@@ -399,3 +404,120 @@ class TestThemeApi(SupersetTestCase):
uri = f"api/v1/theme/?q={prison.dumps(theme_ids)}"
rv = self.delete_assert_metric(uri, "bulk_delete")
assert rv.status_code == 404
+
+ def create_theme_import_zip(self, theme_config: dict[str, Any]) -> BytesIO:
+ """Helper method to create a theme import ZIP file"""
+ buf = BytesIO()
+ with ZipFile(buf, "w") as bundle:
+ # Use a root folder like the export does
+ root = "theme_import"
+
+ # Add metadata.yaml
+ metadata = {
+ "version": "1.0.0",
+ "type": "Theme",
+ "timestamp": datetime.now().isoformat(),
+ }
+ with bundle.open(f"{root}/metadata.yaml", "w") as fp:
+ fp.write(yaml.safe_dump(metadata).encode())
+
+ # Add theme YAML file
+ theme_yaml = yaml.safe_dump(theme_config)
+ with bundle.open(
+ f"{root}/themes/{theme_config['theme_name']}.yaml", "w"
+ ) as fp:
+ fp.write(theme_yaml.encode())
+ buf.seek(0)
+ return buf
+
+ def test_import_theme(self):
+ """
+ Theme API: Test import theme
+ """
+ theme_config = {
+ "theme_name": "imported_theme",
+ "uuid": str(uuid.uuid4()),
+ "version": "1.0.0",
+ "json_data": {"colors": {"primary": "#007bff"}},
+ }
+
+ self.login(ADMIN_USERNAME)
+ uri = "api/v1/theme/import/"
+
+ buf = self.create_theme_import_zip(theme_config)
+ form_data = {
+ "formData": (buf, "theme_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data,
content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 200
+ assert response == {"message": "Theme imported successfully"}
+
+ theme =
db.session.query(Theme).filter_by(uuid=theme_config["uuid"]).one()
+ assert theme.theme_name == "imported_theme"
+
+ # Cleanup
+ db.session.delete(theme)
+ db.session.commit()
+
+ def test_import_theme_overwrite(self):
+ """
+ Theme API: Test import existing theme without and with overwrite
+ """
+ theme_config = {
+ "theme_name": "overwrite_theme",
+ "uuid": str(uuid.uuid4()),
+ "version": "1.0.0",
+ "json_data": {"colors": {"primary": "#007bff"}},
+ }
+
+ self.login(ADMIN_USERNAME)
+ uri = "api/v1/theme/import/"
+
+ # First import
+ buf = self.create_theme_import_zip(theme_config)
+ form_data = {
+ "formData": (buf, "theme_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data,
content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 200
+ assert response == {"message": "Theme imported successfully"}
+
+ # Import again without overwrite flag - should fail with structured
error
+ buf = self.create_theme_import_zip(theme_config)
+ form_data = {
+ "formData": (buf, "theme_export.zip"),
+ }
+ rv = self.client.post(uri, data=form_data,
content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 422
+ assert len(response["errors"]) == 1
+ error = response["errors"][0]
+ assert error["message"].startswith("Error importing theme")
+ assert error["error_type"] == "GENERIC_COMMAND_ERROR"
+ assert error["level"] == "warning"
+ assert f"themes/{theme_config['theme_name']}.yaml" in
str(error["extra"])
+ assert "Theme already exists and `overwrite=true` was not passed" in
str(
+ error["extra"]
+ )
+
+ # Import with overwrite flag - should succeed
+ buf = self.create_theme_import_zip(theme_config)
+ form_data = {
+ "formData": (buf, "theme_export.zip"),
+ "overwrite": "true",
+ }
+ rv = self.client.post(uri, data=form_data,
content_type="multipart/form-data")
+ response = json.loads(rv.data.decode("utf-8"))
+
+ assert rv.status_code == 200
+ assert response == {"message": "Theme imported successfully"}
+
+ # Cleanup
+ theme =
db.session.query(Theme).filter_by(uuid=theme_config["uuid"]).one()
+ db.session.delete(theme)
+ db.session.commit()