Skip to content

Commit de1dd53

Browse files
fix(theme-crud): enable overwrite confirmation UI for theme imports (#35558)
1 parent 58672df commit de1dd53

File tree

2 files changed

+125
-9
lines changed

2 files changed

+125
-9
lines changed

superset/themes/api.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -550,15 +550,9 @@ def import_(self) -> Response:
550550

551551
overwrite = request.form.get("overwrite") == "true"
552552

553-
try:
554-
ImportThemesCommand(contents, overwrite=overwrite).run()
555-
return self.response(200, message="Theme imported successfully")
556-
except ValidationError as err:
557-
logger.exception("Import themes validation error")
558-
return self.response_400(message=str(err))
559-
except Exception as ex:
560-
logger.exception("Unexpected error importing themes")
561-
return self.response_422(message=str(ex))
553+
command = ImportThemesCommand(contents, overwrite=overwrite)
554+
command.run()
555+
return self.response(200, message="Theme imported successfully")
562556

563557
@expose("/<int:pk>/set_system_default", methods=("PUT",))
564558
@protect()

tests/integration_tests/themes/api_tests.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@
1919

2020
import pytest
2121
import prison
22+
import uuid
23+
import yaml
2224
from datetime import datetime
2325
from freezegun import freeze_time
26+
from io import BytesIO
2427
from sqlalchemy.sql import func
28+
from typing import Any
29+
from zipfile import ZipFile
2530

2631
import tests.integration_tests.test_app # noqa: F401
2732
from superset import db
@@ -399,3 +404,120 @@ def test_delete_bulk_theme_not_found(self):
399404
uri = f"api/v1/theme/?q={prison.dumps(theme_ids)}"
400405
rv = self.delete_assert_metric(uri, "bulk_delete")
401406
assert rv.status_code == 404
407+
408+
def create_theme_import_zip(self, theme_config: dict[str, Any]) -> BytesIO:
409+
"""Helper method to create a theme import ZIP file"""
410+
buf = BytesIO()
411+
with ZipFile(buf, "w") as bundle:
412+
# Use a root folder like the export does
413+
root = "theme_import"
414+
415+
# Add metadata.yaml
416+
metadata = {
417+
"version": "1.0.0",
418+
"type": "Theme",
419+
"timestamp": datetime.now().isoformat(),
420+
}
421+
with bundle.open(f"{root}/metadata.yaml", "w") as fp:
422+
fp.write(yaml.safe_dump(metadata).encode())
423+
424+
# Add theme YAML file
425+
theme_yaml = yaml.safe_dump(theme_config)
426+
with bundle.open(
427+
f"{root}/themes/{theme_config['theme_name']}.yaml", "w"
428+
) as fp:
429+
fp.write(theme_yaml.encode())
430+
buf.seek(0)
431+
return buf
432+
433+
def test_import_theme(self):
434+
"""
435+
Theme API: Test import theme
436+
"""
437+
theme_config = {
438+
"theme_name": "imported_theme",
439+
"uuid": str(uuid.uuid4()),
440+
"version": "1.0.0",
441+
"json_data": {"colors": {"primary": "#007bff"}},
442+
}
443+
444+
self.login(ADMIN_USERNAME)
445+
uri = "api/v1/theme/import/"
446+
447+
buf = self.create_theme_import_zip(theme_config)
448+
form_data = {
449+
"formData": (buf, "theme_export.zip"),
450+
}
451+
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
452+
response = json.loads(rv.data.decode("utf-8"))
453+
454+
assert rv.status_code == 200
455+
assert response == {"message": "Theme imported successfully"}
456+
457+
theme = db.session.query(Theme).filter_by(uuid=theme_config["uuid"]).one()
458+
assert theme.theme_name == "imported_theme"
459+
460+
# Cleanup
461+
db.session.delete(theme)
462+
db.session.commit()
463+
464+
def test_import_theme_overwrite(self):
465+
"""
466+
Theme API: Test import existing theme without and with overwrite
467+
"""
468+
theme_config = {
469+
"theme_name": "overwrite_theme",
470+
"uuid": str(uuid.uuid4()),
471+
"version": "1.0.0",
472+
"json_data": {"colors": {"primary": "#007bff"}},
473+
}
474+
475+
self.login(ADMIN_USERNAME)
476+
uri = "api/v1/theme/import/"
477+
478+
# First import
479+
buf = self.create_theme_import_zip(theme_config)
480+
form_data = {
481+
"formData": (buf, "theme_export.zip"),
482+
}
483+
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
484+
response = json.loads(rv.data.decode("utf-8"))
485+
486+
assert rv.status_code == 200
487+
assert response == {"message": "Theme imported successfully"}
488+
489+
# Import again without overwrite flag - should fail with structured error
490+
buf = self.create_theme_import_zip(theme_config)
491+
form_data = {
492+
"formData": (buf, "theme_export.zip"),
493+
}
494+
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
495+
response = json.loads(rv.data.decode("utf-8"))
496+
497+
assert rv.status_code == 422
498+
assert len(response["errors"]) == 1
499+
error = response["errors"][0]
500+
assert error["message"].startswith("Error importing theme")
501+
assert error["error_type"] == "GENERIC_COMMAND_ERROR"
502+
assert error["level"] == "warning"
503+
assert f"themes/{theme_config['theme_name']}.yaml" in str(error["extra"])
504+
assert "Theme already exists and `overwrite=true` was not passed" in str(
505+
error["extra"]
506+
)
507+
508+
# Import with overwrite flag - should succeed
509+
buf = self.create_theme_import_zip(theme_config)
510+
form_data = {
511+
"formData": (buf, "theme_export.zip"),
512+
"overwrite": "true",
513+
}
514+
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
515+
response = json.loads(rv.data.decode("utf-8"))
516+
517+
assert rv.status_code == 200
518+
assert response == {"message": "Theme imported successfully"}
519+
520+
# Cleanup
521+
theme = db.session.query(Theme).filter_by(uuid=theme_config["uuid"]).one()
522+
db.session.delete(theme)
523+
db.session.commit()

0 commit comments

Comments
 (0)