mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 14:12:05 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
279
odoo-bringout-oca-ocb-base/odoo/tools/template_inheritance.py
Normal file
279
odoo-bringout-oca-ocb-base/odoo/tools/template_inheritance.py
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
|
||||
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
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_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 as e:
|
||||
raise ValidationError(_("Invalid Expression while parsing xpath %r", expr)) from e
|
||||
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 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 = ''
|
||||
copied_node = copy.deepcopy(node)
|
||||
# TODO: Remove 'inherit_branding' logic if possible;
|
||||
# currently needed to track node removal for branding
|
||||
# distribution. Avoid marking root nodes to prevent
|
||||
# sibling branding issues.
|
||||
if inherit_branding:
|
||||
copied_node.set('data-oe-no-branding', '1')
|
||||
loc.append(copied_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)
|
||||
if node.tail is not None: # for lxml >= 5.1
|
||||
sentinel.tail = node.tail
|
||||
node.tail = None
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue