[IMP] models, fields: change output format of method onchange for x2many fields
Use `field.convert_to_onchange` to format the output of method `onchange`. By
default it behaves like `convert_to_read`. This is useful for the many2one
case, as the client automatically receives the display name of the record.
For the *2many case, we return a list of dicts values, in recordset order, with
all the known fields in the view:
[{id: 42, name: "Existing record"}, {name: "New record"}, ...]
Each dictionary contains the values of all known fields in the view. The
client is responsible of determining which fields are dirty by computing the
difference with former values.
Since the method `onchange` now uses `field.convert_to_onchange`, the method
`field.convert_to_write` has been simplified, as it no longer needs its extra
parameters `target` and `fnames`.
This commit is contained in:
@@ -82,41 +82,10 @@ class TestOnChange(common.TransactionCase):
|
||||
field_onchange = self.Discussion._onchange_spec()
|
||||
self.assertEqual(field_onchange.get('name'), '1')
|
||||
self.assertEqual(field_onchange.get('messages'), '1')
|
||||
|
||||
# FIXME: commented out because currently not supported by the client
|
||||
# # modify messages
|
||||
# values = {
|
||||
# 'name': "Foo",
|
||||
# 'categories': [],
|
||||
# 'moderator': False,
|
||||
# 'participants': [],
|
||||
# 'messages': [
|
||||
# (0, 0, {
|
||||
# 'name': "[%s] %s" % ('', USER.name),
|
||||
# 'body': BODY,
|
||||
# 'author': USER.id,
|
||||
# 'size': len(BODY),
|
||||
# }),
|
||||
# (4, message.id),
|
||||
# ],
|
||||
# }
|
||||
# self.env.invalidate_all()
|
||||
# result = self.Discussion.onchange(values, 'messages', field_onchange)
|
||||
# self.assertIn('messages', result['value'])
|
||||
# self.assertItemsEqual(result['value']['messages'], [
|
||||
# (0, 0, {
|
||||
# 'name': "[%s] %s" % ("Foo", USER.name),
|
||||
# 'body': BODY,
|
||||
# 'author': USER.id,
|
||||
# 'size': len(BODY),
|
||||
# }),
|
||||
# (1, message.id, {
|
||||
# 'name': "[%s] %s" % ("Foo", USER.name),
|
||||
# 'body': BODY,
|
||||
# 'author': USER.id,
|
||||
# 'size': len(BODY),
|
||||
# }),
|
||||
# ])
|
||||
self.assertItemsEqual(
|
||||
[name for name in field_onchange if name.startswith('messages.')],
|
||||
['messages.author', 'messages.body', 'messages.name', 'messages.size'],
|
||||
)
|
||||
|
||||
# modify discussion name
|
||||
values = {
|
||||
@@ -125,28 +94,32 @@ class TestOnChange(common.TransactionCase):
|
||||
'moderator': False,
|
||||
'participants': [],
|
||||
'messages': [
|
||||
(4, message.id),
|
||||
(0, 0, {
|
||||
'name': "[%s] %s" % ('', USER.name),
|
||||
'body': BODY,
|
||||
'author': USER.id,
|
||||
'size': len(BODY),
|
||||
}),
|
||||
(4, message.id),
|
||||
],
|
||||
}
|
||||
self.env.invalidate_all()
|
||||
result = self.Discussion.onchange(values, 'name', field_onchange)
|
||||
self.assertIn('messages', result['value'])
|
||||
self.assertItemsEqual(result['value']['messages'], [
|
||||
(0, 0, {
|
||||
self.assertEqual(result['value']['messages'], [
|
||||
{
|
||||
'id': message.id,
|
||||
'name': "[%s] %s" % ("Foo", USER.name),
|
||||
'body': message.body,
|
||||
'author': message.author.name_get()[0],
|
||||
'size': message.size,
|
||||
},
|
||||
{
|
||||
'name': "[%s] %s" % ("Foo", USER.name),
|
||||
'body': BODY,
|
||||
'author': USER.id,
|
||||
'author': USER.name_get()[0],
|
||||
'size': len(BODY),
|
||||
}),
|
||||
(1, message.id, {
|
||||
'name': "[%s] %s" % ("Foo", USER.name),
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
def test_onchange_specific(self):
|
||||
@@ -156,25 +129,28 @@ class TestOnChange(common.TransactionCase):
|
||||
|
||||
field_onchange = self.Discussion._onchange_spec()
|
||||
self.assertEqual(field_onchange.get('moderator'), '1')
|
||||
self.assertFalse(any(name.startswith('participants.') for name in field_onchange))
|
||||
|
||||
# first remove demo user from participants
|
||||
discussion.participants -= demo
|
||||
self.assertNotIn(demo, discussion.participants)
|
||||
|
||||
# check that demo_user is added to participants when set as moderator
|
||||
participants = [(4, usr.id) for usr in discussion.participants]
|
||||
values = {
|
||||
'name': discussion.name,
|
||||
'moderator': demo.id,
|
||||
'categories': [(4, cat.id) for cat in discussion.categories],
|
||||
'messages': [(4, msg.id) for msg in discussion.messages],
|
||||
'participants': participants,
|
||||
'participants': [(4, usr.id) for usr in discussion.participants],
|
||||
}
|
||||
self.env.invalidate_all()
|
||||
result = discussion.onchange(values, 'moderator', field_onchange)
|
||||
|
||||
self.assertIn('participants', result['value'])
|
||||
self.assertItemsEqual(result['value']['participants'], participants + [(4, demo.id)])
|
||||
self.assertEqual(
|
||||
result['value']['participants'],
|
||||
[{'id': usr.id} for usr in discussion.participants + demo],
|
||||
)
|
||||
|
||||
def test_onchange_one2many_value(self):
|
||||
""" test the value of the one2many field inside the onchange """
|
||||
|
||||
+31
-46
@@ -761,21 +761,20 @@ class Field(object):
|
||||
"""
|
||||
return False if value is None else value
|
||||
|
||||
def convert_to_write(self, value, target=None, fnames=None):
|
||||
def convert_to_write(self, value):
|
||||
""" convert ``value`` from the cache to a valid value for method
|
||||
:meth:`BaseModel.write`.
|
||||
|
||||
:param target: optional, the record to be modified with this value
|
||||
:param fnames: for relational fields only, an optional collection of
|
||||
field names to convert
|
||||
"""
|
||||
return self.convert_to_read(value)
|
||||
|
||||
def convert_to_onchange(self, value):
|
||||
""" convert ``value`` from the cache to a valid value for an onchange
|
||||
method v7.
|
||||
def convert_to_onchange(self, value, fnames=None):
|
||||
""" convert ``value`` from the cache to a value as returned by method
|
||||
:meth:`BaseModel.onchange`.
|
||||
|
||||
:param fnames: an optional collection of field names to convert
|
||||
(for relational fields only)
|
||||
"""
|
||||
return self.convert_to_write(value)
|
||||
return self.convert_to_read(value)
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
""" convert ``value`` from the cache to a valid value for export. The
|
||||
@@ -1115,18 +1114,13 @@ class Monetary(Field):
|
||||
assert self.currency_field in model._fields, \
|
||||
"Field %s with unknown currency_field %r" % (self, self.currency_field)
|
||||
|
||||
def convert_to_write(self, value, target=None, fnames=None):
|
||||
if target is not None:
|
||||
currency = target[self.currency_field]
|
||||
# FIXME @rco-odoo: currency may not be already initialized if it is
|
||||
# a function or related field!
|
||||
if currency:
|
||||
return currency.round(float(value or 0.0))
|
||||
return float(value or 0.0)
|
||||
return value
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
return self.convert_to_write(value, record)
|
||||
currency = record[self.currency_field]
|
||||
# FIXME @rco-odoo: currency may not be already initialized if it is a
|
||||
# function or related field!
|
||||
if currency:
|
||||
return currency.round(float(value or 0.0))
|
||||
return float(value or 0.0)
|
||||
|
||||
|
||||
class _String(Field):
|
||||
@@ -1649,10 +1643,7 @@ class Many2one(_Relational):
|
||||
else:
|
||||
return value.id
|
||||
|
||||
def convert_to_write(self, value, target=None, fnames=None):
|
||||
return value.id
|
||||
|
||||
def convert_to_onchange(self, value):
|
||||
def convert_to_write(self, value):
|
||||
return value.id
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
@@ -1729,37 +1720,31 @@ class _RelationalMulti(_Relational):
|
||||
def convert_to_read(self, value, use_name_get=True):
|
||||
return value.ids
|
||||
|
||||
def convert_to_write(self, value, target=None, fnames=None):
|
||||
# remove/delete former records
|
||||
if target is None:
|
||||
set_ids = []
|
||||
result = [(6, 0, set_ids)]
|
||||
add_existing = lambda id: set_ids.append(id)
|
||||
else:
|
||||
tag = 2 if self.type == 'one2many' else 3
|
||||
result = [(tag, record.id) for record in target[self.name] - value]
|
||||
add_existing = lambda id: result.append((4, id))
|
||||
|
||||
if fnames is None:
|
||||
# take all fields in cache, except the inverses of self
|
||||
fnames = set(value._fields) - set(MAGIC_COLUMNS)
|
||||
model = value.env[self.model_name]
|
||||
for invf in model._field_inverses[self]:
|
||||
fnames.discard(invf.name)
|
||||
|
||||
# add new and existing records
|
||||
def convert_to_write(self, value):
|
||||
# make result with new and existing records
|
||||
result = [(5,)]
|
||||
for record in value:
|
||||
if not record.id:
|
||||
values = {k: v for k, v in record._cache.iteritems() if k in fnames}
|
||||
values = dict(record._cache)
|
||||
values = record._convert_to_write(values)
|
||||
result.append((0, 0, values))
|
||||
elif record._is_dirty():
|
||||
values = {k: record._cache[k] for k in record._get_dirty() if k in fnames}
|
||||
values = {k: record._cache[k] for k in record._get_dirty()}
|
||||
values = record._convert_to_write(values)
|
||||
result.append((1, record.id, values))
|
||||
else:
|
||||
add_existing(record.id)
|
||||
result.append((4, record.id))
|
||||
return result
|
||||
|
||||
def convert_to_onchange(self, value, fnames=None):
|
||||
# return the recordset value as a list of dicts
|
||||
fields = [(name, value._fields[name]) for name in (fnames or [])]
|
||||
result = []
|
||||
for record in value:
|
||||
vals = {name: field.convert_to_onchange(record[name]) for name, field in fields}
|
||||
if record.id:
|
||||
vals['id'] = record.id
|
||||
result.append(vals)
|
||||
return result
|
||||
|
||||
def convert_to_export(self, value, env):
|
||||
|
||||
+18
-29
@@ -5785,13 +5785,10 @@ class BaseModel(object):
|
||||
def __getattr__(self, name):
|
||||
field = self._record._fields[name]
|
||||
value = self._record[name]
|
||||
return field.convert_to_onchange(value)
|
||||
return field.convert_to_write(value)
|
||||
record = self[self._context['field_parent']]
|
||||
global_vars['parent'] = RawRecord(record)
|
||||
field_vars = {
|
||||
key: self._fields[key].convert_to_onchange(val)
|
||||
for key, val in self._cache.iteritems()
|
||||
}
|
||||
field_vars = self._convert_to_write(self._cache)
|
||||
params = eval("[%s]" % params, global_vars, field_vars)
|
||||
|
||||
# call onchange method with context when possible
|
||||
@@ -5833,7 +5830,7 @@ class BaseModel(object):
|
||||
if not all(name in self._fields for name in names):
|
||||
return {}
|
||||
|
||||
# determine subfields for field.convert_to_write() below
|
||||
# determine subfields for field.convert_to_onchange() below
|
||||
secondary = []
|
||||
subfields = defaultdict(set)
|
||||
for dotname in field_onchange:
|
||||
@@ -5869,7 +5866,8 @@ class BaseModel(object):
|
||||
continue
|
||||
record[name] = value
|
||||
|
||||
result = {'value': {}}
|
||||
result = {}
|
||||
dirty = set()
|
||||
|
||||
# process names in order (or the keys of values if no name given)
|
||||
while todo:
|
||||
@@ -5891,32 +5889,23 @@ class BaseModel(object):
|
||||
for name, oldval in values.iteritems():
|
||||
field = self._fields[name]
|
||||
newval = record[name]
|
||||
if field.type in ('one2many', 'many2many'):
|
||||
if newval != oldval or newval._is_dirty():
|
||||
# put new value in result
|
||||
result['value'][name] = field.convert_to_write(
|
||||
newval, record._origin, subfields.get(name),
|
||||
)
|
||||
todo.append(name)
|
||||
else:
|
||||
# keep result: newval may have been dirty before
|
||||
pass
|
||||
else:
|
||||
if newval != oldval:
|
||||
# put new value in result
|
||||
result['value'][name] = field.convert_to_write(
|
||||
newval, record._origin, subfields.get(name),
|
||||
)
|
||||
todo.append(name)
|
||||
else:
|
||||
# clean up result to not return another value
|
||||
result['value'].pop(name, None)
|
||||
if newval != oldval or (
|
||||
field.type in ('one2many', 'many2many') and newval._is_dirty()
|
||||
):
|
||||
todo.append(name)
|
||||
dirty.add(name)
|
||||
|
||||
# At the moment, the client does not support updates on a *2many field
|
||||
# while this one is modified by the user.
|
||||
if field_name and not isinstance(field_name, list) and \
|
||||
if isinstance(field_name, basestring) and \
|
||||
self._fields[field_name].type in ('one2many', 'many2many'):
|
||||
result['value'].pop(field_name, None)
|
||||
dirty.discard(field_name)
|
||||
|
||||
# collect values from dirty fields
|
||||
result['value'] = {
|
||||
name: self._fields[name].convert_to_onchange(record[name], subfields.get(name))
|
||||
for name in dirty
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user