Files
odoo_source/odoo/tools/template_inheritance.py
T
Alexandre Kühn dc31884dcf [FIX] base, tools: add pre_locate to apply_inheritance_specs
Revision on https://github.com/odoo/odoo/commit/fc5878ecc668ee83cd1a374474f0b21bf6a51940

Commit above introduced server-side inheritance of static XML.
To do so, it made some changes to the way inheritance of view
templates is applied, such as moving this logic outside of the record
`ir.ui.view`.

This change, however, removed the feature to overwrite these methods.
Studio did that in order to correctly apply `group_ids`[1][2].

This commit fixes the issue by introducing an optional callback
`pre_locate` as parameter of the methods `apply_inheritance_specs`.
When set, this function is called just before locating a node during
the application of inheritance of the view. Studio can make use of
this callback to handle group_ids properly, again.

[1] https://github.com/odoo/enterprise/commit/b56101200ac35dae3eadbc0bdf4ba17461f33907
[2] https://github.com/odoo/enterprise/commit/38f7eb8153df8406498fa58eb809271db778e6d8

Related to: https://github.com/odoo/enterprise/pull/5321

Task-Id 2047439

closes odoo/odoo#36200

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
2019-08-30 11:41:12 +00:00

213 lines
8.2 KiB
Python

from lxml import etree
from lxml.builder import E
import copy
import itertools
from odoo.tools.translate import _
from odoo.tools import SKIPPED_ELEMENT_TYPES
def add_text_before(node, text):
""" Add text before ``node`` in its XML tree. """
if text is None:
return
prev = node.getprevious()
if prev is not None:
prev.tail = (prev.tail or "") + text
else:
parent = node.getparent()
parent.text = (parent.text or "") + text
def add_text_inside(node, text):
""" Add text inside ``node``. """
if text is None:
return
if len(node):
node[-1].tail = (node[-1].tail or "") + text
else:
node.text = (node.text or "") + text
def remove_element(node):
""" Remove ``node`` but not its tail, from its XML tree. """
add_text_before(node, node.tail)
node.tail = None
node.getparent().remove(node)
def locate_node(arch, spec):
""" Locate a node in a source (parent) architecture.
Given a complete source (parent) architecture (i.e. the field
`arch` in a view), and a 'spec' node (a node in an inheriting
view that specifies the location in the source view of what
should be changed), return (if it exists) the node in the
source view matching the specification.
:param arch: a parent architecture to modify
:param spec: a modifying node in an inheriting view
:return: a node in the source matching the spec
"""
if spec.tag == 'xpath':
nodes = etree.ETXPath(spec.get('expr'))(arch)
return nodes[0] if nodes else None
elif spec.tag == 'field':
# Only compare the field name: a field can be only once in a given view
# at a given level (and for multilevel expressions, we should use xpath
# inheritance spec anyway).
for node in arch.iter('field'):
if node.get('name') == spec.get('name'):
return node
return None
for node in arch.iter(spec.tag):
if isinstance(node, SKIPPED_ELEMENT_TYPES):
continue
if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
if attr not in ('position', 'version')):
# Version spec should match parent's root element's version
if spec.get('version') and spec.get('version') != arch.get('version'):
return None
return node
return None
def apply_inheritance_specs(source, specs_tree, inherit_branding=False, pre_locate=lambda s: True):
""" Apply an inheriting view (a descendant of the base view)
Apply to a source architecture all the spec nodes (i.e. nodes
describing where and what changes to apply to some parent
architecture) given by an inheriting view.
:param Element source: a parent architecture to modify
:param pre_locate: function that is executed before locating a node.
This function receives an arch as argument.
This is required by studio to properly handle group_ids.
:return: a modified source where the specs are applied
:rtype: Element
"""
# Queue of specification nodes (i.e. nodes describing where and
# changes to apply to some parent architecture).
specs = specs_tree if isinstance(specs_tree, list) else [specs_tree]
def extract(spec):
"""
Utility function that locates a node given a specification, remove
it from the source and returns it.
"""
if len(spec):
raise ValueError(
_("Invalid specification for moved nodes: '%s'") %
etree.tostring(spec)
)
pre_locate(spec)
to_extract = locate_node(source, spec)
if to_extract is not None:
remove_element(to_extract)
return to_extract
else:
raise ValueError(
_("Element '%s' cannot be located in parent view") %
etree.tostring(spec)
)
while len(specs):
spec = specs.pop(0)
if isinstance(spec, SKIPPED_ELEMENT_TYPES):
continue
if spec.tag == 'data':
specs += [c for c in spec]
continue
pre_locate(spec)
node = locate_node(source, spec)
if node is not None:
pos = spec.get('position', 'inside')
if pos == 'replace':
for loc in spec.xpath(".//*[text()='$0']"):
loc.text = ''
loc.append(copy.deepcopy(node))
if node.getparent() is None:
source = copy.deepcopy(spec[0])
else:
replaced_node_tag = None
for child in spec:
if child.get('position') == 'move':
child = extract(child)
if inherit_branding and not replaced_node_tag and child.tag is not etree.Comment:
# To make a correct branding, we need to
# - know exactly which node has been replaced
# - store it before anything else has altered the Tree
# Do it exactly here :D
child.set('meta-oe-xpath-replacing', node.tag)
# We just store the replaced node tag on the first
# child of the xpath replacing it
replaced_node_tag = node.tag
node.addprevious(child)
node.getparent().remove(node)
elif pos == 'attributes':
for child in spec.getiterator('attribute'):
attribute = child.get('name')
value = child.text or ''
if child.get('add') or child.get('remove'):
assert not child.text
separator = child.get('separator', ',')
if separator == ' ':
separator = None # squash spaces
to_add = (
s for s in (s.strip() for s in child.get('add', '').split(separator))
if s
)
to_remove = {s.strip() for s in child.get('remove', '').split(separator)}
values = (s.strip() for s in node.get(attribute, '').split(separator))
value = (separator or ' ').join(itertools.chain(
(v for v in values if v not in to_remove),
to_add
))
if value:
node.set(attribute, value)
elif attribute in node.attrib:
del node.attrib[attribute]
elif pos == 'inside':
add_text_inside(node, spec.text)
for child in spec:
if child.get('position') == 'move':
child = extract(child)
node.append(child)
elif pos == 'after':
# add a sentinel element right after node, insert content of
# spec before the sentinel, then remove the sentinel element
sentinel = E.sentinel()
node.addnext(sentinel)
add_text_before(sentinel, spec.text)
for child in spec:
if child.get('position') == 'move':
child = extract(child)
sentinel.addprevious(child)
remove_element(sentinel)
elif pos == 'before':
add_text_before(node, spec.text)
for child in spec:
if child.get('position') == 'move':
child = extract(child)
node.addprevious(child)
else:
raise ValueError(
_("Invalid position attribute: '%s'") %
pos
)
else:
attrs = ''.join([
' %s="%s"' % (attr, spec.get(attr))
for attr in spec.attrib
if attr != 'position'
])
tag = "<%s%s>" % (spec.tag, attrs)
raise ValueError(
_("Element '%s' cannot be located in parent view") % tag
)
return source