diff --git a/base_view_inheritance_extension/__manifest__.py b/base_view_inheritance_extension/__manifest__.py
index 1c3e46ba5..cc2879c62 100644
--- a/base_view_inheritance_extension/__manifest__.py
+++ b/base_view_inheritance_extension/__manifest__.py
@@ -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",
diff --git a/base_view_inheritance_extension/demo/ir_ui_view.xml b/base_view_inheritance_extension/demo/ir_ui_view.xml
index e40a4a98b..ffa3e9e52 100644
--- a/base_view_inheritance_extension/demo/ir_ui_view.xml
+++ b/base_view_inheritance_extension/demo/ir_ui_view.xml
@@ -8,16 +8,12 @@
Partner form
- 'The company name'
- context.get('company_id', context.get('company'))
+
+ {
+ "default_name": "The company name",
+ "default_company_id": context.get("company_id", context.get("company"))
+ }
+
diff --git a/base_view_inheritance_extension/models/ir_ui_view.py b/base_view_inheritance_extension/models/ir_ui_view.py
index 4be7df898..c04a3960b 100644
--- a/base_view_inheritance_extension/models/ir_ui_view.py
+++ b/base_view_inheritance_extension/models/ir_ui_view.py
@@ -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">
-
- $keyvalue
-
- $node>"""
+ def inheritance_handler_attributes_update(self, source, specs):
+ """Implement dict `update` operation on the attribute node.
+
+ .. code-block:: xml
+
+
+
+ {
+ "key": "value",
+ }
+
+
+ """
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">
-
- $new_value
-
- $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">
-
- $value_to_remove
-
- $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
diff --git a/base_view_inheritance_extension/readme/ROADMAP.rst b/base_view_inheritance_extension/readme/ROADMAP.rst
index c999faa2f..ad27c61b8 100644
--- a/base_view_inheritance_extension/readme/ROADMAP.rst
+++ b/base_view_inheritance_extension/readme/ROADMAP.rst
@@ -1,2 +1 @@
-* On `15.0`, remove `list_add` and `list_remove` fetures.
* Support an ``eval`` attribute for our new node types.
diff --git a/base_view_inheritance_extension/readme/USAGE.rst b/base_view_inheritance_extension/readme/USAGE.rst
index 18c469b6c..66ae50d75 100644
--- a/base_view_inheritance_extension/readme/USAGE.rst
+++ b/base_view_inheritance_extension/readme/USAGE.rst
@@ -3,21 +3,14 @@
.. code-block:: xml
-
- $new_value
-
+
+
+ {
+ "key": "value",
+ }
+
+
+
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 ``.
-
-**Remove values from a list (states for example)**
-
-Deprecated. This feature is now native, use ``.
-
-**Move an element in the view**
-
-This feature is now native, cf the `official Odoo documentation `_.
diff --git a/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py b/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py
index 1ef452187..d1ba7ff50 100644
--- a/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py
+++ b/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py
@@ -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(
- """\
-
- """
- )
- # extend with single value
- specs = etree.fromstring(
- """\
-
- """
- )
- 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(
- """\
-
- """
- )
- 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(
- """\
-
- """
- )
- # remove list of values
- specs = etree.fromstring(
- """\
-
- """
- )
- 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(
"""