diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 659929c2f04..d630faecad0 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -161,7 +161,11 @@ class ir_model(osv.osv): self._drop_table(cr, user, ids, context) res = super(ir_model, self).unlink(cr, user, ids, context) - pooler.restart_pool(cr.dbname) + if not context.get(MODULE_UNINSTALL_FLAG): + # only reload pool for normal unlink. For module uninstall the + # reload is done independently in openerp.modules.loading + pooler.restart_pool(cr.dbname) + return res def write(self, cr, user, ids, vals, context=None): @@ -832,6 +836,7 @@ class ir_model_data(osv.osv): context = dict(context or {}) context[MODULE_UNINSTALL_FLAG] = True # enable model/field deletion + ids_set = set(ids) wkf_todo = [] to_unlink = [] to_drop_table = [] @@ -867,8 +872,11 @@ class ir_model_data(osv.osv): _logger.info('Drop CONSTRAINT %s@%s', name[11:], model) continue - to_unlink.append((model, res_id)) - if model=='workflow.activity': + pair_to_unlink = (model, res_id) + if pair_to_unlink not in to_unlink: + to_unlink.append(pair_to_unlink) + + if model == 'workflow.activity': cr.execute('select res_type,res_id from wkf_instance where id IN (select inst_id from wkf_workitem where act_id=%s)', (res_id,)) wkf_todo.extend(cr.fetchall()) cr.execute("update wkf_transition set condition='True', group_id=NULL, signal=NULL,act_to=act_from,act_from=%s where act_to=%s", (res_id,res_id)) @@ -888,13 +896,13 @@ class ir_model_data(osv.osv): for (model, res_id) in to_unlink: if model in ('ir.model','ir.model.fields', 'ir.model.data'): continue - model_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)]) - if len(model_ids) > 1: + external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)]) + if (set(external_ids)-ids_set): # if other modules have defined this record, we do not delete it continue _logger.info('Deleting %s@%s', res_id, model) try: - self.pool.get(model).unlink(cr, uid, res_id, context=context) + self.pool.get(model).unlink(cr, uid, [res_id], context=context) except: _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True) cr.commit() @@ -902,18 +910,18 @@ class ir_model_data(osv.osv): for (model, res_id) in to_unlink: if model not in ('ir.model.fields',): continue - model_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)]) - if len(model_ids) > 1: + external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)]) + if (set(external_ids)-ids_set): # if other modules have defined this record, we do not delete it continue _logger.info('Deleting %s@%s', res_id, model) - self.pool.get(model).unlink(cr, uid, res_id, context=context) + self.pool.get(model).unlink(cr, uid, [res_id], context=context) for (model, res_id) in to_unlink: if model != 'ir.model': continue - model_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)]) - if len(model_ids) > 1: + external_ids = self.search(cr, uid, [('model', '=', model),('res_id', '=', res_id)]) + if (set(external_ids)-ids_set): # if other modules have defined this record, we do not delete it continue _logger.info('Deleting %s@%s', res_id, model) diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 77ba7f1d51c..526ca5a4910 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -365,6 +365,10 @@ class module(osv.osv): return True def module_uninstall(self, cr, uid, ids, context=None): + """Perform the various steps required to uninstall a module completely + including the deletion of all database structures created by the module: + tables, columns, constraints, etc.""" + # uninstall must be done respecting the reverse-dependency order ir_model_data = self.pool.get('ir.model.data') modules_to_remove = [m.name for m in self.browse(cr, uid, ids, context)] @@ -372,8 +376,6 @@ class module(osv.osv): ir_model_data._pre_process_unlink(cr, uid, data_ids, context) ir_model_data.unlink(cr, uid, data_ids, context) self.write(cr, uid, ids, {'state': 'uninstalled'}) - - # should we call process_end instead of loading, or both ? return True def downstream_dependencies(self, cr, uid, ids, known_dep_ids=None, diff --git a/openerp/addons/base/module/wizard/base_module_upgrade.py b/openerp/addons/base/module/wizard/base_module_upgrade.py index cb27a264e91..a27f79e6905 100644 --- a/openerp/addons/base/module/wizard/base_module_upgrade.py +++ b/openerp/addons/base/module/wizard/base_module_upgrade.py @@ -72,7 +72,7 @@ class base_module_upgrade(osv.osv_memory): def upgrade_module(self, cr, uid, ids, context=None): ir_module = self.pool.get('ir.module.module') - # install and upgrade modules + # install/upgrade: double-check preconditions ids = ir_module.search(cr, uid, [('state', 'in', ['to upgrade', 'to install'])]) unmet_packages = [] mod_dep_obj = self.pool.get('ir.module.module.dependency') @@ -86,16 +86,15 @@ class base_module_upgrade(osv.osv_memory): raise osv.except_osv(_('Unmet dependency !'), _('Following modules are not installed or unknown: %s') % ('\n\n' + '\n'.join(unmet_packages))) ir_module.download(cr, uid, ids, context=context) - # uninstall modules - to_remove_ids = ir_module.search(cr, uid, [('state', 'in', ['to remove'])]) - ir_module.module_uninstall(cr, uid, to_remove_ids, context) + # uninstall: double-check preconditions + # TODO: check all dependent modules are uninstalled + # XXX mod_ids_to_uninstall = ir_module.search(cr, uid, [('state', '=', 'to remove')]) - cr.commit() + cr.commit() # persist changes before reopening a cursor pooler.restart_pool(cr.dbname, update_module=True) ir_model_data = self.pool.get('ir.model.data') _, res_id = ir_model_data.get_object_reference(cr, uid, 'base', 'view_base_module_upgrade_install') - return { 'view_type': 'form', 'view_mode': 'form', diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index c56edd0360d..efc73d44a58 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -40,6 +40,7 @@ import openerp.release as release import openerp.tools as tools import openerp.tools.assertion_report as assertion_report +from openerp import SUPERUSER_ID from openerp.tools.translate import _ from openerp.modules.module import initialize_sys_path, \ load_openerp_module, init_module_models @@ -161,7 +162,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= modobj = pool.get('ir.module.module') if perform_checks: - modobj.check(cr, 1, [module_id]) + modobj.check(cr, SUPERUSER_ID, [module_id]) idref = {} @@ -172,7 +173,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): if package.state=='to upgrade': # upgrading the module information - modobj.write(cr, 1, [module_id], modobj.get_values_from_terp(package.data)) + modobj.write(cr, SUPERUSER_ID, [module_id], modobj.get_values_from_terp(package.data)) load_init_xml(module_name, idref, mode) load_update_xml(module_name, idref, mode) load_data(module_name, idref, mode) @@ -201,9 +202,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= ver = release.major_version + '.' + package.data['version'] # Set new modules and dependencies - modobj.write(cr, 1, [module_id], {'state': 'installed', 'latest_version': ver}) + modobj.write(cr, SUPERUSER_ID, [module_id], {'state': 'installed', 'latest_version': ver}) # Update translations for all installed languages - modobj.update_translations(cr, 1, [module_id], None) + modobj.update_translations(cr, SUPERUSER_ID, [module_id], None) package.state = 'installed' for kind in ('init', 'demo', 'update'): @@ -304,15 +305,15 @@ def load_modules(db, force_demo=False, status=None, update_module=False): mods = [k for k in tools.config['init'] if tools.config['init'][k]] if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) + ids = modobj.search(cr, SUPERUSER_ID, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) if ids: - modobj.button_install(cr, 1, ids) + modobj.button_install(cr, SUPERUSER_ID, ids) mods = [k for k in tools.config['update'] if tools.config['update'][k]] if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) + ids = modobj.search(cr, SUPERUSER_ID, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) if ids: - modobj.button_upgrade(cr, 1, ids) + modobj.button_upgrade(cr, SUPERUSER_ID, ids) cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) @@ -322,7 +323,11 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # partially installed modules (i.e. installed/to upgrade), to # offer a consistent system to the second part: installing # newly selected modules. - states_to_load = ['installed', 'to upgrade'] + # We include the modules 'to remove' in the first step, because + # they are part of the "currently installed" modules. They will + # be dropped in STEP 6 later, before restarting the loading + # process. + states_to_load = ['installed', 'to upgrade', 'to remove'] processed = load_marked_modules(cr, graph, states_to_load, force, status, report, loaded_modules) processed_modules.extend(processed) if update_module: @@ -333,9 +338,9 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # load custom models cr.execute('select model from ir_model where state=%s', ('manual',)) for model in cr.dictfetchall(): - pool.get('ir.model').instanciate(cr, 1, model['model'], {}) + pool.get('ir.model').instanciate(cr, SUPERUSER_ID, model['model'], {}) - # STEP 4: Finish and cleanup + # STEP 4: Finish and cleanup installations if processed_modules: cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""") for (model, name) in cr.fetchall(): @@ -360,53 +365,46 @@ def load_modules(db, force_demo=False, status=None, update_module=False): _logger.warning("Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)", model) # Cleanup orphan records - pool.get('ir.model.data')._process_end(cr, 1, processed_modules) + pool.get('ir.model.data')._process_end(cr, SUPERUSER_ID, processed_modules) for kind in ('init', 'demo', 'update'): tools.config[kind] = {} cr.commit() -# if update_module: + + # STEP 5: Cleanup menus + # Remove menu items that are not referenced by any of other + # (child) menu item, ir_values, or ir_model_data. + # TODO: This code could be a method of ir_ui_menu. Remove menu without actions of children + if update_module: + while True: + cr.execute('''delete from + ir_ui_menu + where + (id not IN (select parent_id from ir_ui_menu where parent_id is not null)) + and + (id not IN (select res_id from ir_values where model='ir.ui.menu')) + and + (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''') + cr.commit() + if not cr.rowcount: + break + else: + _logger.info('removed %d unused menus', cr.rowcount) + + # STEP 6: Uninstall modules to remove + if update_module: # Remove records referenced from ir_model_data for modules to be # removed (and removed the references from ir_model_data). - #cr.execute("select id,name from ir_module_module where state=%s", ('to remove',)) - #remove_modules = map(lambda x: x['name'], cr.dictfetchall()) - # Cleanup orphan records - #pool.get('ir.model.data')._process_end(cr, 1, remove_modules, noupdate=None) -# for mod_id, mod_name in cr.fetchall(): -# cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,)) -# for rmod, rid in cr.fetchall(): -# uid = 1 -# rmod_module= pool.get(rmod) -# if rmod_module: -# rmod_module.unlink(cr, uid, [rid]) -# else: -# _logger.error('Could not locate %s to remove res=%d' % (rmod,rid)) -# cr.execute('delete from ir_model_data where module=%s', (mod_name,)) -# cr.commit() - - # Remove menu items that are not referenced by any of other - # (child) menu item, ir_values, or ir_model_data. - # This code could be a method of ir_ui_menu. - # TODO: remove menu without actions of children -# while True: -# cr.execute('''delete from -# ir_ui_menu -# where -# (id not IN (select parent_id from ir_ui_menu where parent_id is not null)) -# and -# (id not IN (select res_id from ir_values where model='ir.ui.menu')) -# and -# (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''') -# cr.commit() -# if not cr.rowcount: -# break -# else: -# _logger.info('removed %d unused menus', cr.rowcount) - - # Pretend that modules to be removed are actually uninstalled. - #cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',)) - #cr.commit() + cr.execute("SELECT id FROM ir_module_module WHERE state=%s", ('to remove',)) + mod_ids_to_remove = [x[0] for x in cr.fetchall()] + if mod_ids_to_remove: + pool.get('ir.module.module').module_uninstall(cr, SUPERUSER_ID, mod_ids_to_remove) + # Recursive reload, should only happen once, because there should be no + # modules to remove next time + cr.commit() + _logger.info('Reloading registry once more after uninstalling modules') + return pooler.restart_pool(cr.dbname, force_demo, status, update_module) if report.failures: _logger.error('At least one test failed when loading the modules.')