diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql index eb9966bddcd..a227007353c 100644 --- a/openerp/addons/base/base.sql +++ b/openerp/addons/base/base.sql @@ -347,6 +347,25 @@ CREATE TABLE ir_model_data ( res_id integer, primary key(id) ); +-- Records foreign keys and constraints +-- installed by a module (so they can be removed them when the module is +-- uninstalled).: +-- - for a foreign key: type 'f', +-- - for a constraint: type 'u'. +CREATE TABLE ir_model_constraint ( + id serial NOT NULL, + create_uid integer, + create_date timestamp without time zone, + write_date timestamp without time zone, + write_uid integer, + date_init timestamp without time zone, + date_update timestamp without time zone, + module integer NOT NULL references ir_module_module on delete restrict, + model integer NOT NULL references ir_model on delete restrict, + type character varying(1) NOT NULL, + name character varying(128) NOT NULL +); + --------------------------------- -- Users --------------------------------- diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py index a2a4b999ed8..f4605d1615f 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -20,6 +20,7 @@ ############################################################################## import ir_model +import ir_model_constraint import ir_sequence import ir_needaction import ir_ui_menu diff --git a/openerp/addons/base/ir/ir_model_constraint.py b/openerp/addons/base/ir/ir_model_constraint.py new file mode 100644 index 00000000000..790b598763a --- /dev/null +++ b/openerp/addons/base/ir/ir_model_constraint.py @@ -0,0 +1,71 @@ + +import openerp +from openerp.osv import fields +from openerp.osv.orm import Model + +class ir_model_constraint(Model): + """ + This model tracks PostgreSQL foreign keys and constraints used by OpenERP + models. + """ + _name = 'ir.model.constraint' + _columns = { + 'name': fields.char('Constraint', required=True, size=128, select=1, + help="PostgreSQL constraint or foreign key name."), + 'model': fields.many2one('ir.model', string='Model Name', + required=True, select=1), + 'module': fields.many2one('ir.module.module', string='Module Name', + required=True, select=1), + 'type': fields.char('Constraint Type', required=True, size=1, select=1, + help="Type of the constraint: `f` for a foreign key, " + "`u` for other constraints."), + 'date_update': fields.datetime('Update Date'), + 'date_init': fields.datetime('Initialization Date') + } + + _sql_constraints = [ + ('module_name_uniq', 'unique(name, module)', + 'Constraints with the same name are unique per module.'), + ] + + def _module_data_uninstall(self, cr, uid, ids, context=None): + """ + Delete PostgreSQL foreign keys and constraints tracked by this model. + """ + + if uid != 1 and not self.pool.get('ir.model.access').check_groups(cr, uid, "base.group_system"): + raise except_orm(_('Permission Denied'), (_('Administrator access is required to uninstall a module'))) + + context = dict(context or {}) + + ids_set = set(ids) + ids.sort() + ids.reverse() + for data in self.browse(cr, uid, ids, context): + model = data.model + model_obj = self.pool.get(model) + name = tools.ustr(data.name) + typ = data.type + + # double-check we are really going to delete all the owners of this schema element + cr.execute("""SELECT id from ir_model_constraint where name=%s""", (data.name,)) + external_ids = [x[0] for x in cr.fetchall()] + if (set(external_ids)-ids_set): + # as installed modules have defined this element we must not delete it! + pass + + elif typ == 'f': + # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it) + cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) + WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table)) + if cr.fetchone(): + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) + _logger.info('Dropped FK CONSTRAINT %s@%s', name, model) + + elif typ == 'u': + # test if constraint exists + cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) + WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table)) + if cr.fetchone(): + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) + _logger.info('Dropped CONSTRAINT %s@%s', name, model) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index e99296a15e1..be55c1d305d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2744,6 +2744,23 @@ class BaseModel(object): _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", self._table, column['attname']) + def _save_constraint(self, cr, constraint_name, type): + assert type in ('f', 'u') + cr.execute(""" + SELECT 1 FROM ir_model_constraint, ir_module_module + WHERE ir_model_constraint.module=ir_module_module.id + AND ir_model_constraint.name=%s + AND ir_module_module.name=%s + """, (constraint_name, self._module)) + if not cr.rowcount: + cr.execute(""" + INSERT INTO ir_model_constraint + (name, date_init, date_update, module, model, type) + VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', + (SELECT id FROM ir_module_module WHERE name=%s), + (SELECT id FROM ir_model WHERE model=%s), %s)""", + (constraint_name, self._module, self._name, type)) + # quick creation of ir.model.data entry to make uninstall of schema elements easier def _make_ext_id(self, cr, ext_id): cr.execute('SELECT 1 FROM ir_model_data WHERE name=%s AND module=%s', (ext_id, self._module)) @@ -3092,6 +3109,7 @@ class BaseModel(object): for t, k, r, d in self._foreign_keys: cr.execute('ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s' % (t, k, r, d)) self._make_ext_id(cr, "%s%s_%s_fkey" % (EXT_ID_PREFIX_FK, t, k)) + self._save_constraint(cr, "%s_%s_fkey" % (t, k), 'f') cr.commit() del self._foreign_keys diff --git a/openerp/tests/addons/test_uninstall/__init__.py b/openerp/tests/addons/test_uninstall/__init__.py new file mode 100644 index 00000000000..fe4487156b1 --- /dev/null +++ b/openerp/tests/addons/test_uninstall/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +import models +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_uninstall/__openerp__.py b/openerp/tests/addons/test_uninstall/__openerp__.py new file mode 100644 index 00000000000..d8776897df0 --- /dev/null +++ b/openerp/tests/addons/test_uninstall/__openerp__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'test-uninstall', + 'version': '0.1', + 'category': 'Tests', + 'description': """A module to test the uninstall feature.""", + 'author': 'OpenERP SA', + 'maintainer': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'depends': ['base'], + 'data': [], + 'installable': True, + 'auto_install': False, +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_uninstall/models.py b/openerp/tests/addons/test_uninstall/models.py new file mode 100644 index 00000000000..645853cc2f8 --- /dev/null +++ b/openerp/tests/addons/test_uninstall/models.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +import openerp +from openerp.osv.orm import Model + +class test_uninstall_model(Model): + _name = 'test_uninstall.model' + + _columns = { + } + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/t.py b/t.py new file mode 100644 index 00000000000..f5497943a59 --- /dev/null +++ b/t.py @@ -0,0 +1,66 @@ +import openerp +from openerp.osv import fields +from openerp.osv.orm import Model +conf = openerp.tools.config + +if __name__ == '__main__': + openerp.netsvc.init_logger() + conf['addons_path'] = './openerp/tests/addons' + conf['init'] = {'test_uninstall': 1} + registry = openerp.modules.registry.RegistryManager.new('xx', update_module=True) + ir_model_constraint = registry.get('ir.model.constraint') + print ir_model_constraint + cr = registry.db.cursor() + ids = ir_model_constraint.search(cr, openerp.SUPERUSER_ID, [], {}) + print ir_model_constraint.browse(cr, openerp.SUPERUSER_ID, ids, {}) + cr.close() + +##################################################################### + +# Nice idea, but won't work without some more change to the framework (which +# expects everything on disk, maybe we can craft a zip file...). + +MY_MODULE = { + 'author': 'Jean Beauvoir', + 'website': 'http://www.youtube.com/watch?v=FeO5DfdZi7Y', + 'name': 'FEEL THE HEAT', + 'description': "Cobra's theme", + 'web': False, + 'license': 'WTFPL', + 'application': False, + 'icon': False, + 'sequence': 100, + 'depends': ['base'], +} + +def create_virtual_module(db_name, module_name, info): + registry = openerp.modules.registry.RegistryManager.get(db_name) + cr = registry.db.cursor() + + cr.execute("""SELECT 1 FROM ir_module_module WHERE name=%s""", (module_name,)) + if cr.fetchone(): return + + category_id = openerp.modules.db.create_categories(cr, ['Tests']) + cr.execute('INSERT INTO ir_module_module \ + (author, website, name, shortdesc, description, \ + category_id, auto_install, state, certificate, web, license, application, icon, sequence) \ + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', ( + info['author'], + info['website'], module_name, info['name'], + info['description'], category_id, + True, 'to install', False, + info['web'], + info['license'], + info['application'], info['icon'], + info['sequence'])) + module_id = cr.fetchone()[0] + cr.execute('INSERT INTO ir_model_data \ + (name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', ( + 'module_' + module_name, 'ir.module.module', 'base', module_id, True)) + dependencies = info['depends'] + for d in dependencies: + cr.execute('INSERT INTO ir_module_module_dependency \ + (module_id,name) VALUES (%s, %s)', (module_id, d)) + + cr.commit() + cr.close()