[REF] models: improve code of _create/_write

Also remove method `self._check_selection_field_value`, and perform the check
in field method `convert_to_column`.
This commit is contained in:
Raphael Collet
2018-01-16 10:47:13 +01:00
parent be0f24a96b
commit 869f062e08
5 changed files with 128 additions and 136 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ class Serialized(fields.Field):
}
column_type = ('text', 'text')
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
return json.dumps(value)
def convert_to_cache(self, value, record, validate=True):
+1 -7
View File
@@ -186,7 +186,7 @@ class IrTranslation(models.Model):
name = fields.Char(string='Translated field', required=True)
res_id = fields.Integer(string='Record ID', index=True)
lang = fields.Selection(selection='_get_languages', string='Language')
lang = fields.Selection(selection='_get_languages', string='Language', validate=False)
type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility
source = fields.Text(string='Source term', compute='_compute_source',
@@ -266,12 +266,6 @@ class IrTranslation(models.Model):
['type', 'name', 'lang', 'res_id', 'md5(src)'])
return res
@api.model
def _check_selection_field_value(self, field, value):
if field == 'lang':
return
return super(IrTranslation, self)._check_selection_field_value(field, value)
@api.model
def _get_ids(self, name, tt, lang, ids):
""" Return the translations of records.
+19 -10
View File
@@ -787,7 +787,7 @@ class Field(MetaField('DummyField', (object,), {})):
""" Return the null value for this field in the record format. """
return False
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
""" Convert ``value`` from the ``write`` format to the SQL format. """
if value is None or value is False:
return None
@@ -1145,7 +1145,7 @@ class Boolean(Field):
type = 'boolean'
column_type = ('bool', 'bool')
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
return bool(value)
def convert_to_cache(self, value, record, validate=True):
@@ -1166,7 +1166,7 @@ class Integer(Field):
_description_group_operator = property(attrgetter('group_operator'))
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
return int(value or 0)
def convert_to_cache(self, value, record, validate=True):
@@ -1232,7 +1232,7 @@ class Float(Field):
_description_digits = property(attrgetter('digits'))
_description_group_operator = property(attrgetter('group_operator'))
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
result = float(value or 0.0)
digits = self.digits
if digits:
@@ -1286,11 +1286,11 @@ class Monetary(Field):
assert self.currency_field in model._fields, \
"Field %s with unknown currency_field %r" % (self, self.currency_field)
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
# retrieve currency from values or record
if values and self.currency_field in values:
field = record._fields[self.currency_field]
currency = field.convert_to_cache(values[self.currency_field], record)
currency = field.convert_to_cache(values[self.currency_field], record, validate)
currency = field.convert_to_record(currency, record)
else:
# Note: this is wrong if 'record' is several records with different
@@ -1419,7 +1419,7 @@ class Char(_String):
assert self.size is None or isinstance(self.size, int), \
"Char field %s with non-integer size %r" % (self, self.size)
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
if value is None or value is False:
return None
# we need to convert the string to a unicode object to be able
@@ -1484,7 +1484,7 @@ class Html(_String):
_description_strip_style = property(attrgetter('strip_style'))
_description_strip_classes = property(attrgetter('strip_classes'))
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
if value is None or value is False:
return None
if self.sanitize:
@@ -1671,7 +1671,7 @@ class Binary(Field):
_description_attachment = property(attrgetter('attachment'))
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
# Binary values may be byte strings (python 2.6 byte array), but
# the legacy OpenERP convention is to transfer and store binaries
# as base64-encoded strings. The base64 string may be provided as a
@@ -1755,6 +1755,7 @@ class Selection(Field):
type = 'selection'
_slots = {
'selection': None, # [(value, string), ...], function or method name
'validate': True, # whether validating upon write
}
def __init__(self, selection=Default, string=Default, **kwargs):
@@ -1820,6 +1821,11 @@ class Selection(Field):
selection = selection(env[self.model_name])
return [value for value, _ in selection]
def convert_to_column(self, value, record, values=None, validate=True):
if validate and self.validate:
value = self.convert_to_cache(value, record)
return super(Selection, self).convert_to_column(value, record, values, validate)
def convert_to_cache(self, value, record, validate=True):
if not validate:
return value or False
@@ -1846,6 +1852,9 @@ class Reference(Selection):
def column_type(self):
return ('varchar', pg_varchar())
def convert_to_column(self, value, record, values=None, validate=True):
return Field.convert_to_column(self, value, record, values, validate)
def convert_to_cache(self, value, record, validate=True):
# cache format: (res_model, res_id) or False
def process(res_model, res_id):
@@ -1987,7 +1996,7 @@ class Many2one(_Relational):
for record in records:
cache.set(record, self, self.convert_to_cache(value, record, validate=False))
def convert_to_column(self, value, record, values=None):
def convert_to_column(self, value, record, values=None, validate=True):
return value or None
def convert_to_cache(self, value, record, validate=True):
+104 -116
View File
@@ -41,6 +41,7 @@ import dateutil.relativedelta
import psycopg2
from lxml import etree
from lxml.builder import E
from psycopg2.extensions import AsIs
import odoo
from . import SUPERUSER_ID
@@ -2015,14 +2016,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self.invalidate_cache(['parent_left', 'parent_right'])
return True
@api.model
def _check_selection_field_value(self, field, value):
""" Check whether value is among the valid values for the given
selection/reference field, and raise an exception if not.
"""
field = self._fields[field]
field.convert_to_cache(value, self)
@api.model_cr
def _check_removed_columns(self, log=False):
# iterate on the database columns to drop the NOT NULL constraints of
@@ -3035,9 +3028,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# check Python constraints for inversed fields
self._validate_fields(set(inverse_vals) - set(store_vals))
# recompute fields
if self.env.recompute and self._context.get('recompute', True):
self.recompute()
# recompute fields
if self.env.recompute and self._context.get('recompute', True):
self.recompute()
return True
@@ -3050,11 +3043,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cr = self._cr
# for recomputing new-style fields
extra_fields = ['write_date', 'write_uid'] if self._log_access else []
self.modified(list(vals) + extra_fields)
# for updating parent_left, parent_right
# determine records that require updating parent_left, parent_right
parents_changed = []
if self._parent_store and (self._parent_name in vals) and \
not self._context.get('defer_parent_store_computation'):
@@ -3074,81 +3063,88 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cr.execute(query, (tuple(self.ids),))
parents_changed = [x[0] for x in cr.fetchall()]
updates = [] # list of (column, expr) or (column, pattern, value)
upd_todo = [] # list of column names to set explicitly
direct = [] # list of direcly updated columns
has_trans = self.env.lang and self.env.lang != 'en_US'
# determine SQL values
columns = [] # list of (column_name, format, value)
updated = [] # list of updated or translated columns
other_fields = [] # list of non-column fields
single_lang = len(self.env['res.lang'].get_installed()) <= 1
has_translation = self.env.lang and self.env.lang != 'en_US'
for name, val in vals.items():
field = self._fields[name]
assert field.store
if field.deprecated:
_logger.warning('Field %s.%s is deprecated: %s', self._name, name, field.deprecated)
if hasattr(field, 'selection') and val:
self._check_selection_field_value(name, val)
_logger.warning('Field %s is deprecated: %s', field, field.deprecated)
if field.column_type:
if single_lang or not (has_trans and field.translate is True):
if single_lang or not (has_translation and field.translate is True):
# val is not a translation: update the table
val = field.convert_to_column(val, self, vals)
updates.append((name, field.column_format, val))
direct.append(name)
columns.append((name, field.column_format, val))
updated.append(name)
else:
upd_todo.append(name)
other_fields.append(field)
if self._log_access:
updates.append(('write_uid', '%s', self._uid))
updates.append(('write_date', "(now() at time zone 'UTC')"))
direct.append('write_uid')
direct.append('write_date')
columns.append(('write_uid', '%s', self._uid))
columns.append(('write_date', '%s', AsIs("(now() at time zone 'UTC')")))
updated.append('write_uid')
updated.append('write_date')
if updates:
# mark fields to recompute (the ones that depend on old values)
self.modified(vals)
# update columns
if columns:
self.check_access_rule('write')
query = 'UPDATE "%s" SET %s WHERE id IN %%s' % (
self._table, ','.join('"%s"=%s' % (u[0], u[1]) for u in updates),
self._table, ','.join('"%s"=%s' % (column[0], column[1]) for column in columns),
)
params = tuple(u[2] for u in updates if len(u) > 2)
params = [column[2] for column in columns]
for sub_ids in cr.split_for_in_conditions(set(self.ids)):
cr.execute(query, params + (sub_ids,))
cr.execute(query, params + [sub_ids])
if cr.rowcount != len(sub_ids):
raise MissingError(_('One of the records you are trying to modify has already been deleted (Document type: %s).') % self._description)
# TODO: optimize
for name in direct:
for name in updated:
field = self._fields[name]
if callable(field.translate):
# The source value of a field has been modified,
# synchronize translated terms when possible.
self.env['ir.translation']._sync_terms_translations(self._fields[name], self)
self.env['ir.translation']._sync_terms_translations(field, self)
elif has_trans and field.translate:
elif has_translation and field.translate:
# The translated value of a field has been modified.
src_trans = self.read([name])[0][name]
if not src_trans:
# Insert value to DB
src_trans = vals[name]
self.with_context(lang=None).write({name: src_trans})
val = field.convert_to_column(vals[name], self, vals)
tname = "%s,%s" % (self._name, name)
val = field.convert_to_column(vals[name], self, vals)
self.env['ir.translation']._set_ids(
tname, 'model', self.env.lang, self.ids, val, src_trans)
# invalidate and mark new-style fields to recompute; do this before
# setting other fields, because it can require the value of computed
# fields, e.g., a one2many checking constraints on records
self.modified(direct)
# mark fields to recompute; do this before setting other fields, because
# the latter can require the value of computed fields, e.g., a one2many
# checking constraints on records
self.modified(updated)
# defaults in context must be removed when call a one2many or many2many
rel_context = {key: val
for key, val in self._context.items()
if not key.startswith('default_')}
# set the value of non-column fields
if other_fields:
# discard default values from context
other = self.with_context({
key: val
for key, val in self._context.items()
if not key.startswith('default_')
})
# call the 'write' method of fields which are not columns
for name in upd_todo:
field = self._fields[name]
field.write(self.with_context(rel_context), vals[name])
for field in sorted(other_fields, key=attrgetter('_sequence')):
field.write(other, vals[field.name])
# for recomputing new-style fields
self.modified(upd_todo)
# mark fields to recompute
self.modified(field.name for field in other_fields)
# check Python constraints
self._validate_fields(vals)
@@ -3211,10 +3207,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self.invalidate_cache(['parent_left', 'parent_right'])
# recompute new-style fields
if self.env.recompute and self._context.get('recompute', True):
self.recompute()
return True
@api.model
@@ -3283,7 +3275,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
parent_record.write(parent_vals)
# create record with stored fields
record = self.browse(self._create(store_vals))
record = self._create(store_vals)
with self.env.protecting(protected_fields, record):
# put the values of inverse fields in cache, and inverse them
@@ -3303,60 +3295,55 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
@api.model
def _create(self, vals):
# list of column assignments defined as tuples like:
# (column_name, format_string, column_value)
# (column_name, sql_formula)
# Those tuples will be used by the string formatting for the INSERT
# statement below.
updates = [
('id', "nextval('%s')" % self._sequence),
]
upd_todo = []
protected_fields = []
for name, val in list(vals.items()):
field = self._fields[name]
assert field.store
if field.inverse:
protected_fields.append(field)
self = self.browse()
# set boolean fields to False by default (to make search more powerful)
for name, field in self._fields.items():
if field.type == 'boolean' and field.store and name not in vals:
vals[name] = False
if field.type == 'boolean' and field.store:
vals.setdefault(name, False)
# determine SQL values
self = self.browse()
columns = [] # list of (column_name, format, value)
other_fields = [] # list of non-column fields
protected_fields = [] # list of fields to not recompute on self
translated_names = [] # list of translated field names
columns.append(('id', "nextval(%s)", self._sequence))
for name, val in vals.items():
field = self._fields[name]
if field.store and field.column_type:
column_val = field.convert_to_column(val, self, vals)
updates.append((name, field.column_format, column_val))
else:
upd_todo.append(name)
assert field.store
if hasattr(field, 'selection') and val:
self._check_selection_field_value(name, val)
if field.column_type:
column_val = field.convert_to_column(val, self, vals)
columns.append((name, field.column_format, column_val))
if field.translate is True:
translated_names.append(name)
else:
other_fields.append(field)
if field.inverse:
protected_fields.append(field)
if self._log_access:
updates.append(('create_uid', '%s', self._uid))
updates.append(('write_uid', '%s', self._uid))
updates.append(('create_date', "(now() at time zone 'UTC')"))
updates.append(('write_date', "(now() at time zone 'UTC')"))
columns.append(('create_uid', '%s', self._uid))
columns.append(('write_uid', '%s', self._uid))
columns.append(('create_date', '%s', AsIs("(now() at time zone 'UTC')")))
columns.append(('write_date', '%s', AsIs("(now() at time zone 'UTC')")))
# insert a row for this record
cr = self._cr
query = """INSERT INTO "%s" (%s) VALUES(%s) RETURNING id""" % (
self._table,
', '.join('"%s"' % u[0] for u in updates),
', '.join(u[1] for u in updates),
', '.join('"%s"' % column[0] for column in columns),
', '.join(column[1] for column in columns),
)
cr.execute(query, tuple(u[2] for u in updates if len(u) > 2))
cr.execute(query, [column[2] for column in columns])
# from now on, self is the new record
id_new, = cr.fetchone()
self = self.browse(id_new)
self = self.browse(cr.fetchone()[0])
# update parent_left, parent_right
if self._parent_store and not self._context.get('defer_parent_store_computation'):
if self.pool._init:
self.pool._init_parent[self._name] = True
@@ -3388,46 +3375,47 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cr.execute("UPDATE %s SET parent_right=parent_right+2 WHERE parent_right>=%%s" % self._table,
(pleft,))
cr.execute("UPDATE %s SET parent_left=%%s, parent_right=%%s WHERE id=%%s" % self._table,
(pleft, pleft + 1, id_new))
(pleft, pleft + 1, record_id))
self.invalidate_cache(['parent_left', 'parent_right'])
with self.env.protecting(protected_fields, self):
# invalidate and mark new-style fields to recompute; do this before
# setting other fields, because it can require the value of computed
# fields, e.g., a one2many checking constraints on records
# mark fields to recompute; do this before setting other fields,
# because the latter can require the value of computed fields, e.g.,
# a one2many checking constraints on records
self.modified(self._fields)
# defaults in context must be removed when call a one2many or many2many
rel_context = {key: val
for key, val in self._context.items()
if not key.startswith('default_')}
# set the value of non-column fields
if other_fields:
# discard default values from context
other = self.with_context({
key: val
for key, val in self._context.items()
if not key.startswith('default_')
})
# call the 'write' method of fields which are not columns
for name in sorted(upd_todo, key=lambda name: self._fields[name]._sequence):
field = self._fields[name]
field.write(self.with_context(rel_context), vals[name], create=True)
for field in sorted(other_fields, key=attrgetter('_sequence')):
field.write(other, vals[field.name], create=True)
# for recomputing new-style fields
self.modified(upd_todo)
# mark fields to recompute
self.modified(field.name for field in other_fields)
# check Python constraints
self._validate_fields(vals)
# recompute fields
if self.env.recompute and self._context.get('recompute', True):
# recompute new-style fields
self.recompute()
self.check_access_rule('create')
# add translations
if self.env.lang and self.env.lang != 'en_US':
# add translations for self.env.lang
for name, val in vals.items():
field = self._fields[name]
if field.store and field.column_type and field.translate is True:
tname = "%s,%s" % (self._name, name)
self.env['ir.translation']._set_ids(tname, 'model', self.env.lang, self.ids, val, val)
for name in translated_names:
tname = "%s,%s" % (self._name, name)
val = vals[name]
self.env['ir.translation']._set_ids(tname, 'model', self.env.lang, self.ids, val, val)
return id_new
return self
# TODO: ameliorer avec NULL
@api.model
+3 -2
View File
@@ -1201,7 +1201,7 @@ class expression(object):
else:
field = model._fields[left]
instr = ','.join([field.column_format] * len(params))
params = [field.convert_to_column(p, record=model) for p in params]
params = [field.convert_to_column(p, model, validate=False) for p in params]
query = '(%s."%s" %s (%s))' % (table_alias, left, operator, instr)
else:
# The case for (left, 'in', []) or (left, 'not in', []).
@@ -1257,7 +1257,8 @@ class expression(object):
query = '(%s OR %s."%s" IS NULL)' % (query, table_alias, left)
params = ['%%%s%%' % native_str]
else:
params = [model._fields[left].convert_to_column(right, model)]
field = model._fields[left]
params = [field.convert_to_column(right, model, validate=False)]
return query, params