Files
odoo_source/odoo/tools/template_inheritance.py
T
Denis Ledoux 15948702e5 [IMP] tools: locate_node, remove useless code
`if isinstance(node, SKIPPED_ELEMENT_TYPES):`
---------------------------------------------

It has been added in
https://github.com/odoo/odoo/commit/31b9bee00676e179c124f3d42539103305cf7423

It's hard to tell why it has been added. Though it seems not useful:
- Has we iter the nodes using the tag of the specification,
  `arch.iter(spec.tag)`,
  this is normally impossible to iter on a node type from that
  SKIPPED_ELEMENT_TYPES node type list
  (`etree._Comment, etree._ProcessingInstruction, etree.CommentBase, etree.PIBase, etree._Entity`),
  as those node types do not have a tag (`spec.tag`).
  In addition, in the place calling `locate_node`,
  we already do this same check on the specification node:
  https://github.com/odoo/odoo/blob/48109c15d319ae69a8fcdd4a6c3b19172a2de6d9/odoo/tools/template_inheritance.py#L144-L150

Version spec should match parent's root element's version
---------------------------------------------------------

It has also been added in the same revision:
https://github.com/odoo/odoo/commit/31b9bee00676e179c124f3d42539103305cf7423

This has been added in 7.0 to handle `version="7.0"` in xml views.

However, these `version="7.0"` have been removed in 8.0, in revision
odoo/odoo@faa09da325
After the above revision, there were still some leftovers,
but which have all been removed in 11.0 in revision
odoo/odoo@70942e4cfb
during an improvement of the RNG validator.

This is therefore enough evidence to say this code
checking for the `version` attribute in `locate_node`
is no longer useful since a quite long time now.
It can therefore be removed.

closes odoo/odoo#107340

Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
2022-12-12 11:22:32 +01:00

264 lines
11 KiB
Python

from lxml import etree
from lxml.builder import E
import copy
import itertools
import logging
import re
from odoo.tools.translate import _
from odoo.tools import SKIPPED_ELEMENT_TYPES, html_escape
_logger = logging.getLogger(__name__)
RSTRIP_REGEXP = re.compile(r'\n[ \t]*$')
def add_stripped_items_before(node, spec, extract):
text = spec.text or ''
before_text = ''
prev = node.getprevious()
if prev is None:
parent = node.getparent()
result = parent.text and RSTRIP_REGEXP.search(parent.text)
before_text = result.group(0) if result else ''
parent.text = (parent.text or '').rstrip() + text
else:
result = prev.tail and RSTRIP_REGEXP.search(prev.tail)
before_text = result.group(0) if result else ''
prev.tail = (prev.tail or '').rstrip() + text
if len(spec) > 0:
spec[-1].tail = (spec[-1].tail or "").rstrip() + before_text
else:
spec.text = (spec.text or "").rstrip() + before_text
for child in spec:
if child.get('position') == 'move':
child = extract(child)
node.addprevious(child)
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 "").rstrip() + 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 all(node.get(attr) == spec.get(attr) for attr in spec.attrib if attr != 'position'):
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 Element specs_tree: a modifying architecture in an inheriting view
:param bool inherit_branding:
: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: %r", etree.tostring(spec, encoding='unicode'))
)
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 %r cannot be located in parent view", etree.tostring(spec, encoding='unicode'))
)
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':
mode = spec.get('mode', 'outer')
if mode == "outer":
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:
# TODO ideally the notion of 'inherit_branding' should
# not exist in this function. Given the current state of
# the code, it is however necessary to know where nodes
# were removed when distributing branding. As a stable
# fix, this solution was chosen: the location is marked
# with a "ProcessingInstruction" which will not impact
# the "Element" structure of the resulting tree.
# Exception: if we happen to replace a node that already
# has xpath branding (root level nodes), do not mark the
# location of the removal as it will mess up the branding
# of siblings elements coming from other views, after the
# branding is distributed (and those processing instructions
# removed).
if inherit_branding and not node.get('data-oe-xpath'):
node.addprevious(etree.ProcessingInstruction('apply-inheritance-specs-node-removal', node.tag))
for child in spec:
if child.get('position') == 'move':
child = extract(child)
node.addprevious(child)
node.getparent().remove(node)
elif mode == "inner":
# Replace the entire content of an element
for child in node:
node.remove(child)
node.text = None
for child in spec:
node.append(copy.deepcopy(child))
node.text = spec.text
else:
raise ValueError(_("Invalid mode attribute:") + " '%s'" % mode)
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 a sentinel element at the end, insert content of spec
# before the sentinel, then remove the sentinel element
sentinel = E.sentinel()
node.append(sentinel)
add_stripped_items_before(sentinel, spec, extract)
remove_element(sentinel)
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_stripped_items_before(sentinel, spec, extract)
remove_element(sentinel)
elif pos == 'before':
add_stripped_items_before(node, spec, extract)
else:
raise ValueError(
_("Invalid position attribute: '%s'") %
pos
)
else:
attrs = ''.join([
' %s="%s"' % (attr, html_escape(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