[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).
{
"name": "Extended view inheritance",
"version": "14.0.1.1.0",
"version": "15.0.1.0.0",
"author": "Therp BV,Odoo Community Association (OCA)",
"license": "LGPL-3",
"category": "Hidden/Dependency",

View File

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

View File

@ -11,6 +11,45 @@ from lxml import etree
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):
_inherit = "ir.ui.view"
@ -41,14 +80,14 @@ class IrUiView(models.Model):
@api.model
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):
handler = getattr(self, "inheritance_handler_%s" % node.tag)
return handler
@api.model
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")):
handler = getattr(
self, "inheritance_handler_attributes_%s" % node.get("operation")
@ -56,75 +95,35 @@ class IrUiView(models.Model):
return handler
@api.model
def inheritance_handler_attributes_python_dict(self, source, specs):
"""Implement
<$node position="attributes">
<attribute name="$attribute" operation="python_dict" key="$key">
$keyvalue
</attribute>
</$node>"""
def inheritance_handler_attributes_update(self, source, specs):
"""Implement dict `update` operation on the attribute node.
.. code-block:: xml
<field position="attributes">
<attribute name="context" operation="update">
{
"key": "value",
}
</attribute>
</field>
"""
node = self.locate_node(source, specs)
for attribute_node in specs:
attr_name = attribute_node.get("name")
attr_key = attribute_node.get("key")
str_dict = node.get(attr_name) or "{}"
ast_dict = ast.parse(str_dict, mode="eval").body
assert isinstance(ast_dict, ast.Dict), f"'{attr_name}' is not a dict"
assert attr_key, "No key specified for 'python_dict' operation"
# Find the ast dict key
# python < 3.8 uses ast.Str; python >= 3.8 uses ast.Constant
key_idx = next(
(
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)
for spec in specs:
attr_name = spec.get("name")
# Parse ast from both node and spec
source_ast = ast.parse(node.get(attr_name) or "{}", mode="eval").body
update_ast = ast.parse(spec.text.strip(), mode="eval").body
if not isinstance(source_ast, ast.Dict):
raise TypeError(f"Attribute `{attr_name}` is not a dict")
if not isinstance(update_ast, ast.Dict):
raise TypeError(f"Operation for attribute `{attr_name}` is not a dict")
# Update node ast dict
source_ast = ast_dict_update(source_ast, update_ast)
# Dump the ast back to source
# TODO: once odoo requires python >= 3.9; use `ast.unparse` instead
node.attrib[attribute_node.get("name")] = astor.to_source(
ast_dict, pretty_source=lambda s: "".join(s).strip()
node.attrib[attr_name] = astor.to_source(
source_ast,
pretty_source=lambda s: "".join(s).strip(),
)
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.

View File

@ -3,21 +3,14 @@
.. code-block:: xml
<attribute name="$attribute" operation="python_dict" key="$key">
$new_value
</attribute>
<field position="attributes">
<attribute name="context" operation="update">
{
"key": "value",
}
</attribute>
</field>
Note that views are subject to evaluation of xmlids anyways, so if you need
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 odoo.tests.common import SavepointCase
from odoo.tests.common import TransactionCase
class TestBaseViewInheritanceExtension(SavepointCase):
class TestBaseViewInheritanceExtension(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ViewModel = cls.env["ir.ui.view"]
cls.maxDiff = None
def test_base_view_inheritance_extension(self):
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)
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")
# verify our extra context key worked
# Verify our extra context key worked
self.assertTrue(
"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")
)
def test_list_add(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):
def test_update_context_default(self):
source = etree.fromstring(
"""
<form>
@ -108,19 +40,19 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="account_move_id" position="attributes">
<attribute name="context" operation="python_dict" key="default_company_id">
company_id
<attribute name="context" operation="update">
{"default_company_id": company_id}
</attribute>
</field>
"""
)
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs)
res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual(
res.xpath('//field[@name="account_move_id"]')[0].attrib["context"],
"{'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(
"""
<form>
@ -142,27 +74,39 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="invoice_line_ids" position="attributes">
<attribute name="context" operation="python_dict" key="default_product_id">
product_id
</attribute>
<attribute name="context" operation="python_dict" key="default_cost_center_id">
context.get('handle_mrp_cost') and cost_center_id or False
<attribute name="context" operation="update">
{
"default_product_id": product_id,
"default_cost_center_id": (
context.get("handle_mrp_cost") and cost_center_id or False
),
}
</attribute>
</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(
res.xpath('//field[@name="invoice_line_ids"]')[0].attrib["context"],
"{'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}",
"{%s}" % ", ".join(expected_items),
)
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"""
source = etree.fromstring(
"""
@ -177,20 +121,22 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict" key="required">
[('state', '!=', 'draft')]
<attribute name="attrs" operation="update">
{
"required": [("state", "!=", "draft")],
}
</attribute>
</field>
"""
)
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs)
res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual(
res.xpath('//field[@name="ref"]')[0].attrib["attrs"],
"{'invisible': [('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"""
source = etree.fromstring(
"""
@ -208,20 +154,22 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict" key="required">
[('state', '!=', 'draft')]
<attribute name="attrs" operation="update">
{
"required": [('state', '!=', 'draft')],
}
</attribute>
</field>
"""
)
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs)
res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual(
res.xpath('//field[@name="ref"]')[0].attrib["attrs"],
"{'invisible': [('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"""
source = etree.fromstring(
"""
@ -233,20 +181,22 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict" key="required">
[('state', '!=', 'draft')]
<attribute name="attrs" operation="update">
{
"required": [('state', '!=', 'draft')],
}
</attribute>
</field>
"""
)
res = self.ViewModel.inheritance_handler_attributes_python_dict(source, specs)
res = self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
self.assertEqual(
res.xpath('//field[@name="ref"]')[0].attrib["attrs"],
"{'required': [('state', '!=', 'draft')]}",
)
def test_python_dict_inheritance_attrs_missing_key(self):
"""We should get an error if we try to update a dict without specifing a key"""
def test_update_operation_not_a_dict(self):
"""We should get an error if we try to update a dict with a non-dict spec"""
source = etree.fromstring(
"""
<form>
@ -257,18 +207,18 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="ref" position="attributes">
<attribute name="attrs" operation="python_dict">
[('state', '!=', 'draft')]
<attribute name="attrs" operation="update">
["not", "a", "dict"]
</attribute>
</field>
"""
)
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"""
source = etree.fromstring(
"""
@ -280,11 +230,13 @@ class TestBaseViewInheritanceExtension(SavepointCase):
specs = etree.fromstring(
"""
<field name="child_ids" position="attributes">
<attribute name="domain" operation="python_dict" key="required">
[('state', '!=', 'draft')]
<attribute name="domain" operation="update">
{
"required": [('state', '!=', 'draft')],
}
</attribute>
</field>
"""
)
with self.assertRaisesRegex(AssertionError, "'domain' is not a dict"):
self.ViewModel.inheritance_handler_attributes_python_dict(source, specs)
with self.assertRaisesRegex(TypeError, "Attribute `domain` is not a dict"):
self.env["ir.ui.view"].apply_inheritance_specs(source, specs)