[FIX] core: don't send readonly fields in o2m in SSF

The SSF would correctly filter out readonly fields when saving a
toplevel form, however it could not remove readonly values when saving
o2m pseudo-records to the parent form (as these would be expected to
remain available for reading upon the next edition and whatnot), so
these values would get sent in 0/1 commands.

Filter out these fields during the parent / toplevel save call.

Complexity notes:

* evaluating readonly modifiers requires the entire record, so
  unchanged fields still have to be written back to the parent form
  and be filtered out when *it* is set up for save, an alternative
  would be to store the `changed` and `readonly` flags alongside the
  record dict, and have the post-process only override the
  pre-computed readonly flag using force_save
* had to fix a test to match the new behaviour, the post-edition
  states turns out to be in line with the client's behaviour (or how
  it looks anyway)

Fixes #32019
This commit is contained in:
Xavier Morel
2019-04-03 08:44:22 +00:00
parent 922d77a291
commit ef8df8fb4e
4 changed files with 118 additions and 34 deletions
@@ -17,3 +17,5 @@ access_test_testing_utilities_onchange_parent,access_test_testing_utilities_onch
access_test_testing_utilities_onchange_line,access_test_testing_utilities_onchange_line,model_test_testing_utilities_onchange_line,,1,0,0,0
access_model_test_testing_utilities_onchange_count,access_model_test_testing_utilities_onchange_count,model_test_testing_utilities_onchange_count,,1,1,1,1
access_test_testing_utilities_onchange_count_sub,access_test_testing_utilities_onchange_count_sub,model_test_testing_utilities_onchange_count_sub,,1,1,1,1
access_o2m_readonly_subfield_parent,access_o2m_readonly_subfield_parent,model_o2m_readonly_subfield_parent,,1,1,1,1
access_o2m_readonly_subfield_child,access_o2m_readonly_subfield_child,model_o2m_readonly_subfield_child,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
17 access_test_testing_utilities_onchange_line access_test_testing_utilities_onchange_line model_test_testing_utilities_onchange_line 1 0 0 0
18 access_model_test_testing_utilities_onchange_count access_model_test_testing_utilities_onchange_count model_test_testing_utilities_onchange_count 1 1 1 1
19 access_test_testing_utilities_onchange_count_sub access_test_testing_utilities_onchange_count_sub model_test_testing_utilities_onchange_count_sub 1 1 1 1
20 access_o2m_readonly_subfield_parent access_o2m_readonly_subfield_parent model_o2m_readonly_subfield_parent 1 1 1 1
21 access_o2m_readonly_subfield_child access_o2m_readonly_subfield_child model_o2m_readonly_subfield_child 1 1 1 1
@@ -240,3 +240,24 @@ class O2MChangeSub(models.Model):
parent = fields.Many2one('test_testing_utilities.onchange_count')
name = fields.Char()
class O2MReadonlySubfield(models.Model):
_name = 'o2m_readonly_subfield_parent'
_description = _name
line_ids = fields.One2many('o2m_readonly_subfield_child', 'parent_id')
class O2MReadonlySubfieldChild(models.Model):
_name = _description = 'o2m_readonly_subfield_child'
name = fields.Char()
parent_id = fields.Many2one('o2m_readonly_subfield_parent')
f = fields.Integer(compute='_compute_f', inverse='_inverse_f', readonly=True)
@api.depends('name')
def _compute_f(self):
for r in self:
r.f = len(r.name) if r.name else 0
def _inverse_f(self):
raise AssertionError("Inverse of f should not be called")
@@ -359,7 +359,7 @@ class TestO2M(TransactionCase):
self.assertEqual(
[get(s) for s in r.subs],
[("5", 2, 5)]
[("5", 0, 5)]
)
def test_o2m_inner_default(self):
@@ -409,7 +409,10 @@ class TestO2M(TransactionCase):
self.assertEqual(sub.value, 1)
self.assertEqual(sub.v, 1)
def test_m2o_readonly(self):
def test_readonly_o2m(self):
""" Tests that o2m fields flagged as readonly (readonly="1" in the
view) can't be written to
"""
r = self.env['test_testing_utilities.parent'].create({
'subs': [(0, 0, {})]
})
@@ -422,6 +425,20 @@ class TestO2M(TransactionCase):
with self.assertRaises(AssertionError):
f.subs.remove(index=0)
def test_o2m_readonly_subfield(self):
""" Tests that readonly is applied to the field of the o2m = not sent
as part of the create / write values
"""
f = Form(self.env['o2m_readonly_subfield_parent'])
with f.line_ids.new() as new_line:
new_line.name = "ok"
self.assertEqual(new_line.f, 2)
r = f.save()
self.assertEqual(
(r.line_ids.name, r.line_ids.f),
('ok', 2)
)
def test_o2m_dyn_onchange(self):
f = Form(self.env['test_testing_utilities.onchange_parent'], view='test_testing_utilities.m2o_onchange_view')
+76 -32
View File
@@ -1149,14 +1149,6 @@ class Form(object):
self._process_fvg(submodel, edition, o2m=False)
descr['views']['edition'] = edition
def _get_node(self, f):
""" Find etree node for the field ``f`` in the current arch
"""
return next(
n for n in self._view['tree'].iter('field')
if n.get('name') == f
)
def __str__(self):
return "<%s %s(%s)>" % (
type(self).__name__,
@@ -1250,12 +1242,13 @@ class Form(object):
return O2MProxy(self, field)
return v
def _get_modifier(self, field, modifier, default=False):
d = self._view['modifiers'][field].get(modifier, default)
def _get_modifier(self, field, modifier, default=False, modmap=None, vals=None):
d = (modmap or self._view['modifiers'])[field].get(modifier, default)
if isinstance(d, bool):
return d
vals = self._values
if vals is None:
vals = self._values
stack = []
for it in reversed(d):
if it == '!':
@@ -1369,7 +1362,8 @@ class Form(object):
load/save
"""
values = {}
for f in self._view['fields']:
fields = self._view['fields']
for f in fields:
v = self._values[f]
if self._get_modifier(f, 'required'):
assert v is not False, "{} is a required field".format(f)
@@ -1379,10 +1373,31 @@ class Form(object):
continue
if self._get_modifier(f, 'readonly'):
node = self._get_node(f)
node = _get_node(self._view, f)
if not node.get('force_save'):
continue
if fields[f]['type'] == 'one2many':
view = fields[f]['views']['edition']
modifiers = view['modifiers']
oldvals = v
v = []
nodes = {
n.get('name'): n
for n in view['tree'].iter('field')
}
nodes['id'] = etree.Element('field', attrib={'name': 'id'})
for (c, rid, vs) in oldvals:
if c in (0, 1):
items = getattr(vs, 'changed_items', vs.items)
vs = {
k: v for k, v in items()
if nodes[k].get('force_save') or not self._get_modifier(k, 'readonly', modmap=modifiers, vals=vs)
}
v.append((c, rid, vs))
values[f] = v
return values
@@ -1417,11 +1432,15 @@ class Form(object):
values = {}
for k, v in self._values.items():
if f[k]['type'] == 'one2many':
# web client sends a 4 for unmodified o2m rows
values[k] = [
(4, rid, False) if (c == 1 and not vs) else (c, rid, vs)
for (c, rid, vs) in v
]
it = values[k] = []
for (c, rid, vs) in v:
if c == 1 and not vs:
# web client sends a 4 for unmodified o2m rows
it.append((4, rid, False))
elif c == 1 and isinstance(vs, UpdateDict):
it.append((1, rid, dict(vs.changed_items())))
else:
it.append((c, rid, vs))
else:
values[k] = v
return values
@@ -1520,9 +1539,16 @@ class O2MForm(Form):
if self._index is None:
commands.append((0, 0, values))
else:
(c, _, vs) = commands[proxy._command_index(self._index)]
assert c in (0, 1)
vs.update(values)
index = proxy._command_index(self._index)
(c, id_, vs) = commands[index]
if c == 0:
vs.update(values)
elif c == 1:
vs = UpdateDict(vs)
vs.update(values)
commands[index] = (1, id_, vs)
else:
raise AssertionError("Expected command type 0 or 1, found %s" % c)
# FIXME: should be called when performing on change => value needs to be serialised into parent every time?
proxy._parent._perform_onchange([proxy._field])
@@ -1531,20 +1557,31 @@ class O2MForm(Form):
""" Validates values and returns only fields modified since
load/save
"""
values = {}
for f in self._view['fields']:
v = self._values[f]
if self._get_modifier(f, 'required'):
assert v is not False, "{} is a required field".format(f)
values = UpdateDict(self._values)
values._changed.update(self._changed)
for f in self._view['fields']:
if self._get_modifier(f, 'required'):
assert self._values[f] is not False, "{} is a required field".format(f)
# skip unmodified fields
if f not in self._changed:
continue
# if self._get_modifier(f, 'readonly'):
# continue
values[f] = v
return values
class UpdateDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._changed = set()
def changed_items(self):
return (
(k, v) for k, v in self.items()
if k in self._changed
)
def update(self, *args, **kw):
super().update(*args, **kw)
if args and isinstance(args[0], UpdateDict):
self._changed.update(args[0]._changed)
class X2MProxy(object):
_parent = None
_field = None
@@ -1746,6 +1783,13 @@ def record_to_values(fields, record):
r[f] = v
return r
def _get_node(view, f, *arg):
""" Find etree node for the field ``f`` in the view's arch
"""
return next((
n for n in view['tree'].iter('field')
if n.get('name') == f
), *arg)
def tagged(*tags):
"""