diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index de7e3b082d..587e3b7312 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -697,6 +697,7 @@ def create_desktop_icons_from_workspace(): if w.module: app_name = w.app or frappe.db.get_value("Module Def", w.module, "app_name") if app_name in frappe.get_installed_apps(): + icon.app_name = app_name app_title = frappe.get_hooks("app_title", app_name=app_name)[0] app_icon = frappe.db.exists("Desktop Icon", {"label": app_title, "icon_type": "App"}) if app_icon: @@ -744,6 +745,7 @@ def create_desktop_icons_from_installed_apps(): icon.standard = 1 icon.idx = index icon.icon_type = "App" + icon.app = a icon.link = app_details[0]["route"] icon.logo_url = app_details[0]["logo"] if not frappe.db.exists("Desktop Icon", [{"label": icon.label, "icon_type": icon.icon_type}]): diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index c45b249cf7..40c75bc697 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -127,6 +127,8 @@ class Workspace(Document): def on_trash(self): if self.public and not is_workspace_manager(): frappe.throw(_("You need to be Workspace Manager to delete a public workspace.")) + self.delete_desktop_icon() + self.delete_workspace_sidebar() self.delete_from_my_workspaces() def delete_from_my_workspaces(self): @@ -143,6 +145,25 @@ class Workspace(Document): if self.module and frappe.conf.developer_mode: delete_folder(self.module, "Workspace", self.title) + def delete_desktop_icon(self): + if self.public: + desktop_icon = frappe.get_all( + "Desktop Icon", + filters=[{"link_type": "Workspace"}, {"link_to": self.name}], + limit=1, + pluck="name", + ) + if desktop_icon: + frappe.delete_doc("Desktop Icon", desktop_icon[0]) + + def delete_workspace_sidebar(self): + if self.public: + workspace_sidebar = frappe.get_all( + "Workspace Sidebar", filters=[{"name": self.name}], limit=1, pluck="name" + ) + if workspace_sidebar: + frappe.delete_doc("Workspace Sidebar", workspace_sidebar[0]) + @staticmethod def get_module_wise_workspaces(): workspaces = frappe.get_all( diff --git a/frappe/migrate.py b/frappe/migrate.py index 623fa1bcbe..87287408bc 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -182,6 +182,9 @@ class SiteMigration: print("Removing orphan doctypes...") frappe.model.sync.remove_orphan_doctypes() + frappe.model.sync.remove_orphan_entities() + frappe.model.sync.delete_duplicate_icons() + print("Syncing portal menu...") frappe.get_single("Portal Settings").sync_menu() diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 4d6b01c14b..61c4d0f0f3 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -6,6 +6,7 @@ perms will get synced only if none exist """ import os +import re import frappe from frappe.cache_manager import clear_controller_cache @@ -197,3 +198,94 @@ def remove_orphan_doctypes(): update_progress_bar("Deleting orphaned DocTypes", i, len(orphan_doctypes)) frappe.db.commit() print() + + +def remove_orphan_entities(): + entites = ["Workspace", "Dashboard", "Page", "Report"] + entity_filter_map = { + "Workspace": {"public": 1}, + "Page": {"standard": "Yes"}, + "Report": {"is_standard": "Yes"}, + "Dashboard": {"is_standard": True}, + } + entity_file_map = create_entity_file_map(entites) + for entity in entites: + print(f"Removing orphan {entity}s") + all_enitities = frappe.get_all( + entity, filters=entity_filter_map.get(entity), fields=["name", "module"] + ) + for i, w in enumerate(all_enitities): + try: + entity_file_map[entity][w.name] + except KeyError: + try: + print(f"Deleting entity {entity} {w.name}") + frappe.delete_doc(entity, w.name, force=True, ignore_missing=True) + update_progress_bar(f"Deleting orphaned {entity}", i, len(all_enitities)) + print() + + except Exception as e: + print(f"Error occurred while deleting entity: {entity} {w.name}") + print(e) + # save the deleted icons + frappe.db.commit() # nosemgrep + + +def create_entity_file_map(entities): + import glob + + from frappe.modules.import_file import read_doc_from_file + + entity_file_map = {} + for entity in entities: + entity_file_map[entity] = {} + for app in frappe.get_installed_apps(): + app_path = frappe.get_app_path(app) + for entity in entities: + entity_folder = entity.lower() + if entity.lower() == "dashboard": + entity_folder = f"*_{entity_folder}" + entity_files = list(glob.glob(f"{app_path}/**/{entity_folder}/**/*.json", recursive=True)) + for file in entity_files: + entity_json = read_doc_from_file(file) + if isinstance(entity_json, dict): + entity_file_map[entity][entity_json.get("name")] = file + elif isinstance(entity_json, list): + if len(entity_json) > 0: + entity_file_map[entity][entity_json[0].get("name")] = file + + return entity_file_map + + +def check_if_record_exists(type=None, path=None, entity_type=None, name=None, module_name=None): + scrubbed_name = frappe.scrub(name.lower()) + scrubbed_entity_type = frappe.scrub(entity_type.lower()) + if scrubbed_entity_type == "dashboard" and module_name: + scrubbed_entity_type = f"{frappe.scrub(module_name.lower())}_dashboard" + + def build_path(entity_name): + if type == "app": + return os.path.join(path, scrubbed_entity_type, f"{entity_name}.json") + return os.path.join(path, scrubbed_entity_type, entity_name, f"{entity_name}.json") + + entity_path = build_path(scrubbed_name) + if os.path.exists(entity_path): + return True + + return False + + +def delete_duplicate_icons(): + # This handles app icons which are renamed. Removes the old entry from db. + for app in frappe.get_installed_apps(): + icons = frappe.get_all("Desktop Icon", filters=[{"icon_type": "App"}, {"app": app}], pluck="name") + + if len(icons) > 1: + for i in icons: + app_path = frappe.get_app_path(app) + if not check_if_record_exists(type="app", path=app_path, entity_type="Desktop Icon", name=i): + print(f"Deleting icon {i}") + frappe.delete_doc("Desktop Icon", i) + + # save the deleted icons + frappe.db.commit() # nosemgrep diff --git a/frappe/tests/test_removing_orphans.py b/frappe/tests/test_removing_orphans.py new file mode 100644 index 0000000000..5dcf9484a1 --- /dev/null +++ b/frappe/tests/test_removing_orphans.py @@ -0,0 +1,29 @@ +import frappe +from frappe.model.sync import remove_orphan_entities +from frappe.modules.export_file import delete_folder +from frappe.tests import IntegrationTestCase + + +class TestRemovingOrphans(IntegrationTestCase): + def test_removing_orphan(self): + _before = frappe.conf.developer_mode + frappe.conf.developer_mode = True + # Create a new report + report = frappe.new_doc("Report") + args = { + "doctype": "Report", + "report_name": "Orphan Report", + "ref_doctype": "DocType", + "is_standard": "Yes", + "module": "Custom", + } + report.update(args) + report.save() + print(f"Created report: {report.name}") + # delete only fixture (emulating that the export/entity is deleted by the developer) + delete_folder("Custom", "Report", report.name) + self.assertTrue(frappe.db.exists("Report", report.name)) + if frappe.db.exists("Report", report.name): + remove_orphan_entities() + self.assertFalse(frappe.db.exists("Report", report.name)) + frappe.conf.developer_mode = _before