[IMP] core: allow auto_install restriction on a subset of dependencies

Currently, auto_install is triggered when all dependencies get
installed, but there are cases where one would want such trigger on
only a subset thereof.

e.g. we want `website_sale_dashboard` to auto-install when
`website_sale` is installed. Currently, it requires `web_dashboard` to
also be auto-installed otherwise `website_sale_dashboard` would "wait"
for both dependencies to be explicitly installed before the
auto-install triggers. That's despite `web_dashboard` not being very
useful on its own. More generally this is an issue with technical
modules which need to be marked as auto_install so as not to block
e.g. bridge modules from automatically installing.

This change allows setting `auto_install` to a subset of `depends`:

* if auto_install is set to `False`, the module does not get
automatically installed (no change in semantics)
* if auto_install is set to `True`, the module gets automatically
installed if and only if all its dependencies are installed (also no
change in semantics)
* if auto_install is set to a list of dependencies, the module will be
installed when all *these* dependencies are installed, other
dependencies (excluded from auto_install) will be installed
alongside as a consequence
* auto_install can be set to an empty list, in this case the module
will always be automatically installed regardless of its
dependencies (and will force their installation).

So after this change, `web_dashboard`'s auto_install can be set to
`False` (such that it's not installed if no module defining dashboards
is installed) and `website_sale_dashboard`'s manifest can be edited
to:

'auto_install': ['website_sale']

possibilities:

# no automatic installation
'depends': ['a', 'b'],
'auto_install': False

# automatic installation if both a and b are installed
'depends': ['a', 'b'],
'auto_install': True

# automatic installation if both a and b are installed (explicit)
'depends': ['a', 'b'],
'auto_install': ['a', 'b']

# automatic installation if b is installed, a will get forcefully
# installed if it isn't yet
'depends': ['a', 'b'],
'auto_install': ['b']

# always automatically installed, will cause the installation of
# its dependencies even if they're not marked explicitly
'depends': ['a', 'b'],
'auto_install': []

Task 1851328

closes odoo/odoo#29431

Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
This commit is contained in:
Xavier Morel
2019-05-07 11:44:26 +00:00
parent b9ec983571
commit 2cc7f0dd49
5 changed files with 53 additions and 14 deletions
+1
View File
@@ -77,6 +77,7 @@ CREATE TABLE ir_module_module_dependency (
write_uid integer, -- references res_users on delete set null,
name character varying(128),
module_id integer REFERENCES ir_module_module ON DELETE cascade,
auto_install_required boolean DEFAULT true,
primary key(id)
);
+12 -6
View File
@@ -382,7 +382,7 @@ class Module(models.Model):
# - at least one dependency is 'to install'
install_states = frozenset(('installed', 'to install', 'to upgrade'))
def must_install(module):
states = set(dep.state for dep in module.dependencies_id)
states = {dep.state for dep in module.dependencies_id if dep.auto_install_required}
return states <= install_states and 'to install' in states
modules = self
@@ -667,7 +667,7 @@ class Module(models.Model):
'license': terp.get('license', 'LGPL-3'),
'sequence': terp.get('sequence', 100),
'application': terp.get('application', False),
'auto_install': terp.get('auto_install', False),
'auto_install': terp.get('auto_install', False) is not False,
'icon': terp.get('icon', False),
'summary': terp.get('summary', ''),
'url': terp.get('url') or terp.get('live_test_url', ''),
@@ -707,8 +707,7 @@ class Module(models.Model):
updated_values = {}
for key in values:
old = getattr(mod, key)
updated = tools.ustr(values[key]) if isinstance(values[key], str) else values[key]
if (old or updated) and updated != old:
if (old or values[key]) and values[key] != old:
updated_values[key] = values[key]
if terp.get('installable', True) and mod.state == 'uninstallable':
updated_values['state'] = 'uninstalled'
@@ -724,7 +723,7 @@ class Module(models.Model):
mod = self.create(dict(name=mod_name, state=state, **values))
res[1] += 1
mod._update_dependencies(terp.get('depends', []))
mod._update_dependencies(terp.get('depends', []), terp.get('auto_install'))
mod._update_exclusions(terp.get('excludes', []))
mod._update_category(terp.get('category', 'Uncategorized'))
@@ -835,13 +834,15 @@ class Module(models.Model):
def get_apps_server(self):
return tools.config.get('apps_server', 'https://apps.odoo.com/apps')
def _update_dependencies(self, depends=None):
def _update_dependencies(self, depends=None, auto_install_requirements=()):
existing = set(dep.name for dep in self.dependencies_id)
needed = set(depends or [])
for dep in (needed - existing):
self._cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (self.id, dep))
for dep in (existing - needed):
self._cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (self.id, dep))
self._cr.execute('UPDATE ir_module_module_dependency SET auto_install_required = (name = any(%s)) WHERE module_id = %s',
(list(auto_install_requirements or ()), self.id))
self.invalidate_cache(['dependencies_id'], self.ids)
def _update_exclusions(self, excludes=None):
@@ -913,6 +914,11 @@ class ModuleDependency(models.Model):
depend_id = fields.Many2one('ir.module.module', 'Dependency', compute='_compute_depend')
state = fields.Selection(DEP_STATES, string='Status', compute='_compute_state')
auto_install_required = fields.Boolean(
default=True,
help="Whether this dependency blocks automatic installation "
"of the dependent")
@api.multi
@api.depends('name')
def _compute_depend(self):
+6 -3
View File
@@ -56,7 +56,7 @@ def initialize(cr):
info['author'],
info['website'], i, info['name'],
info['description'], category_id,
info['auto_install'], state,
info['auto_install'] is not False, state,
info['web'],
info['license'],
info['application'], info['icon'],
@@ -67,8 +67,11 @@ def initialize(cr):
'module_'+i, 'ir.module.module', 'base', id, True))
dependencies = info['depends']
for d in dependencies:
cr.execute('INSERT INTO ir_module_module_dependency \
(module_id,name) VALUES (%s, %s)', (id, d))
cr.execute(
'INSERT INTO ir_module_module_dependency (module_id, name, auto_install_required)'
' VALUES (%s, %s, %s)',
(id, d, d in (info['auto_install'] or ()))
)
# Install recursively all auto-installing modules
while True:
+16 -1
View File
@@ -379,7 +379,22 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
_check_module_names(cr, itertools.chain(tools.config['init'], tools.config['update']))
module_names = [k for k, v in tools.config['init'].items() if v]
# auto-install module second pass: recursive bit in db.py handles
# auto_install modules with no dependencies or where all
# dependencies are auto_install, couldn't get it to work with
# non-required auto_install deps so here
cr.execute("""
SELECT m.name FROM ir_module_module m
WHERE m.auto_install
AND m.state != 'installed'
AND NOT EXISTS (
SELECT 1 FROM ir_module_module_dependency d
JOIN ir_module_module mdep ON (d.name = mdep.name)
WHERE d.module_id = m.id
AND d.auto_install_required
AND mdep.state NOT IN ('installed', 'to install')
)""")
module_names = [k for k, v in tools.config['init'].items() if v] + [x[0] for x in cr.fetchall()]
if module_names:
modules = Module.search([('state', '=', 'uninstalled'), ('name', 'in', module_names)])
if modules:
+18 -4
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import ast
import functools
import collections
import imp
import importlib
import inspect
@@ -341,9 +341,23 @@ def load_information_from_description_file(module, mod_path=None):
readme_text = tools.file_open(readme_path[0]).read()
info['description'] = readme_text
if 'active' in info:
# 'active' has been renamed 'auto_install'
info['auto_install'] = info['active']
# auto_install is set to `False` if disabled, and a set of
# auto_install dependencies otherwise. That way, we can set
# auto_install: [] to always auto_install a module regardless of its
# dependencies
auto_install = info.get('auto_install', info.get('active', False))
if isinstance(auto_install, collections.Iterable):
info['auto_install'] = set(auto_install)
non_dependencies = info['auto_install'].difference(info['depends'])
assert not non_dependencies,\
"auto_install triggers must be dependencies, found " \
"non-dependencies [%s] for module %s" % (
', '.join(non_dependencies), module
)
elif auto_install:
info['auto_install'] = set(info['depends'])
else:
info['auto_install'] = False
info['version'] = adapt_version(info['version'])
return info