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:
Ankush Menat 2021-10-13 16:38:18 +05:30 committed by GitHub
commit a6bfbe4f1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 121 additions and 30 deletions

View file

@ -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

View file

@ -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)