Skip to content

Commit 7735d28

Browse files
committed
feat: support for asset translation
feat: import translated dashboard roles feat: import translated charts feat: translated dashboard filters fix: hide original dashboards feat: translated dashboard tabs
1 parent abad1d9 commit 7735d28

11 files changed

+350
-59
lines changed

tutoraspects/plugin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
"SUPERSET_OPENEDX_COURSES_LIST_PATH",
156156
"/api/courses/v1/courses/?permissions={permission}&username={username}",
157157
),
158+
("SUPERSET_OPENEDX_PREFERENCE_PATH", "api/user/v1/preferences/{username}"),
158159
(
159160
"SUPERSET_ROLES_MAPPING",
160161
{
@@ -189,7 +190,7 @@
189190
"SUPERSET_SUPPORTED_LANGUAGES",
190191
{
191192
"en": {"flag": "us", "name": "English"},
192-
"es": {"flag": "es", "name": "Spanish"},
193+
"es": {"flag": "es", "name": "Español"},
193194
},
194195
),
195196
("SUPERSET_EXTRA_JINJA_FILTERS", {}),
@@ -309,7 +310,6 @@
309310
# Each override is a pair: (setting_name, new_value). For example:
310311
### ("PLATFORM_NAME", "My platform"),
311312
# Superset overrides
312-
("SUPERSET_XAPI_DASHBOARD_SLUG", "openedx-xapi"),
313313
("SUPERSET_ROW_LEVEL_SECURITY_XAPI_GROUP_KEY", "xapi_course_id"),
314314
]
315315
)
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
"""Import a list of assets from a yaml file and create them in the superset assets folder."""
22
import os
3-
from zipfile import ZipFile
4-
3+
import uuid
54
import yaml
5+
from zipfile import ZipFile
66
from superset.app import create_app
77

88
app = create_app()
99
app.app_context().push()
1010

11+
from superset import security_manager
1112
from superset.commands.importers.v1.assets import ImportAssetsCommand
1213
from superset.commands.importers.v1.utils import get_contents_from_bundle
1314
from superset.extensions import db
1415
from superset.models.dashboard import Dashboard
1516
from superset.utils.database import get_or_create_db
1617

17-
from superset import security_manager
18+
from copy import deepcopy
1819

1920
BASE_DIR = "/app/assets/superset"
2021

@@ -25,8 +26,17 @@
2526
"table_name": "datasets",
2627
}
2728

29+
for folder in ASSET_FOLDER_MAPPING.values():
30+
os.makedirs(f"{BASE_DIR}/{folder}", exist_ok=True)
31+
2832
FILE_NAME_ATTRIBUTE = "_file_name"
2933

34+
TRANSLATIONS_FILE_PATH = "/app/pythonpath/locale.yaml"
35+
ASSETS_FILE_PATH = "/app/pythonpath/assets.yaml"
36+
ASSETS_ZIP_PATH = "/app/assets/assets.zip"
37+
38+
ASSETS_TRANSLATIONS = yaml.load(open(TRANSLATIONS_FILE_PATH, "r"), Loader=yaml.FullLoader)
39+
3040

3141
def main():
3242
create_assets()
@@ -35,7 +45,7 @@ def main():
3545
def create_assets():
3646
"""Create assets from a yaml file."""
3747
roles = {}
38-
with open("/app/pythonpath/assets.yaml", "r") as file:
48+
with open(ASSETS_FILE_PATH, "r") as file:
3949
extra_assets = yaml.safe_load(file)
4050

4151
if not extra_assets:
@@ -45,53 +55,149 @@ def create_assets():
4555
# For each asset, create a file in the right folder
4656
for asset in extra_assets:
4757
if FILE_NAME_ATTRIBUTE not in asset:
48-
print(f"Asset {asset} has no _file_name")
49-
continue
58+
raise Exception(f"Asset {asset} has no {FILE_NAME_ATTRIBUTE}")
5059
file_name = asset.pop(FILE_NAME_ATTRIBUTE)
5160

5261
# Find the right folder to create the asset in
5362
for asset_name, folder in ASSET_FOLDER_MAPPING.items():
54-
if asset_name in asset:
55-
if folder == "databases":
56-
# This will fix the URI connection string by setting the right password.
57-
create_superset_db(
58-
asset["database_name"], asset["sqlalchemy_uri"]
59-
)
60-
elif folder == "dashboards":
61-
dashboard_roles = asset.pop("_roles", None)
62-
if dashboard_roles:
63-
roles[asset["uuid"]] = [security_manager.find_role(role) for role in dashboard_roles]
64-
65-
path = f"{BASE_DIR}/{folder}/{file_name}"
66-
os.makedirs(os.path.dirname(path), exist_ok=True)
67-
file = open(path, "w")
68-
yaml.dump(asset, file)
69-
file.close()
70-
break
71-
72-
# Create the zip file and import the assets
73-
zip_path = "/app/assets/assets.zip"
74-
with ZipFile(zip_path, "w") as zip:
75-
for folder in ASSET_FOLDER_MAPPING.values():
76-
for file_name in os.listdir(f"{BASE_DIR}/{folder}"):
77-
zip.write(f"{BASE_DIR}/{folder}/{file_name}", f"import/{folder}/{file_name}")
78-
zip.write(f"{BASE_DIR}/metadata.yaml", "import/metadata.yaml")
63+
if not asset_name in asset:
64+
continue
7965

80-
contents = get_contents_from_bundle(zip)
81-
command = ImportAssetsCommand(
82-
contents,
66+
write_asset_to_file(asset, asset_name, folder, file_name, roles)
67+
break
68+
69+
create_zip_and_import_assets()
70+
update_dashboard_roles(roles)
71+
72+
73+
def get_uuid5(base_uuid, name):
74+
"""Generate an idempotent uuid."""
75+
base_uuid = uuid.UUID(base_uuid)
76+
base_namespace = uuid.uuid5(base_uuid, "superset")
77+
return uuid.uuid5(base_namespace, name)
78+
79+
80+
def write_asset_to_file(asset, asset_name, folder, file_name, roles):
81+
"""Write an asset to a file and generated translated assets"""
82+
if folder == "databases":
83+
# This will fix the URI connection string by setting the right password.
84+
create_superset_db(asset["database_name"], asset["sqlalchemy_uri"])
85+
86+
asset_translation = ASSETS_TRANSLATIONS.get(asset.get("uuid"), {})
87+
88+
for language, title in asset_translation.items():
89+
updated_asset = generate_translated_asset(
90+
asset, asset_name, folder, language, title, roles
8391
)
8492

85-
command.run()
93+
path = f"{BASE_DIR}/{folder}/{file_name}-{language}.yaml"
94+
with open(path, "w") as file:
95+
yaml.dump(updated_asset, file)
8696

87-
os.remove(zip_path)
88-
89-
# Create the roles
90-
for dashboard_uuid, role_ids in roles.items():
91-
dashboard = db.session.query(Dashboard).filter_by(uuid=dashboard_uuid).one()
92-
dashboard.roles = role_ids
93-
dashboard.published = True
94-
db.session.commit()
97+
## WARNING: Dashboard are assigned a Dummy role which prevents users to
98+
# access the original dashboards.
99+
dashboard_roles = asset.pop("_roles", None)
100+
if dashboard_roles:
101+
roles[asset["uuid"]] = [
102+
security_manager.find_role("Admin")
103+
]
104+
105+
path = f"{BASE_DIR}/{folder}/{file_name}.yaml"
106+
with open(path, "w") as file:
107+
yaml.dump(asset, file)
108+
109+
110+
def generate_translated_asset(asset, asset_name, folder, language, title, roles):
111+
"""Generate a translated asset with their elements updated"""
112+
copy = deepcopy(asset)
113+
copy["uuid"] = str(get_uuid5(copy["uuid"], language))
114+
copy[asset_name] = title
115+
116+
if folder == "dashboards":
117+
copy["slug"] = f"{copy['slug']}-{language}"
118+
119+
dashboard_roles = copy.pop("_roles", [])
120+
translated_dashboard_roles = []
121+
122+
for role in dashboard_roles:
123+
translated_dashboard_roles.append(f"{role} - {language}")
124+
125+
roles[copy["uuid"]] = [
126+
security_manager.find_role(role) for role in translated_dashboard_roles
127+
]
128+
129+
generate_translated_dashboard_elements(copy, language)
130+
generate_translated_dashboard_filters(copy, language)
131+
return copy
132+
133+
134+
def generate_translated_dashboard_elements(copy, language):
135+
"""Generate translated elements for a dashboard"""
136+
position = copy.get("position", {})
137+
138+
for element in position.values():
139+
if not isinstance(element, dict):
140+
continue
141+
142+
meta = element.get("meta", {})
143+
original_uuid = meta.get("uuid", None)
144+
145+
element_type = element.get("type", "Unknown")
146+
147+
translation, element_type, element_id = None, None, None
148+
149+
if original_uuid:
150+
if not original_uuid in ASSETS_TRANSLATIONS:
151+
print(f"Chart {meta['uuid']} not found in translations")
152+
continue
153+
154+
element_type = "Chart"
155+
element_id = str(get_uuid5(original_uuid, language))
156+
translation = ASSETS_TRANSLATIONS.get(original_uuid, {}).get(
157+
language, meta["sliceName"]
158+
)
159+
160+
meta["sliceName"] = translation
161+
meta["uuid"] = element_id
162+
163+
elif element.get("type") == "TAB":
164+
chart_body_id = element.get("id")
165+
if not meta or not meta.get("text"):
166+
continue
167+
168+
if not chart_body_id in ASSETS_TRANSLATIONS:
169+
print(f"Tab {chart_body_id} not found in translations")
170+
continue
171+
172+
element_type = "Tab"
173+
element_id = chart_body_id
174+
translation = ASSETS_TRANSLATIONS.get(chart_body_id, {}).get(
175+
language, meta["text"]
176+
)
177+
178+
meta["text"] = translation
179+
180+
if translation and element_type and element_id:
181+
print(
182+
f"Generating {element_type} {element_id} for language {language} {translation}"
183+
)
184+
185+
186+
def generate_translated_dashboard_filters(copy, language):
187+
"""Generate translated filters for a dashboard"""
188+
metadata = copy.get("metadata", {})
189+
190+
for filter in metadata.get("native_filter_configuration", []):
191+
element_type = "Filter"
192+
element_id = filter["id"]
193+
translation = ASSETS_TRANSLATIONS.get(element_id, {}).get(
194+
language, filter["name"]
195+
)
196+
197+
filter["name"] = translation
198+
print(
199+
f"Generating {element_type} {element_id} for language {language} {translation}"
200+
)
95201

96202

97203
def create_superset_db(database_name, uri) -> None:
@@ -101,5 +207,30 @@ def create_superset_db(database_name, uri) -> None:
101207
db.session.commit()
102208

103209

210+
def create_zip_and_import_assets():
211+
"""Create a zip file with all the assets and import them in superset"""
212+
with ZipFile(ASSETS_ZIP_PATH, "w") as zip:
213+
for folder in ASSET_FOLDER_MAPPING.values():
214+
for file_name in os.listdir(f"{BASE_DIR}/{folder}"):
215+
zip.write(
216+
f"{BASE_DIR}/{folder}/{file_name}", f"import/{folder}/{file_name}"
217+
)
218+
zip.write(f"{BASE_DIR}/metadata.yaml", "import/metadata.yaml")
219+
contents = get_contents_from_bundle(zip)
220+
command = ImportAssetsCommand(contents)
221+
command.run()
222+
223+
os.remove(ASSETS_ZIP_PATH)
224+
225+
226+
def update_dashboard_roles(roles):
227+
"""Update the roles of the dashboards"""
228+
for dashboard_uuid, role_ids in roles.items():
229+
dashboard = db.session.query(Dashboard).filter_by(uuid=dashboard_uuid).one()
230+
dashboard.roles = role_ids
231+
dashboard.published = True
232+
db.session.commit()
233+
234+
104235
if __name__ == "__main__":
105236
main()

0 commit comments

Comments
 (0)