Merge pull request #14427 from ankush/app_uninstall_failure
fix: app uninstallation failure if module def link field isn't called "module"
This commit is contained in:
commit
a6bfbe4f1a
2 changed files with 121 additions and 30 deletions
|
|
@ -4,6 +4,8 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from typing import List, Dict
|
||||
|
||||
import frappe
|
||||
from frappe.defaults import _clear_cache
|
||||
|
|
@ -230,9 +232,29 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
|
|||
scheduled_backup(ignore_files=True)
|
||||
|
||||
frappe.flags.in_uninstall = True
|
||||
drop_doctypes = []
|
||||
|
||||
modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
|
||||
|
||||
drop_doctypes = _delete_modules(modules, dry_run=dry_run)
|
||||
_delete_doctypes(drop_doctypes, dry_run=dry_run)
|
||||
|
||||
if not dry_run:
|
||||
remove_from_installed_apps(app_name)
|
||||
frappe.db.commit()
|
||||
|
||||
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
|
||||
frappe.flags.in_uninstall = False
|
||||
|
||||
|
||||
def _delete_modules(modules: List[str], dry_run: bool) -> List[str]:
|
||||
""" Delete modules belonging to the app and all related doctypes.
|
||||
|
||||
Note: All record linked linked to Module Def are also deleted.
|
||||
|
||||
Returns: list of deleted doctypes."""
|
||||
drop_doctypes = []
|
||||
|
||||
doctype_link_field_map = _get_module_linked_doctype_field_map()
|
||||
for module_name in modules:
|
||||
print(f"Deleting Module '{module_name}'")
|
||||
|
||||
|
|
@ -242,45 +264,67 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
|
|||
print(f"* removing DocType '{doctype.name}'...")
|
||||
|
||||
if not dry_run:
|
||||
frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
|
||||
|
||||
if not doctype.issingle:
|
||||
if doctype.issingle:
|
||||
frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
|
||||
else:
|
||||
drop_doctypes.append(doctype.name)
|
||||
|
||||
linked_doctypes = frappe.get_all(
|
||||
"DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"]
|
||||
)
|
||||
ordered_doctypes = ["Workspace", "Report", "Page", "Web Form"]
|
||||
all_doctypes_with_linked_modules = ordered_doctypes + [
|
||||
doctype.parent
|
||||
for doctype in linked_doctypes
|
||||
if doctype.parent not in ordered_doctypes
|
||||
]
|
||||
doctypes_with_linked_modules = [
|
||||
x for x in all_doctypes_with_linked_modules if frappe.db.exists("DocType", x)
|
||||
]
|
||||
for doctype in doctypes_with_linked_modules:
|
||||
for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"):
|
||||
print(f"* removing {doctype} '{record}'...")
|
||||
if not dry_run:
|
||||
frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
|
||||
_delete_linked_documents(module_name, doctype_link_field_map, dry_run=dry_run)
|
||||
|
||||
print(f"* removing Module Def '{module_name}'...")
|
||||
if not dry_run:
|
||||
frappe.delete_doc("Module Def", module_name, ignore_on_trash=True, force=True)
|
||||
|
||||
for doctype in set(drop_doctypes):
|
||||
return drop_doctypes
|
||||
|
||||
|
||||
def _delete_linked_documents(
|
||||
module_name: str,
|
||||
doctype_linkfield_map: Dict[str, str],
|
||||
dry_run: bool
|
||||
) -> None:
|
||||
|
||||
"""Deleted all records linked with module def"""
|
||||
for doctype, fieldname in doctype_linkfield_map.items():
|
||||
for record in frappe.get_all(doctype, filters={fieldname: module_name}, pluck="name"):
|
||||
print(f"* removing {doctype} '{record}'...")
|
||||
if not dry_run:
|
||||
frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True)
|
||||
|
||||
def _get_module_linked_doctype_field_map() -> Dict[str, str]:
|
||||
""" Get all the doctypes which have module linked with them.
|
||||
|
||||
returns ordered dictionary with doctype->link field mapping."""
|
||||
|
||||
# Hardcoded to change order of deletion
|
||||
ordered_doctypes = [
|
||||
("Workspace", "module"),
|
||||
("Report", "module"),
|
||||
("Page", "module"),
|
||||
("Web Form", "module")
|
||||
]
|
||||
doctype_to_field_map = OrderedDict(ordered_doctypes)
|
||||
|
||||
linked_doctypes = frappe.get_all(
|
||||
"DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent", "fieldname"]
|
||||
)
|
||||
existing_linked_doctypes = [d for d in linked_doctypes if frappe.db.exists("DocType", d.parent)]
|
||||
|
||||
for d in existing_linked_doctypes:
|
||||
# DocType deletion is handled separately in the end
|
||||
if d.parent not in doctype_to_field_map and d.parent != "DocType":
|
||||
doctype_to_field_map[d.parent] = d.fieldname
|
||||
|
||||
return doctype_to_field_map
|
||||
|
||||
|
||||
def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None:
|
||||
for doctype in set(doctypes):
|
||||
print(f"* dropping Table for '{doctype}'...")
|
||||
if not dry_run:
|
||||
frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
|
||||
frappe.db.sql_ddl(f"drop table `tab{doctype}`")
|
||||
|
||||
if not dry_run:
|
||||
remove_from_installed_apps(app_name)
|
||||
frappe.db.commit()
|
||||
|
||||
click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
|
||||
frappe.flags.in_uninstall = False
|
||||
|
||||
|
||||
def post_install(rebuild_website=False):
|
||||
from frappe.website.utils import clear_website_cache
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import glob
|
|||
# imports - module imports
|
||||
import frappe
|
||||
import frappe.recorder
|
||||
from frappe.installer import add_to_installed_apps
|
||||
from frappe.installer import add_to_installed_apps, remove_app
|
||||
from frappe.utils import add_to_date, get_bench_relative_path, now
|
||||
from frappe.utils.backups import fetch_latest_backups
|
||||
|
||||
|
|
@ -465,3 +465,50 @@ class TestCommands(BaseTestCommands):
|
|||
self.execute("bench --site {site} set-admin-password test2")
|
||||
self.assertEqual(self.returncode, 0)
|
||||
self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')
|
||||
|
||||
|
||||
class RemoveAppUnitTests(unittest.TestCase):
|
||||
def test_delete_modules(self):
|
||||
from frappe.installer import (
|
||||
_delete_doctypes,
|
||||
_delete_modules,
|
||||
_get_module_linked_doctype_field_map,
|
||||
)
|
||||
|
||||
test_module = frappe.new_doc("Module Def")
|
||||
|
||||
test_module.update({"module_name": "RemoveThis", "app_name": "frappe"})
|
||||
test_module.save()
|
||||
|
||||
module_def_linked_doctype = frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"name": "Doctype linked with module def",
|
||||
"module": "RemoveThis",
|
||||
"custom": 1,
|
||||
"fields": [{
|
||||
"label": "Modulen't",
|
||||
"fieldname": "notmodule",
|
||||
"fieldtype": "Link",
|
||||
"options": "Module Def"
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
doctype_to_link_field_map = _get_module_linked_doctype_field_map()
|
||||
|
||||
self.assertIn("Report", doctype_to_link_field_map)
|
||||
self.assertIn(module_def_linked_doctype.name, doctype_to_link_field_map)
|
||||
self.assertEqual(doctype_to_link_field_map[module_def_linked_doctype.name], "notmodule")
|
||||
self.assertNotIn("DocType", doctype_to_link_field_map)
|
||||
|
||||
doctypes_to_delete = _delete_modules([test_module.module_name], dry_run=False)
|
||||
self.assertEqual(len(doctypes_to_delete), 1)
|
||||
|
||||
_delete_doctypes(doctypes_to_delete, dry_run=False)
|
||||
self.assertFalse(frappe.db.exists("Module Def", test_module.module_name))
|
||||
self.assertFalse(frappe.db.exists("DocType", module_def_linked_doctype.name))
|
||||
|
||||
def test_dry_run(self):
|
||||
"""Check if dry run in not destructive."""
|
||||
|
||||
# nothing to assert, if this fails rest of the test suite will crumble.
|
||||
remove_app("frappe", dry_run=True, yes=True, no_backup=True)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue