summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael Collet <rco@openerp.com>2016-08-23 17:16:26 +0200
committerRaphael Collet <rco@openerp.com>2016-08-31 17:21:51 +0200
commitf8c845bbdb66a85198b620d56806ededceb0e906 (patch)
tree64c60d20aaf2e646b0b8aec7a3e0942e53ed5b7e
parent31f829c39419623a9d39845a70758552d5180e77 (diff)
[REF] models: delegate o2m and m2m schema update to the field
-rw-r--r--openerp/fields.py67
-rw-r--r--openerp/models.py53
-rw-r--r--openerp/osv/expression.py2
3 files changed, 64 insertions, 58 deletions
diff --git a/openerp/fields.py b/openerp/fields.py
index cf90a8a01d4..ce58e4ea0c0 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -20,13 +20,14 @@ from openerp.tools import float_precision, float_repr, float_round, frozendict,
html_sanitize, human_size, pg_varchar, ustr, OrderedSet
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
-from openerp.tools.translate import html_translate
+from openerp.tools.translate import html_translate, _
DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
EMPTY_DICT = frozendict()
_logger = logging.getLogger(__name__)
+_schema = logging.getLogger(__name__[:-7] + '.schema')
Default = object() # default value for __init__() methods
@@ -866,6 +867,12 @@ class Field(object):
# Read from/write to database
#
+ def check_schema(self, model):
+ """ Make sure the database contains everything for this field. Return
+ ``True`` if the schema was altered and the field must be recomputed.
+ """
+ pass
+
def read(self, records):
""" Read the value of ``self`` on ``records``, and store it in cache. """
return NotImplementedError("Method read() undefined on %s" % self)
@@ -2172,6 +2179,12 @@ class One2many(_RelationalMulti):
fnames = [name for name in fnames if name != self.inverse_name]
return super(One2many, self).convert_to_onchange(value, record, fnames)
+ def check_schema(self, model):
+ if self.comodel_name in model.env:
+ comodel = model.env[self.comodel_name]
+ if self.inverse_name not in comodel._fields:
+ raise UserError(_("No inverse field %r found for %r") % (self.inverse_name, self.comodel_name))
+
def read(self, records):
# retrieve the lines in the comodel
comodel = records.env[self.comodel_name].with_context(**self.context)
@@ -2285,12 +2298,21 @@ class Many2many(_RelationalMulti):
def _setup_regular_base(self, model):
super(Many2many, self)._setup_regular_base(model)
- if not self.relation and self.store:
- # retrieve self.relation from the corresponding column
- column = self.to_column()
- if isinstance(column, fields.many2many):
- self.relation, self.column1, self.column2 = column._sql_names(model)
- elif self.store:
+ if self.store:
+ if not (self.relation and self.column1 and self.column2):
+ # table name is based on the stable alphabetical order of tables
+ comodel = model.env[self.comodel_name]
+ if not self.relation:
+ tables = sorted([model._table, comodel._table])
+ assert tables[0] != tables[1], \
+ "%s: Implicit/canonical naming of many2many relationship " \
+ "table is not possible when source and destination models " \
+ "are the same" % self
+ self.relation = '%s_%s_rel' % tuple(tables)
+ if not self.column1:
+ self.column1 = '%s_id' % model._table
+ if not self.column2:
+ self.column2 = '%s_id' % comodel._table
# check validity of table name
check_pg_name(self.relation)
@@ -2314,6 +2336,35 @@ class Many2many(_RelationalMulti):
_column_auto_join = property(attrgetter('auto_join'))
_column_limit = property(attrgetter('limit'))
+ def check_schema(self, model):
+ cr = model._cr
+ rel, id1, id2 = self.relation, self.column1, self.column2
+ # do not create relations for custom fields as they do not belong to a module
+ # they will be automatically removed when dropping the corresponding ir.model.field
+ # table name for custom relation all starts with x_, see __init__
+ if not rel.startswith('x_'):
+ model._save_relation_table(rel, self._module)
+ cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (rel,))
+ if not cr.dictfetchall():
+ if self.comodel_name not in model.env:
+ raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,))
+ comodel = model.env[self.comodel_name]
+ cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2))
+ # create foreign key references with ondelete=cascade, unless the targets are SQL views
+ cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (comodel._table,))
+ if not cr.fetchall():
+ model._m2o_add_foreign_key_unchecked(rel, id2, comodel, 'cascade', self._module)
+ cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (model._table,))
+ if not cr.fetchall():
+ model._m2o_add_foreign_key_unchecked(rel, id1, model, 'cascade', self._module)
+
+ cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1))
+ cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id2))
+ cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (rel, model._table, comodel._table))
+ cr.commit()
+ _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", rel, model._table, comodel._table)
+ return True
+
def read(self, records):
comodel = records.env[self.comodel_name]
@@ -2434,6 +2485,6 @@ class Id(Field):
# imported here to avoid dependency cycle issues
from openerp import SUPERUSER_ID
-from .exceptions import AccessError, MissingError
+from .exceptions import AccessError, MissingError, UserError
from .models import check_pg_name, BaseModel, IdType
from .osv import fields
diff --git a/openerp/models.py b/openerp/models.py
index ad8aa8f1c00..284deff5c6a 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -2399,12 +2399,9 @@ class BaseModel(object):
# Don't update custom (also called manual) fields
continue
- if field.type == 'one2many':
- self._o2m_raise_on_missing_reference(field.column)
-
- elif field.type == 'many2many':
- res = self._m2m_raise_or_create_relation(field.column)
- if res and field.compute:
+ if not field.column_type:
+ # the field is not stored as a column
+ if field.check_schema(self) and field.compute:
stored_fields.append(field)
else:
@@ -2537,7 +2534,7 @@ class BaseModel(object):
if comodel._auto and comodel._table != 'ir_actions':
self._m2o_fix_foreign_key(self._table, name, comodel, field.ondelete)
- elif field.column_type:
+ else:
# the column doesn't exist in database, create it
cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, name, field.column_type[1]))
cr.execute("COMMENT ON COLUMN %s.\"%s\" IS %%s" % (self._table, name), (field.string,))
@@ -2674,48 +2671,6 @@ class BaseModel(object):
return {row['attname']: row for row in self._cr.dictfetchall()}
@api.model_cr
- def _o2m_raise_on_missing_reference(self, column):
- # TODO this check should be a method on fields.one2many.
- if column._obj in self.env:
- other = self.env[column._obj]
- # TODO the condition could use fields_get_keys().
- if column._fields_id not in other._fields:
- raise UserError(_("There is no reference field '%s' found for '%s'") % (column._fields_id, column._obj))
-
- @api.model_cr
- def _m2m_raise_or_create_relation(self, column):
- """ Create the table for the relation if necessary.
- Return ``True`` if the relation had to be created.
- """
- cr = self._cr
- m2m_tbl, col1, col2 = column._sql_names(self)
- # do not create relations for custom fields as they do not belong to a module
- # they will be automatically removed when dropping the corresponding ir.model.field
- # table name for custom relation all starts with x_, see __init__
- if not m2m_tbl.startswith('x_'):
- self._save_relation_table(m2m_tbl, column._module)
- cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (m2m_tbl,))
- if not cr.dictfetchall():
- if column._obj not in self.env:
- raise UserError(_('Many2Many destination model does not exist: `%s`') % (column._obj,))
- dest_model = self.env[column._obj]
- cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (m2m_tbl, col1, col2, col1, col2))
- # create foreign key references with ondelete=cascade, unless the targets are SQL views
- cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (dest_model._table,))
- if not cr.fetchall():
- self._m2o_add_foreign_key_unchecked(m2m_tbl, col2, dest_model, 'cascade', column._module)
- cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (self._table,))
- if not cr.fetchall():
- self._m2o_add_foreign_key_unchecked(m2m_tbl, col1, self, 'cascade', column._module)
-
- cr.execute('CREATE INDEX ON "%s" ("%s")' % (m2m_tbl, col1))
- cr.execute('CREATE INDEX ON "%s" ("%s")' % (m2m_tbl, col2))
- cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (m2m_tbl, self._table, dest_model._table))
- cr.commit()
- _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", m2m_tbl, self._table, dest_model._table)
- return True
-
- @api.model_cr
def _add_sql_constraints(self):
"""
diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py
index 985b1135d4b..14bd0ec76b2 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -966,7 +966,7 @@ class expression(object):
push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, column._fields_id, comodel._table)), model))
elif column._type == 'many2many':
- rel_table, rel_id1, rel_id2 = column._sql_names(model)
+ rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2
if operator in HIERARCHY_FUNCS:
def _rec_convert(ids):