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: