diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql index eb9966bddcd..f2cb77709ed 100644 --- a/openerp/addons/base/base.sql +++ b/openerp/addons/base/base.sql @@ -347,6 +347,39 @@ 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 when the module is uninstalled): +-- - for a foreign key: type is 'f', +-- - for a constraint: type is 'u' (this is the convention PostgreSQL uses). +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 +); + +-- Records relation tables (i.e. implementing many2many) installed by a module +-- (so they can be removed when the module is uninstalled). +CREATE TABLE ir_model_relation ( + 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, + 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..bc4f3eecfbf 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -20,6 +20,8 @@ ############################################################################## import ir_model +import ir_model_constraint +import ir_model_relation import ir_sequence import ir_needaction import ir_ui_menu diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 6d5da0fc47a..392bb60c53e 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -1127,6 +1127,65 @@ + + ir.model.constraint.form + ir.model.constraint + form + +
+ + + + + + + + + +
+ + + ir.model.constraint.list + ir.model.constraint + tree + + + + + + + + + + + + ir.model.relation.form + ir.model.relation + form + +
+ + + + + + + + +
+ + + ir.model.relation.list + ir.model.relation + tree + + + + + + + + Models @@ -1156,6 +1215,24 @@ + + Model Constraints + ir.model.constraint + form + + + + + + ManyToMany Relations + ir.model.relation + form + + + + diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index b366d1dfb1c..beed30a2178 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -29,8 +29,7 @@ from openerp import netsvc, pooler, tools from openerp.tools.safe_eval import safe_eval as eval from openerp.tools import config from openerp.tools.translate import _ -from openerp.osv.orm import except_orm, browse_record, EXT_ID_PREFIX_FK, \ - EXT_ID_PREFIX_M2M_TABLE, EXT_ID_PREFIX_CONSTRAINT +from openerp.osv.orm import except_orm, browse_record _logger = logging.getLogger(__name__) @@ -864,7 +863,7 @@ class ir_model_data(osv.osv): cr.execute('UPDATE ir_values set value=%s WHERE model=%s and key=%s and name=%s'+where,(value, model, key, name)) return True - def _module_data_uninstall(self, cr, uid, ids, context=None): + def _module_data_uninstall(self, cr, uid, modules_to_remove, context=None): """Deletes all the records referenced by the ir.model.data entries ``ids`` along with their corresponding database backed (including dropping tables, columns, FKs, etc, as long as there is no other @@ -875,6 +874,8 @@ class ir_model_data(osv.osv): This step is performed as part of the full uninstallation of a module. """ + ids = self.search(cr, uid, [('module', 'in', modules_to_remove)]) + 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'))) @@ -884,7 +885,6 @@ class ir_model_data(osv.osv): ids_set = set(ids) wkf_todo = [] to_unlink = [] - to_drop_table = [] ids.sort() ids.reverse() for data in self.browse(cr, uid, ids, context): @@ -893,42 +893,6 @@ class ir_model_data(osv.osv): model_obj = self.pool.get(model) name = tools.ustr(data.name) - if name.startswith(EXT_ID_PREFIX_FK) or name.startswith(EXT_ID_PREFIX_M2M_TABLE)\ - or name.startswith(EXT_ID_PREFIX_CONSTRAINT): - # double-check we are really going to delete all the owners of this schema element - cr.execute("""SELECT id from ir_model_data where name = %s and res_id IS NULL""", (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! - continue - - if name.startswith(EXT_ID_PREFIX_FK): - name = name[len(EXT_ID_PREFIX_FK):] - # 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) - continue - - if name.startswith(EXT_ID_PREFIX_M2M_TABLE): - name = name[len(EXT_ID_PREFIX_M2M_TABLE):] - cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,)) - if cr.fetchone() and not name in to_drop_table: - to_drop_table.append(name) - continue - - if name.startswith(EXT_ID_PREFIX_CONSTRAINT): - name = name[len(EXT_ID_PREFIX_CONSTRAINT):] - # 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) - continue - pair_to_unlink = (model, res_id) if pair_to_unlink not in to_unlink: to_unlink.append(pair_to_unlink) @@ -948,17 +912,12 @@ class ir_model_data(osv.osv): except: _logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model) - # drop m2m relation tables - for table in to_drop_table: - cr.execute('DROP TABLE %s CASCADE'% (table),) - _logger.info('Dropped table %s', table) - def unlink_if_refcount(to_unlink): for model, res_id in to_unlink: 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 must not delete it - return + continue _logger.info('Deleting %s@%s', res_id, model) try: self.pool.get(model).unlink(cr, uid, [res_id], context=context) @@ -970,11 +929,18 @@ class ir_model_data(osv.osv): if model not in ('ir.model','ir.model.fields')) unlink_if_refcount((model, res_id) for model, res_id in to_unlink if model == 'ir.model.fields') + + ir_model_relation = self.pool.get('ir.model.relation') + relation_ids = ir_model_relation.search(cr, uid, [('module', 'in', modules_to_remove)]) + ir_model_relation._module_data_uninstall(cr, uid, relation_ids, context) + unlink_if_refcount((model, res_id) for model, res_id in to_unlink if model == 'ir.model') cr.commit() + self.unlink(cr, uid, ids, context) + def _process_end(self, cr, uid, modules): """ Clear records removed from updated module data. This method is called at the end of the module loading process. 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..4a69bc2f0a7 --- /dev/null +++ b/openerp/addons/base/ir/ir_model_constraint.py @@ -0,0 +1,79 @@ +import logging + +import openerp +from openerp import SUPERUSER_ID +from openerp.osv import fields +from openerp.osv.orm import Model + +_logger = logging.getLogger(__name__) + +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', + required=True, select=1), + 'module': fields.many2one('ir.module.module', string='Module', + 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 != SUPERUSER_ID 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() + to_unlink = [] + for data in self.browse(cr, uid, ids, context): + model = data.model.model + model_obj = self.pool.get(model) + name = openerp.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! + continue + + if 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) + + if 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) + + to_unlink.append(data.id) + self.unlink(cr, uid, to_unlink, context) diff --git a/openerp/addons/base/ir/ir_model_relation.py b/openerp/addons/base/ir/ir_model_relation.py new file mode 100644 index 00000000000..eede6eae44f --- /dev/null +++ b/openerp/addons/base/ir/ir_model_relation.py @@ -0,0 +1,65 @@ +import logging + +import openerp +from openerp import SUPERUSER_ID +from openerp.osv import fields +from openerp.osv.orm import Model + +_logger = logging.getLogger(__name__) + +class ir_model_relation(Model): + """ + This model tracks PostgreSQL tables used to implement OpenERP many2many + relations. + """ + _name = 'ir.model.relation' + _columns = { + 'name': fields.char('Relation Name', required=True, size=128, select=1, + help="PostgreSQL table name implementing a many2many relation."), + 'model': fields.many2one('ir.model', string='Model', + required=True, select=1), + 'module': fields.many2one('ir.module.module', string='Module', + required=True, select=1), + 'date_update': fields.datetime('Update Date'), + 'date_init': fields.datetime('Initialization Date') + } + + def _module_data_uninstall(self, cr, uid, ids, context=None): + """ + Delete PostgreSQL many2many relations tracked by this model. + """ + + if uid != SUPERUSER_ID 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'))) + + ids_set = set(ids) + to_drop_table = [] + ids.sort() + ids.reverse() + to_unlink = [] + for data in self.browse(cr, uid, ids, context): + model = data.model + model_obj = self.pool.get(model) + name = openerp.tools.ustr(data.name) + + # double-check we are really going to delete all the owners of this schema element + cr.execute("""SELECT id from ir_model_relation 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! + continue + + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name=%s", (name,)) + if cr.fetchone() and not name in to_drop_table: + to_drop_table.append(name) + + to_unlink.append(data.id) + + self.unlink(cr, uid, to_unlink, context) + + # drop m2m relation tables + for table in to_drop_table: + cr.execute('DROP TABLE %s CASCADE'% (table),) + _logger.info('Dropped table %s', table) + + cr.commit() diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index d75f330afa2..392850f7b77 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -378,10 +378,11 @@ class module(osv.osv): including the deletion of all database structures created by the module: tables, columns, constraints, etc.""" ir_model_data = self.pool.get('ir.model.data') + ir_model_constraint = self.pool.get('ir.model.constraint') modules_to_remove = [m.name for m in self.browse(cr, uid, ids, context)] - data_ids = ir_model_data.search(cr, uid, [('module', 'in', modules_to_remove)]) - ir_model_data._module_data_uninstall(cr, uid, data_ids, context) - ir_model_data.unlink(cr, uid, data_ids, context) + constraint_ids = ir_model_constraint.search(cr, uid, [('module', 'in', modules_to_remove)]) + ir_model_constraint._module_data_uninstall(cr, uid, constraint_ids, context) + ir_model_data._module_data_uninstall(cr, uid, modules_to_remove, context) self.write(cr, uid, ids, {'state': 'uninstalled'}) return True diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 1f4e11fca26..2cd4e83bdc7 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -89,7 +89,7 @@ class res_partner_category(osv.osv): 'active' : fields.boolean('Active', help="The active field allows you to hide the category without removing it."), 'parent_left' : fields.integer('Left parent', select=True), 'parent_right' : fields.integer('Right parent', select=True), - 'partner_ids': fields.many2many('res.partner', 'res_partner_category_rel', 'category_id', 'partner_id', 'Partners'), + 'partner_ids': fields.many2many('res.partner', id1='category_id', id2='partner_id', string='Partners'), } _constraints = [ (osv.osv._check_recursion, 'Error ! You can not create recursive categories.', ['parent_id']) @@ -144,7 +144,7 @@ class res_partner(osv.osv): 'website': fields.char('Website',size=64, help="Website of Partner or Company"), 'comment': fields.text('Notes'), 'address': fields.one2many('res.partner.address', 'partner_id', 'Contacts'), # should be removed in version 7, but kept until then for backward compatibility - 'category_id': fields.many2many('res.partner.category', 'res_partner_category_rel', 'partner_id', 'category_id', 'Tags'), + 'category_id': fields.many2many('res.partner.category', id1='partner_id', id2='category_id', string='Tags'), 'credit_limit': fields.float(string='Credit Limit'), 'ean13': fields.char('EAN13', size=13), 'active': fields.boolean('Active'), diff --git a/openerp/addons/base/tests/test_ir_values.py b/openerp/addons/base/tests/test_ir_values.py index b7ea0107776..b306e1f0929 100644 --- a/openerp/addons/base/tests/test_ir_values.py +++ b/openerp/addons/base/tests/test_ir_values.py @@ -54,7 +54,10 @@ class test_ir_values(common.TransactionCase): ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True) ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,11', isobject=True) ir_values.set(self.cr, self.uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,12', isobject=True) - ir_values.set(self.cr, self.uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,45', isobject=True) + report_ids = self.registry('ir.actions.report.xml').search(self.cr, self.uid, [], {}) + reports = self.registry('ir.actions.report.xml').browse(self.cr, self.uid, report_ids, {}) + report_id = [report.id for report in reports if not report.groups_id][0] # assume at least one + ir_values.set(self.cr, self.uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,%s'%report_id, isobject=True) ir_values.set(self.cr, self.uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,14', isobject=True) # Replace one action binding to set a new name. @@ -86,7 +89,7 @@ class test_ir_values(common.TransactionCase): assert len(actions) == 1, "Mismatching number of bound actions" assert len(actions[0]) == 3, "Malformed action definition" assert actions[0][1] == 'Nice Report', 'Bound action does not match definition' - assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 45, 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == report_id, 'Bound action does not match definition' actions = ir_values.get(self.cr, self.uid, 'action', 'client_action_relate', ['unexisting_model']) assert len(actions) == 1, "Mismatching number of bound actions" @@ -96,4 +99,4 @@ class test_ir_values(common.TransactionCase): if __name__ == '__main__': - unittest2.main() \ No newline at end of file + unittest2.main() diff --git a/openerp/modules/module.py b/openerp/modules/module.py index e226b69a6fc..265a23190e5 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -493,7 +493,7 @@ def get_test_modules(module, submodule, explode): # It seems the module has no `tests` sub-module, no problem. pass else: - print 'Can not `import %s`.' % module + _logger.exception('Can not `import %s`.', module) return [] # Discover available test sub-modules. @@ -554,6 +554,7 @@ def run_unit_tests(module_name): """ import unittest2 ms = get_test_modules(module_name, '__fast_suite__', explode=False) + # TODO: No need to try again if the above call failed because of e.g. a syntax error. ms.extend(get_test_modules(module_name, '__sanity_checks__', explode=False)) suite = unittest2.TestSuite() for m in ms: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index af66ca3b137..4284f77d1d7 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -70,11 +70,6 @@ _schema = logging.getLogger(__name__ + '.schema') # List of etree._Element subclasses that we choose to ignore when parsing XML. from openerp.tools import SKIPPED_ELEMENT_TYPES -# Prefixes for external IDs of schema elements -EXT_ID_PREFIX_FK = "_foreign_key_" -EXT_ID_PREFIX_M2M_TABLE = "_m2m_rel_table_" -EXT_ID_PREFIX_CONSTRAINT = "_constraint_" - regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I) regex_object_name = re.compile(r'^[a-z0-9_.]+$') @@ -2741,13 +2736,45 @@ class BaseModel(object): _schema.debug("Table '%s': column '%s': dropped NOT NULL constraint", self._table, column['attname']) - # 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)) + def _save_constraint(self, cr, constraint_name, type): + """ + Record the creation of a constraint for this model, to make it possible + to delete it later when the module is uninstalled. Type can be either + 'f' or 'u' depending on the constraing being a foreign key or not. + """ + 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_data (name,date_init,date_update,module,model) - VALUES (%s, now() AT TIME ZONE 'UTC', now() AT TIME ZONE 'UTC', %s, %s)""", - (ext_id, self._module, self._name)) + 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)) + + def _save_relation_table(self, cr, relation_table): + """ + Record the creation of a many2many for this model, to make it possible + to delete it later when the module is uninstalled. + """ + cr.execute(""" + SELECT 1 FROM ir_model_relation, ir_module_module + WHERE ir_model_relation.module=ir_module_module.id + AND ir_model_relation.name=%s + AND ir_module_module.name=%s + """, (relation_table, self._module)) + if not cr.rowcount: + cr.execute("""INSERT INTO ir_model_relation (name, date_init, date_update, module, model) + 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))""", + (relation_table, self._module, self._name)) # checked version: for direct m2o starting from `self` def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete): @@ -3088,7 +3115,7 @@ class BaseModel(object): """ Create the foreign keys recorded by _auto_init. """ 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 @@ -3177,7 +3204,7 @@ class BaseModel(object): def _m2m_raise_or_create_relation(self, cr, f): m2m_tbl, col1, col2 = f._sql_names(self) - self._make_ext_id(cr, EXT_ID_PREFIX_M2M_TABLE + m2m_tbl) + self._save_relation_table(cr, m2m_tbl) cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,)) if not cr.dictfetchall(): if not self.pool.get(f._obj): @@ -3213,7 +3240,7 @@ class BaseModel(object): for (key, con, _) in self._sql_constraints: conname = '%s_%s' % (self._table, key) - self._make_ext_id(cr, EXT_ID_PREFIX_CONSTRAINT + conname) + self._save_constraint(cr, conname, 'u') cr.execute("SELECT conname, pg_catalog.pg_get_constraintdef(oid, true) as condef FROM pg_constraint where conname=%s", (conname,)) existing_constraints = cr.dictfetchall() sql_actions = { diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index 5fccb07a082..913c5c759c9 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -11,6 +11,7 @@ See the :ref:`test-framework` section in the :ref:`features` list. import test_expression import test_ir_sequence import test_orm +import test_uninstall fast_suite = [ test_ir_sequence, 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..96a5800917d --- /dev/null +++ b/openerp/tests/addons/test_uninstall/models.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +import openerp +from openerp.osv import fields +from openerp.osv.orm import Model + +class test_uninstall_model(Model): + """ + This model uses different types of columns to make it possible to test + the uninstall feature of OpenERP. + """ + _name = 'test_uninstall.model' + + _columns = { + 'name': fields.char('Name', size=64), + 'ref': fields.many2one('res.users', string='User'), + 'rel': fields.many2many('res.users', string='Users'), + } + + _sql_constraints = [ + ('name_uniq', 'unique (name)', 'Each name must be unique.') + ] + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/test_uninstall.py b/openerp/tests/test_uninstall.py new file mode 100644 index 00000000000..1f74b660ce0 --- /dev/null +++ b/openerp/tests/test_uninstall.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# This assumes an existing but uninitialized database. +import psycopg2 +import unittest2 + +import openerp +from openerp import SUPERUSER_ID +import common + +DB = common.DB +ADMIN_USER_ID = common.ADMIN_USER_ID + +def registry(model): + return openerp.modules.registry.RegistryManager.get(DB)[model] + +def cursor(): + return openerp.modules.registry.RegistryManager.get(DB).db.cursor() + +def get_module(module_name): + registry = openerp.modules.registry.RegistryManager.get(DB) + return registry.get(module_name) + +def reload_registry(): + openerp.modules.registry.RegistryManager.new( + DB, update_module=True) + +def search_registry(model_name, domain): + cr = cursor() + model = registry(model_name) + record_ids = model.search(cr, SUPERUSER_ID, domain, {}) + cr.close() + return record_ids + +def install_module(module_name): + ir_module_module = registry('ir.module.module') + cr = cursor() + module_ids = ir_module_module.search(cr, SUPERUSER_ID, + [('name', '=', module_name)], {}) + assert len(module_ids) == 1 + ir_module_module.button_install(cr, SUPERUSER_ID, module_ids, {}) + cr.commit() + cr.close() + reload_registry() + +def uninstall_module(module_name): + ir_module_module = registry('ir.module.module') + cr = cursor() + module_ids = ir_module_module.search(cr, SUPERUSER_ID, + [('name', '=', module_name)], {}) + assert len(module_ids) == 1 + ir_module_module.button_uninstall(cr, SUPERUSER_ID, module_ids, {}) + cr.commit() + cr.close() + reload_registry() + +class test_uninstall(unittest2.TestCase): + """ + Test the install/uninstall of a test module. The module is available in + `openerp.tests` which should be present in the addons-path. + """ + + def test_01_install(self): + """ Check a few things showing the module is installed. """ + install_module('test_uninstall') + assert get_module('test_uninstall.model') + + assert search_registry('ir.model.data', + [('module', '=', 'test_uninstall')]) + + assert search_registry('ir.model.fields', + [('model', '=', 'test_uninstall.model')]) + + def test_02_uninstall(self): + """ Check a few things showing the module is uninstalled. """ + uninstall_module('test_uninstall') + assert not get_module('test_uninstall.model') + + assert not search_registry('ir.model.data', + [('module', '=', 'test_uninstall')]) + + assert not search_registry('ir.model.fields', + [('model', '=', 'test_uninstall.model')]) + + + +if __name__ == '__main__': + unittest2.main() + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: