Skip to content

Commit 48d749d

Browse files
committed
[IMP] remove and uninstall modules and themes recursively
Current implementation won't uninstall/remove dependencies. This can easily lead to a broken database, and is an unexpected and important divergence compared to the normal ORM uninstallation or removal process. @moduon MT-7110
1 parent c8c1a6e commit 48d749d

File tree

1 file changed

+61
-17
lines changed

1 file changed

+61
-17
lines changed

src/util/modules.py

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,49 @@ def module_installed(cr, module):
9898
return modules_installed(cr, module)
9999

100100

101-
def uninstall_module(cr, module):
101+
def module_dependencies(cr, module):
102+
"""Get dependencies of given module.
103+
104+
:param str module: name of the module
105+
:return: list names of the dependencies
106+
:rtype: list(str)
107+
"""
108+
found = []
109+
while True:
110+
cr.execute(
111+
"""
112+
SELECT ARRAY_AGG(DISTINCT m.name ORDER BY m.name)
113+
FROM ir_module_module m
114+
INNER JOIN ir_module_module_dependency d ON d.module_id = m.id
115+
WHERE d.name = ANY(%s)
116+
""",
117+
(found + [module],),
118+
)
119+
new = cr.fetchone()[0]
120+
if new == found:
121+
return found
122+
found = new
123+
124+
125+
def uninstall_module(cr, module, with_dependencies=False):
102126
"""
103127
Uninstall and remove all records owned by a module.
104128
105129
:param str module: name of the module to uninstall
130+
:param bool with_dependencies: whether to also remove dependencies of the module
131+
:return: set of uninstalled module names
132+
:rtype: set(str)
106133
"""
134+
result = set()
107135
cr.execute("SELECT id FROM ir_module_module WHERE name=%s", (module,))
108136
(mod_id,) = cr.fetchone() or [None]
109137
if not mod_id:
110-
return
138+
return result
139+
140+
if with_dependencies:
141+
dependencies = module_dependencies(cr, module)
142+
for dep in dependencies:
143+
result.union(uninstall_module(cr, dep, with_dependencies=with_dependencies))
111144

112145
# delete constraints only owned by this module
113146
cr.execute(
@@ -219,14 +252,18 @@ def uninstall_module(cr, module):
219252
if table_exists(cr, "ir_translation"):
220253
cr.execute("DELETE FROM ir_translation WHERE module=%s", [module])
221254
cr.execute("UPDATE ir_module_module SET state='uninstalled' WHERE name=%s", (module,))
255+
return result | {module}
222256

223257

224-
def uninstall_theme(cr, theme, base_theme=None):
258+
def uninstall_theme(cr, theme, base_theme=None, with_dependencies=False):
225259
"""
226260
Uninstall a theme module and remove it from websites.
227261
228262
:param str theme: name of the theme module to uninstall
229263
:param str or None base_theme: if not `None`, unload first this base theme
264+
:param bool with_dependencies: whether to also remove dependencies of the theme
265+
:return: set of uninstalled module names
266+
:rtype: set(str)
230267
231268
.. warning::
232269
@@ -238,7 +275,7 @@ def uninstall_theme(cr, theme, base_theme=None):
238275
cr.execute("SELECT id FROM ir_module_module WHERE name=%s AND state in %s", [theme, INSTALLED_MODULE_STATES])
239276
(theme_id,) = cr.fetchone() or [None]
240277
if not theme_id:
241-
return
278+
return None
242279

243280
env_ = env(cr)
244281
IrModuleModule = env_["ir.module.module"]
@@ -253,17 +290,20 @@ def uninstall_theme(cr, theme, base_theme=None):
253290
for website in websites:
254291
IrModuleModule._theme_remove(website)
255292
flush(env_["base"])
256-
uninstall_module(cr, theme)
293+
return uninstall_module(cr, theme, with_dependencies=with_dependencies)
257294

258295

259-
def remove_module(cr, module):
296+
def remove_module(cr, module, with_dependencies=False):
260297
"""
261298
Completely remove a module.
262299
263300
This operation is equivalent to uninstall and removal of *all* references to
264301
the module - no trace of it is left in the database.
265302
266303
:param str module: name of the module to remove
304+
:param bool with_dependencies: whether to also remove dependencies of the module
305+
:return: set of uninstalled module names
306+
:rtype: set(str)
267307
268308
.. warning::
269309
Since this function removes *all* data associated to the module. Ensure to
@@ -273,15 +313,17 @@ def remove_module(cr, module):
273313
# module need to be currently installed and running as deletions
274314
# are made using orm.
275315

276-
uninstall_module(cr, module)
277-
cr.execute("DELETE FROM ir_module_module_dependency WHERE name=%s", (module,))
278-
cr.execute("DELETE FROM ir_module_module WHERE name=%s RETURNING id", (module,))
316+
result = uninstall_module(cr, module, with_dependencies=with_dependencies)
317+
names = list(result)
318+
cr.execute("DELETE FROM ir_module_module_dependency WHERE name = ANY(%s)", (names,))
319+
cr.execute("DELETE FROM ir_module_module WHERE name = ANY(%s) RETURNING id", (names,))
279320
if cr.rowcount:
280-
[mod_id] = cr.fetchone()
281-
cr.execute("DELETE FROM ir_model_data WHERE model='ir.module.module' AND res_id=%s", [mod_id])
321+
ids = [id_ for (id_,) in cr.fetchall()]
322+
cr.execute("DELETE FROM ir_model_data WHERE model='ir.module.module' AND res_id = ANY(%s)", (ids,))
323+
return result
282324

283325

284-
def remove_theme(cr, theme, base_theme=None):
326+
def remove_theme(cr, theme, base_theme=None, with_dependencies=False):
285327
"""
286328
Uninstall a theme module.
287329
@@ -290,12 +332,14 @@ def remove_theme(cr, theme, base_theme=None):
290332
291333
See :func:`remove_module` and :func:`uninstall_theme`.
292334
"""
293-
uninstall_theme(cr, theme, base_theme=base_theme)
294-
cr.execute("DELETE FROM ir_module_module_dependency WHERE name=%s", (theme,))
295-
cr.execute("DELETE FROM ir_module_module WHERE name=%s RETURNING id", (theme,))
335+
result = uninstall_theme(cr, theme, base_theme=base_theme, with_dependencies=with_dependencies)
336+
themes = list(result)
337+
cr.execute("DELETE FROM ir_module_module_dependency WHERE name = ANY(%s)", (themes,))
338+
cr.execute("DELETE FROM ir_module_module WHERE name = ANY(%s) RETURNING id", (themes,))
296339
if cr.rowcount:
297-
[mod_id] = cr.fetchone()
298-
cr.execute("DELETE FROM ir_model_data WHERE model='ir.module.module' AND res_id=%s", [mod_id])
340+
ids = [id_ for (id_,) in cr.fetchall()]
341+
cr.execute("DELETE FROM ir_model_data WHERE model='ir.module.module' AND res_id = ANY(%s)", (ids,))
342+
return result
299343

300344

301345
def _update_view_key(cr, old, new):

0 commit comments

Comments
 (0)