[MIG] base_view_inheritance_extension: Migration to 15.0

* Removed `list_add` and `list_remove`, they've been deprecated and
implemented in Odoo core since several versions ago.

* Removed `move`, as it has also already been implemented in core several
versions ago.

* Replaced `python_dict` by `update`, that performs an operation similar
to :meth:`dict.update`, but on the ast.Dict.
pull/2494/head
Ivàn Todorovich 2021-12-13 11:58:55 -03:00 committed by Enric Tobella
parent 44871e11f2
commit 1b70255898
6 changed files with 148 additions and 209 deletions

View File

@ -3,7 +3,7 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
{ {
"name": "Extended view inheritance", "name": "Extended view inheritance",
"version": "14.0.1.1.0", "version": "15.0.1.0.0",
"author": "Therp BV,Odoo Community Association (OCA)", "author": "Therp BV,Odoo Community Association (OCA)",
"license": "LGPL-3", "license": "LGPL-3",
"category": "Hidden/Dependency", "category": "Hidden/Dependency",

View File

@ -8,16 +8,12 @@
<attribute name="string">Partner form</attribute> <attribute name="string">Partner form</attribute>
</xpath> </xpath>
<field name="parent_id" position="attributes"> <field name="parent_id" position="attributes">
<attribute <attribute name="context" operation="update">
name="context" {
operation="python_dict" "default_name": "The company name",
key="default_name" "default_company_id": context.get("company_id", context.get("company"))
>'The company name'</attribute> }
<attribute </attribute>
name="context"
operation="python_dict"
key="default_company_id"
>context.get('company_id', context.get('company'))</attribute>
</field> </field>
<!-- without operations, the standard handler should be called /--> <!-- without operations, the standard handler should be called /-->
<field name="parent_id" position="attributes"> <field name="parent_id" position="attributes">

View File

@ -11,6 +11,45 @@ from lxml import etree
from odoo import api, models from odoo import api, models
def ast_dict_update(source, update):
"""Perform a dict `update` on an ast.Dict
Behaves similar to :meth:`dict.update`, but on ast.Dict instead.
Only compares string-like ast.Dict keys (ast.Str or ast.Constant).
:returns: The updated ast.Dict
:rtype: ast.Dict
"""
if not isinstance(source, ast.Dict):
raise TypeError("`source` must be an AST dict")
if not isinstance(update, ast.Dict):
raise TypeError("`update` must be an AST dict")
def ast_key_eq(k1, k2):
# python < 3.8 uses ast.Str; python >= 3.8 uses ast.Constant
if type(k1) != type(k2):
return False
elif isinstance(k1, ast.Str):
return k1.s == k2.s
elif isinstance(k1, ast.Constant):
return k1.value == k2.value
toadd_uidx = []
for uidx, ukey in enumerate(update.keys):
found = False
for sidx, skey in enumerate(source.keys):
if ast_key_eq(ukey, skey):
source.values[sidx] = update.values[uidx]
found = True
break
if not found:
toadd_uidx.append(uidx)
for uidx in toadd_uidx:
source.keys.append(update.keys[uidx])
source.values.append(update.values[uidx])
return source
class IrUiView(models.Model): class IrUiView(models.Model):
_inherit = "ir.ui.view" _inherit = "ir.ui.view"
@ -41,14 +80,14 @@ class IrUiView(models.Model):
@api.model @api.model
def _get_inheritance_handler(self, node): def _get_inheritance_handler(self, node):
handler = super(IrUiView, self).apply_inheritance_specs handler = super().apply_inheritance_specs
if hasattr(self, "inheritance_handler_%s" % node.tag): if hasattr(self, "inheritance_handler_%s" % node.tag):
handler = getattr(self, "inheritance_handler_%s" % node.tag) handler = getattr(self, "inheritance_handler_%s" % node.tag)
return handler return handler
@api.model @api.model
def _get_inheritance_handler_attributes(self, node): def _get_inheritance_handler_attributes(self, node):
handler = super(IrUiView, self).apply_inheritance_specs handler = super().apply_inheritance_specs
if hasattr(self, "inheritance_handler_attributes_%s" % node.get("operation")): if hasattr(self, "inheritance_handler_attributes_%s" % node.get("operation")):
handler = getattr( handler = getattr(
self, "inheritance_handler_attributes_%s" % node.get("operation") self, "inheritance_handler_attributes_%s" % node.get("operation")
@ -56,75 +95,35 @@ class IrUiView(models.Model):
return handler return handler
@api.model @api.model
def inheritance_handler_attributes_python_dict(self, source, specs): def inheritance_handler_attributes_update(self, source, specs):
"""Implement """Implement dict `update` operation on the attribute node.
<$node position="attributes">
<attribute name="$attribute" operation="python_dict" key="$key"> .. code-block:: xml
$keyvalue
</attribute> <field position="attributes">
</$node>""" <attribute name="context" operation="update">
{
"key": "value",
}
</attribute>
</field>
"""
node = self.locate_node(source, specs) node = self.locate_node(source, specs)
for attribute_node in specs: for spec in specs:
attr_name = attribute_node.get("name") attr_name = spec.get("name")
attr_key = attribute_node.get("key") # Parse ast from both node and spec
str_dict = node.get(attr_name) or "{}" source_ast = ast.parse(node.get(attr_name) or "{}", mode="eval").body
ast_dict = ast.parse(str_dict, mode="eval").body update_ast = ast.parse(spec.text.strip(), mode="eval").body
assert isinstance(ast_dict, ast.Dict), f"'{attr_name}' is not a dict" if not isinstance(source_ast, ast.Dict):
assert attr_key, "No key specified for 'python_dict' operation" raise TypeError(f"Attribute `{attr_name}` is not a dict")
# Find the ast dict key if not isinstance(update_ast, ast.Dict):
# python < 3.8 uses ast.Str; python >= 3.8 uses ast.Constant raise TypeError(f"Operation for attribute `{attr_name}` is not a dict")
key_idx = next( # Update node ast dict
( source_ast = ast_dict_update(source_ast, update_ast)
i
for i, k in enumerate(ast_dict.keys)
if (isinstance(k, ast.Str) and k.s == attr_key)
or (isinstance(k, ast.Constant) and k.value == attr_key)
),
None,
)
# Update or create the key
value = ast.parse(attribute_node.text.strip(), mode="eval").body
if key_idx:
ast_dict.values[key_idx] = value
else:
ast_dict.keys.append(ast.Str(attr_key))
ast_dict.values.append(value)
# Dump the ast back to source # Dump the ast back to source
# TODO: once odoo requires python >= 3.9; use `ast.unparse` instead # TODO: once odoo requires python >= 3.9; use `ast.unparse` instead
node.attrib[attribute_node.get("name")] = astor.to_source( node.attrib[attr_name] = astor.to_source(
ast_dict, pretty_source=lambda s: "".join(s).strip() source_ast,
pretty_source=lambda s: "".join(s).strip(),
) )
return source return source
@api.model
def inheritance_handler_attributes_list_add(self, source, specs):
"""Implement
<$node position="attributes">
<attribute name="$attribute" operation="list_add">
$new_value
</attribute>
</$node>"""
node = self.locate_node(source, specs)
for attribute_node in specs:
attribute_name = attribute_node.get("name")
old_value = node.get(attribute_name) or ""
new_value = old_value + "," + attribute_node.text
node.attrib[attribute_name] = new_value
return source
@api.model
def inheritance_handler_attributes_list_remove(self, source, specs):
"""Implement
<$node position="attributes">
<attribute name="$attribute" operation="list_remove">
$value_to_remove
</attribute>
</$node>"""
node = self.locate_node(source, specs)
for attribute_node in specs:
attribute_name = attribute_node.get("name")
old_values = (node.get(attribute_name) or "").split(",")
remove_values = attribute_node.text.split(",")
new_values = [x for x in old_values if x not in remove_values]
node.attrib[attribute_name] = ",".join([_f for _f in new_values if _f])
return source

View File

@ -1,2 +1 @@
* On `15.0`, remove `list_add` and `list_remove` fetures.
* Support an ``eval`` attribute for our new node types. * Support an ``eval`` attribute for our new node types.

View File

@ -3,21 +3,14 @@
.. code-block:: xml .. code-block:: xml
<attribute name="$attribute" operation="python_dict" key="$key"> <field position="attributes">
$new_value <attribute name="context" operation="update">
</attribute> {
"key": "value",
}
</attribute>
</field>
Note that views are subject to evaluation of xmlids anyways, so if you need Note that views are subject to evaluation of xmlids anyways, so if you need
to refer to some xmlid, say ``%(xmlid)s``. to refer to some xmlid, say ``%(xmlid)s``.
**Add to values in a list (states for example)**
Deprecated. This feature is now native, use `<attribute name="attrname" separator="," add="something" />`.
**Remove values from a list (states for example)**
Deprecated. This feature is now native, use `<attribute name="attrname" separator="," remove="something" />`.
**Move an element in the view**
This feature is now native, cf the `official Odoo documentation <https://www.odoo.com/documentation/14.0/developer/reference/addons/views.html#inheritance-specs>`_.

View File

@ -5,22 +5,22 @@
from lxml import etree from lxml import etree
from odoo.tests.common import SavepointCase from odoo.tests.common import TransactionCase
class TestBaseViewInheritanceExtension(SavepointCase): class TestBaseViewInheritanceExtension(TransactionCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.ViewModel = cls.env["ir.ui.view"] cls.maxDiff = None
def test_base_view_inheritance_extension(self): def test_base_view_inheritance_extension(self):
view_id = self.env.ref("base.view_partner_simple_form").id view_id = self.env.ref("base.view_partner_simple_form").id
fields_view_get = self.env["res.partner"].fields_view_get(view_id=view_id) fields_view_get = self.env["res.partner"].fields_view_get(view_id=view_id)
view = etree.fromstring(fields_view_get["arch"]) view = etree.fromstring(fields_view_get["arch"])
# verify normal attributes work # Verify normal attributes work
self.assertEqual(view.xpath("//form")[0].get("string"), "Partner form") self.assertEqual(view.xpath("//form")[0].get("string"), "Partner form")
# verify our extra context key worked # Verify our extra context key worked
self.assertTrue( self.assertTrue(
"default_name" in view.xpath('//field[@name="parent_id"]')[0].get("context") "default_name" in view.xpath('//field[@name="parent_id"]')[0].get("context")
) )
@ -29,75 +29,7 @@ class TestBaseViewInheritanceExtension(SavepointCase):
in view.xpath('//field[@name="parent_id"]')[0].get("context") in view.xpath('//field[@name="parent_id"]')[0].get("context")
) )
def test_list_add(self): def test_update_context_default(self):
view_model = self.env["ir.ui.view"]
source = etree.fromstring(
"""\
<form>
<button name="test" states="draft,open"/>
</form>
"""
)
# extend with single value
specs = etree.fromstring(
"""\
<button name="test" position="attributes">
<attribute
name="states"
operation="list_add"
>valid</attribute>
</button>
"""
)
modified_source = view_model.inheritance_handler_attributes_list_add(
source, specs
)
button_node = modified_source.xpath('//button[@name="test"]')[0]
self.assertEqual(button_node.attrib["states"], "draft,open,valid")
# extend with list of values
specs = etree.fromstring(
"""\
<button name="test" position="attributes">
<attribute
name="states"
operation="list_add"
>payable,paid</attribute>
</button>
"""
)
modified_source = view_model.inheritance_handler_attributes_list_add(
source, specs
)
button_node = modified_source.xpath('//button[@name="test"]')[0]
self.assertEqual(button_node.attrib["states"], "draft,open,valid,payable,paid")
def test_list_remove(self):
view_model = self.env["ir.ui.view"]
source = etree.fromstring(
"""\
<form>
<button name="test" states="draft,open,valid,payable,paid"/>
</form>
"""
)
# remove list of values
specs = etree.fromstring(
"""\
<button name="test" position="attributes">
<attribute
name="states"
operation="list_remove"
>open,payable</attribute>
</button>
"""
)
modified_source = view_model.inheritance_handler_attributes_list_remove(
source, specs
)
button_node = modified_source.xpath('//button[@name="test"]')[0]
self.assertEqual(button_node.attrib["states"], "draft,valid,paid")
def test_python_dict_inheritance_context_default(self):
source = etree.fromstring( source = etree.fromstring(
""" """
<form> <form>
@ -108,19 +40,19 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="account_move_id" position="attributes"> <field name="account_move_id" position="attributes">
<attribute name="context" operation="python_dict" key="default_company_id"> <attribute name="context" operation="update">
company_id {"default_company_id": company_id}
</attribute> </attribute>
</field> </field>
""" """
) )
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual( self.assertEqual(
res.xpath('//field[@name="account_move_id"]')[0].attrib["context"], res.xpath('//field[@name="account_move_id"]')[0].attrib["context"],
"{'default_journal_id': journal_id, 'default_company_id': company_id}", "{'default_journal_id': journal_id, 'default_company_id': company_id}",
) )
def test_python_dict_inheritance_context_complex(self): def test_update_context_complex(self):
source = etree.fromstring( source = etree.fromstring(
""" """
<form> <form>
@ -142,27 +74,39 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="invoice_line_ids" position="attributes"> <field name="invoice_line_ids" position="attributes">
<attribute name="context" operation="python_dict" key="default_product_id"> <attribute name="context" operation="update">
product_id {
</attribute> "default_product_id": product_id,
<attribute name="context" operation="python_dict" key="default_cost_center_id"> "default_cost_center_id": (
context.get('handle_mrp_cost') and cost_center_id or False context.get("handle_mrp_cost") and cost_center_id or False
),
}
</attribute> </attribute>
</field> </field>
""" """
) )
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
expected_items = [
"'default_type': context.get('default_type')",
"'journal_id': journal_id",
"'default_partner_id': commercial_partner_id",
(
"'default_currency_id': "
"currency_id != company_currency_id and currency_id or False"
),
"'default_name': 'The company name'",
"'default_product_id': product_id",
(
"'default_cost_center_id': "
"context.get('handle_mrp_cost') and cost_center_id or False"
),
]
self.assertEqual( self.assertEqual(
res.xpath('//field[@name="invoice_line_ids"]')[0].attrib["context"], res.xpath('//field[@name="invoice_line_ids"]')[0].attrib["context"],
"{'default_type': context.get('default_type'), 'journal_id': journal_id, " "{%s}" % ", ".join(expected_items),
"'default_partner_id': commercial_partner_id, 'default_currency_id': "
"currency_id != company_currency_id and currency_id or False, "
"'default_name': 'The company name', 'default_product_id': product_id, "
"'default_cost_center_id': context.get('handle_mrp_cost') and "
"cost_center_id or False}",
) )
def test_python_dict_inheritance_attrs_add(self): def test_update_attrs_new_key(self):
"""Test that we can add new keys to an existing dict""" """Test that we can add new keys to an existing dict"""
source = etree.fromstring( source = etree.fromstring(
""" """
@ -177,20 +121,22 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="ref" position="attributes"> <field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict" key="required"> <attribute name="attrs" operation="update">
[('state', '!=', 'draft')] {
"required": [("state", "!=", "draft")],
}
</attribute> </attribute>
</field> </field>
""" """
) )
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual( self.assertEqual(
res.xpath('//field[@name="ref"]')[0].attrib["attrs"], res.xpath('//field[@name="ref"]')[0].attrib["attrs"],
"{'invisible': [('state', '=', 'draft')], " "{'invisible': [('state', '=', 'draft')], "
"'required': [('state', '!=', 'draft')]}", "'required': [('state', '!=', 'draft')]}",
) )
def test_python_dict_inheritance_attrs_update(self): def test_update_attrs_replace(self):
"""Test that we can replace an existing dict key""" """Test that we can replace an existing dict key"""
source = etree.fromstring( source = etree.fromstring(
""" """
@ -208,20 +154,22 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="ref" position="attributes"> <field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict" key="required"> <attribute name="attrs" operation="update">
[('state', '!=', 'draft')] {
"required": [('state', '!=', 'draft')],
}
</attribute> </attribute>
</field> </field>
""" """
) )
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual( self.assertEqual(
res.xpath('//field[@name="ref"]')[0].attrib["attrs"], res.xpath('//field[@name="ref"]')[0].attrib["attrs"],
"{'invisible': [('state', '=', 'draft')], " "{'invisible': [('state', '=', 'draft')], "
"'required': [('state', '!=', 'draft')]}", "'required': [('state', '!=', 'draft')]}",
) )
def test_python_dict_inheritance_attrs_new(self): def test_update_empty_source_dict(self):
"""Test that we can add new keys by creating the dict if it's missing""" """Test that we can add new keys by creating the dict if it's missing"""
source = etree.fromstring( source = etree.fromstring(
""" """
@ -233,20 +181,22 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="ref" position="attributes"> <field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict" key="required"> <attribute name="attrs" operation="update">
[('state', '!=', 'draft')] {
"required": [('state', '!=', 'draft')],
}
</attribute> </attribute>
</field> </field>
""" """
) )
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual( self.assertEqual(
res.xpath('//field[@name="ref"]')[0].attrib["attrs"], res.xpath('//field[@name="ref"]')[0].attrib["attrs"],
"{'required': [('state', '!=', 'draft')]}", "{'required': [('state', '!=', 'draft')]}",
) )
def test_python_dict_inheritance_attrs_missing_key(self): def test_update_operation_not_a_dict(self):
"""We should get an error if we try to update a dict without specifing a key""" """We should get an error if we try to update a dict with a non-dict spec"""
source = etree.fromstring( source = etree.fromstring(
""" """
<form> <form>
@ -257,18 +207,18 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="ref" position="attributes"> <field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict"> <attribute name="attrs" operation="update">
[('state', '!=', 'draft')] ["not", "a", "dict"]
</attribute> </attribute>
</field> </field>
""" """
) )
with self.assertRaisesRegex( with self.assertRaisesRegex(
AssertionError, "No key specified for 'python_dict' operation" TypeError, "Operation for attribute `attrs` is not a dict"
): ):
self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
def test_python_dict_inheritance_error_if_not_a_dict(self): def test_update_source_not_a_dict(self):
"""We should get an error if we try to update a non-dict attribute""" """We should get an error if we try to update a non-dict attribute"""
source = etree.fromstring( source = etree.fromstring(
""" """
@ -280,11 +230,13 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring( specs = etree.fromstring(
""" """
<field name="child_ids" position="attributes"> <field name="child_ids" position="attributes">
<attribute name="domain" operation="python_dict" key="required"> <attribute name="domain" operation="update">
[('state', '!=', 'draft')] {
"required": [('state', '!=', 'draft')],
}
</attribute> </attribute>
</field> </field>
""" """
) )
with self.assertRaisesRegex(AssertionError, "'domain' is not a dict"): with self.assertRaisesRegex(TypeError, "Attribute `domain` is not a dict"):
self.ViewModel.inheritance_handler_attributes_python_dict(source, specs) self.env["ir.ui.view"].apply_inheritance_specs(source, specs)