[IMP] tests: make Form call onchange with the context of the modified field

This aligns the behavior of Form with the web client form view.  Also
simplify the API of method _perform_onchange()

Part-of: odoo/odoo#116779
This commit is contained in:
Raphael Collet
2023-03-31 17:02:43 +02:00
parent 91b3813550
commit c93a212213
5 changed files with 95 additions and 49 deletions
+9 -9
View File
@@ -441,7 +441,7 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=436), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.sales_categ_field] = False
UserForm._perform_onchange([self.sales_categ_field])
UserForm._perform_onchange(self.sales_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -449,7 +449,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.sales_categ_field] = self.group_sales_administrator.id
UserForm._perform_onchange([self.sales_categ_field])
UserForm._perform_onchange(self.sales_categ_field)
self.assertFalse(UserForm.user_group_warning)
def test_user_group_inheritance_warning(self):
@@ -463,7 +463,7 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=437), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.sales_categ_field] = self.group_sales_user.id
UserForm._perform_onchange([self.sales_categ_field])
UserForm._perform_onchange(self.sales_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -471,7 +471,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.sales_categ_field] = self.group_sales_administrator.id
UserForm._perform_onchange([self.sales_categ_field])
UserForm._perform_onchange(self.sales_categ_field)
self.assertFalse(UserForm.user_group_warning)
def test_user_group_inheritance_warning_multi(self):
@@ -488,7 +488,7 @@ class TestUsersGroupWarning(TransactionCase):
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.sales_categ_field] = self.group_sales_user.id
UserForm._values[self.project_categ_field] = self.group_project_user.id
UserForm._perform_onchange([self.sales_categ_field])
UserForm._perform_onchange(self.sales_categ_field)
self.assertTrue(
UserForm.user_group_warning,
@@ -496,7 +496,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.sales_categ_field] = self.group_sales_administrator.id
UserForm._perform_onchange([self.sales_categ_field])
UserForm._perform_onchange(self.sales_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -515,7 +515,7 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=437), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.timesheets_categ_field] = self.group_timesheets_user_own_timesheet.id
UserForm._perform_onchange([self.timesheets_categ_field])
UserForm._perform_onchange(self.timesheets_categ_field)
self.assertEqual(
UserForm.user_group_warning,
@@ -523,7 +523,7 @@ class TestUsersGroupWarning(TransactionCase):
)
UserForm._values[self.timesheets_categ_field] = self.group_timesheets_user_all_timesheet.id
UserForm._perform_onchange([self.timesheets_categ_field])
UserForm._perform_onchange(self.timesheets_categ_field)
self.assertFalse(UserForm.user_group_warning)
def test_user_group_parent_inheritance_no_warning(self):
@@ -536,6 +536,6 @@ class TestUsersGroupWarning(TransactionCase):
with self.assertQueryCount(__system__=420), \
Form(self.test_group_user.with_context(show_user_group_warning=True), view='base.view_users_form') as UserForm:
UserForm._values[self.field_service_categ_field] = self.group_field_service_user.id
UserForm._perform_onchange([self.field_service_categ_field])
UserForm._perform_onchange(self.field_service_categ_field)
self.assertFalse(UserForm.user_group_warning)
@@ -113,6 +113,23 @@
</field>
</record>
<record id="o2m_parent_context" model="ir.ui.view">
<field name="name">Parent Inline Tree with context on field</field>
<field name="model">test_testing_utilities.parent</field>
<field name="arch" type="xml">
<form>
<field name="value"/>
<field name="v"/>
<field name="subs">
<tree editable="bottom">
<field name="value" context="{'parent_value': parent.value}"/>
<field name="name"/>
</tree>
</field>
</form>
</field>
</record>
<record id="non_normalized_attrs" model="ir.ui.view">
<field name="name">Non Normalised Domains</field>
<field name="model">test_testing_utilities.a</field>
@@ -365,6 +365,13 @@ class TestO2M(TransactionCase):
"should not have set v (and thus not name)"
)
def test_o2m_parent_context(self):
""" Test the o2m form with a context on the field that uses 'parent'. """
view = 'test_testing_utilities.o2m_parent_context'
with Form(self.env['test_testing_utilities.parent'], view=view) as f:
with f.subs.new() as s:
s.value = 42
def test_o2m_default(self):
""" Tests that default_get can return defaults for the o2m
"""
@@ -621,7 +628,7 @@ class TestNestedO2M(TransactionCase):
})
form = Form(obj)
form.qty_producing = 1
form._perform_onchange(['move_raw_ids'])
form._perform_onchange('move_raw_ids')
form.save()
def test_empty_update(self):
+1 -1
View File
@@ -6331,7 +6331,7 @@ class BaseModel(metaclass=MetaModel):
if name == 'id':
continue
field = record._fields[name]
if (field.type == 'properties' and field.definition_record in field_name
if (field.type == 'properties' and field.definition_record in names
and other.get(name) == self[name] == []):
# TODO: The parent field on "record" can be False, if it was changed,
# (even if if was changed to a not Falsy value) because of
+60 -38
View File
@@ -283,9 +283,9 @@ class Form:
vals.clear()
vals['id'] = False
# call onchange with an empty list of fields; this retrieves default
# values, applies onchanges and return the result
self._perform_onchange([])
# call onchange with no field; this retrieves default values, applies
# onchanges and return the result
self._perform_onchange()
# fill in whatever fields are still missing with falsy values
vals.update({
field_name: _cleanup_from_default(field_info['type'], False)
@@ -323,7 +323,7 @@ class Form:
value = value.id
self._values[field_name] = value
self._perform_onchange([field_name])
self._perform_onchange(field_name)
def _get_modifier(self, field_name, modifier, *, view=None, vals=None):
if view is None:
@@ -398,26 +398,27 @@ class Form:
def _get_context(self, field_name):
""" Return the context of a given field. """
context = self._view['contexts'].get(field_name)
if not context:
context_str = self._view['contexts'].get(field_name)
if not context_str:
return {}
eval_context = self._get_eval_context()
return safe_eval(context_str, eval_context)
# see _getEvalContext
# the context for a field's evals (of domain/context) is the composition of:
# * the parent's values
# * ??? element.context ???
# * the environment's context (?)
# * a few magic values
eval_context = dict(self._values_to_save(all_fields=True))
eval_context.update(self._env.context)
eval_context.update(
id=self._record.id,
active_id=self._record.id,
active_ids=self._record.ids,
active_model=self._record._name,
current_date=date.today().strftime("%Y-%m-%d"),
)
return safe_eval(context, eval_context, {'context': eval_context})
def _get_eval_context(self):
""" Return the context dict to eval something. """
context = {
'id': self._record.id,
'active_id': self._record.id,
'active_ids': self._record.ids,
'active_model': self._record._name,
'current_date': date.today().strftime("%Y-%m-%d"),
**self._env.context,
}
return {
**context,
'context': context,
**self._values_to_save(all_fields=True),
}
def __enter__(self):
""" This makes the Form usable as a context manager. """
@@ -562,22 +563,27 @@ class Form:
result[field_name] = value
return result
def _perform_onchange(self, field_names, context=None):
assert isinstance(field_names, list)
def _perform_onchange(self, field_name=None):
assert field_name is None or isinstance(field_name, str)
# marks any onchange source as changed
self._changed.update(field_names)
# marks onchange source as changed
if field_name:
self._changed.add(field_name)
# skip calling onchange() if there's no trigger on any of the changed
# fields
# skip calling onchange() if there's no on_change on the field
spec = self._view['onchange']
if field_names and not any(spec[field_name] for field_name in field_names):
if field_name and not spec[field_name]:
return
record = self._record
if context is not None:
record = record.with_context(**context)
result = record.onchange(self._onchange_values(), field_names, spec)
# if the onchange is triggered by a field, add the context of that field
if field_name:
context = self._get_context(field_name)
if context:
record = record.with_context(**context)
result = record.onchange(self._onchange_values(), field_name, spec)
self._env.flush_all()
self._env.clear() # discard cache and pending recomputations
@@ -740,6 +746,11 @@ class O2MForm(Form):
vals = {**self._values, '•parent•': self._proxy._form._values}
return super()._get_modifier(field_name, modifier, view=view, vals=vals)
def _get_eval_context(self):
eval_context = super()._get_eval_context()
eval_context['parent'] = Dotter(self._proxy._form._values)
return eval_context
def _onchange_values(self):
values = super()._onchange_values()
# computed o2m may not have a relation_field(?)
@@ -768,8 +779,7 @@ class O2MForm(Form):
else:
raise AssertionError(f"Expected command 0 or 1, found {cmd!r}")
# FIXME: should be called when performing on change => value needs to be serialised into parent every time?
proxy._form._perform_onchange([proxy._field], self._env.context)
proxy._form._perform_onchange(proxy._field)
def _values_to_save(self, all_fields=False):
""" Validates values and returns only fields modified since
@@ -905,7 +915,7 @@ class O2MProxy(X2MProxy):
raise AssertionError("Expected command 0 or 1, got %s" % commands[cidx])
# remove reified record
del self._records[index]
self._form._perform_onchange([self._field])
self._form._perform_onchange(self._field)
def _command_index(self, for_record):
""" Takes a record index and finds the corresponding record index
@@ -962,7 +972,7 @@ class M2MProxy(X2MProxy, collections.abc.Sequence):
f"trying to assign a {record._name!r} object to a {comodel_name!r} field"
self._get_ids().append(record.id)
parent._perform_onchange([self._field])
parent._perform_onchange(self._field)
# pylint: disable=redefined-builtin
def remove(self, id=None, index=None):
@@ -976,14 +986,14 @@ class M2MProxy(X2MProxy, collections.abc.Sequence):
del self._get_ids()[index]
else:
self._get_ids().remove(id)
self._form._perform_onchange([self._field])
self._form._perform_onchange(self._field)
def clear(self):
""" Removes all existing records in the m2m
"""
self._assert_editable()
self._get_ids()[:] = []
self._form._perform_onchange([self._field])
self._form._perform_onchange(self._field)
def read_record(record, fields):
@@ -1031,3 +1041,15 @@ def _cleanup_from_default(type_, value):
elif type_ == 'date' and isinstance(value, date):
return odoo.fields.Date.to_string(value)
return value
class Dotter:
""" Simple wrapper for a dict where keys are accessed as readonly attributes. """
__slots__ = ['__values']
def __init__(self, values):
self.__values = values
def __getattr__(self, key):
val = self.__values[key]
return Dotter(val) if isinstance(val, dict) else val