Before create_multi, in case of invalid syntax used in an XPath, only the problematic record was displayed. It was not ideal for long definition but still usable. Since the views are created using create_multi, the whole file content is displayed in the error traceback, making it almost impossible to locate on files with multiple records. closes odoo/odoo#43590 X-original-commit: 8622469e1fdd4789cc45ed52093d0f329abca14e Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
238 lines
9.2 KiB
Python
238 lines
9.2 KiB
Python
|
|
from lxml import etree
|
|
from lxml.builder import E
|
|
import copy
|
|
import itertools
|
|
import logging
|
|
|
|
from odoo.tools.translate import _
|
|
from odoo.tools import SKIPPED_ELEMENT_TYPES
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
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':
|
|
expr = spec.get('expr')
|
|
try:
|
|
xPath = etree.ETXPath(expr)
|
|
except etree.XPathSyntaxError:
|
|
_logger.error("XPathSyntaxError while parsing xpath %r", expr)
|
|
raise
|
|
nodes = xPath(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:
|
|
spec_content = None
|
|
comment = None
|
|
for content in spec:
|
|
if content.tag is not etree.Comment:
|
|
spec_content = content
|
|
break
|
|
else:
|
|
comment = content
|
|
source = copy.deepcopy(spec_content)
|
|
# only keep the t-name of a template root node
|
|
t_name = node.get('t-name')
|
|
if t_name:
|
|
source.set('t-name', t_name)
|
|
if comment is not None:
|
|
text = source.text
|
|
source.text = None
|
|
comment.tail = text
|
|
source.insert(0, comment)
|
|
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
|