[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:
Raphael Collet
2015-08-14 15:09:04 +02:00
parent 36ffa5bc69
commit 9f81c6d651
3 changed files with 71 additions and 121 deletions
@@ -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
View File
@@ -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
View File
@@ -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