1 2from lxml import etree 3from lxml.builder import E 4import copy 5import itertools 6import logging 7 8from odoo.tools.translate import _ 9from odoo.tools import SKIPPED_ELEMENT_TYPES 10_logger = logging.getLogger(__name__) 11 12 13def add_text_before(node, text): 14 """ Add text before ``node`` in its XML tree. """ 15 if text is None: 16 return 17 prev = node.getprevious() 18 if prev is not None: 19 prev.tail = (prev.tail or "") + text 20 else: 21 parent = node.getparent() 22 parent.text = (parent.text or "") + text 23 24 25def add_text_inside(node, text): 26 """ Add text inside ``node``. """ 27 if text is None: 28 return 29 if len(node): 30 node[-1].tail = (node[-1].tail or "") + text 31 else: 32 node.text = (node.text or "") + text 33 34 35def remove_element(node): 36 """ Remove ``node`` but not its tail, from its XML tree. """ 37 add_text_before(node, node.tail) 38 node.tail = None 39 node.getparent().remove(node) 40 41 42def locate_node(arch, spec): 43 """ Locate a node in a source (parent) architecture. 44 45 Given a complete source (parent) architecture (i.e. the field 46 `arch` in a view), and a 'spec' node (a node in an inheriting 47 view that specifies the location in the source view of what 48 should be changed), return (if it exists) the node in the 49 source view matching the specification. 50 51 :param arch: a parent architecture to modify 52 :param spec: a modifying node in an inheriting view 53 :return: a node in the source matching the spec 54 """ 55 if spec.tag == 'xpath': 56 expr = spec.get('expr') 57 try: 58 xPath = etree.ETXPath(expr) 59 except etree.XPathSyntaxError: 60 _logger.error("XPathSyntaxError while parsing xpath %r", expr) 61 raise 62 nodes = xPath(arch) 63 return nodes[0] if nodes else None 64 elif spec.tag == 'field': 65 # Only compare the field name: a field can be only once in a given view 66 # at a given level (and for multilevel expressions, we should use xpath 67 # inheritance spec anyway). 68 for node in arch.iter('field'): 69 if node.get('name') == spec.get('name'): 70 return node 71 return None 72 73 for node in arch.iter(spec.tag): 74 if isinstance(node, SKIPPED_ELEMENT_TYPES): 75 continue 76 if all(node.get(attr) == spec.get(attr) for attr in spec.attrib 77 if attr not in ('position', 'version')): 78 # Version spec should match parent's root element's version 79 if spec.get('version') and spec.get('version') != arch.get('version'): 80 return None 81 return node 82 return None 83 84 85def apply_inheritance_specs(source, specs_tree, inherit_branding=False, pre_locate=lambda s: True): 86 """ Apply an inheriting view (a descendant of the base view) 87 88 Apply to a source architecture all the spec nodes (i.e. nodes 89 describing where and what changes to apply to some parent 90 architecture) given by an inheriting view. 91 92 :param Element source: a parent architecture to modify 93 :param pre_locate: function that is executed before locating a node. 94 This function receives an arch as argument. 95 This is required by studio to properly handle group_ids. 96 :return: a modified source where the specs are applied 97 :rtype: Element 98 """ 99 # Queue of specification nodes (i.e. nodes describing where and 100 # changes to apply to some parent architecture). 101 specs = specs_tree if isinstance(specs_tree, list) else [specs_tree] 102 103 def extract(spec): 104 """ 105 Utility function that locates a node given a specification, remove 106 it from the source and returns it. 107 """ 108 if len(spec): 109 raise ValueError( 110 _("Invalid specification for moved nodes: '%s'") % 111 etree.tostring(spec) 112 ) 113 pre_locate(spec) 114 to_extract = locate_node(source, spec) 115 if to_extract is not None: 116 remove_element(to_extract) 117 return to_extract 118 else: 119 raise ValueError( 120 _("Element '%s' cannot be located in parent view") % 121 etree.tostring(spec) 122 ) 123 124 while len(specs): 125 spec = specs.pop(0) 126 if isinstance(spec, SKIPPED_ELEMENT_TYPES): 127 continue 128 if spec.tag == 'data': 129 specs += [c for c in spec] 130 continue 131 pre_locate(spec) 132 node = locate_node(source, spec) 133 if node is not None: 134 pos = spec.get('position', 'inside') 135 if pos == 'replace': 136 for loc in spec.xpath(".//*[text()='$0']"): 137 loc.text = '' 138 loc.append(copy.deepcopy(node)) 139 if node.getparent() is None: 140 spec_content = None 141 comment = None 142 for content in spec: 143 if content.tag is not etree.Comment: 144 spec_content = content 145 break 146 else: 147 comment = content 148 source = copy.deepcopy(spec_content) 149 # only keep the t-name of a template root node 150 t_name = node.get('t-name') 151 if t_name: 152 source.set('t-name', t_name) 153 if comment is not None: 154 text = source.text 155 source.text = None 156 comment.tail = text 157 source.insert(0, comment) 158 else: 159 replaced_node_tag = None 160 for child in spec: 161 if child.get('position') == 'move': 162 child = extract(child) 163 if inherit_branding and not replaced_node_tag and child.tag is not etree.Comment: 164 # To make a correct branding, we need to 165 # - know exactly which node has been replaced 166 # - store it before anything else has altered the Tree 167 # Do it exactly here :D 168 child.set('meta-oe-xpath-replacing', node.tag) 169 # We just store the replaced node tag on the first 170 # child of the xpath replacing it 171 replaced_node_tag = node.tag 172 node.addprevious(child) 173 node.getparent().remove(node) 174 elif pos == 'attributes': 175 for child in spec.getiterator('attribute'): 176 attribute = child.get('name') 177 value = child.text or '' 178 if child.get('add') or child.get('remove'): 179 assert not child.text 180 separator = child.get('separator', ',') 181 if separator == ' ': 182 separator = None # squash spaces 183 to_add = ( 184 s for s in (s.strip() for s in child.get('add', '').split(separator)) 185 if s 186 ) 187 to_remove = {s.strip() for s in child.get('remove', '').split(separator)} 188 values = (s.strip() for s in node.get(attribute, '').split(separator)) 189 value = (separator or ' ').join(itertools.chain( 190 (v for v in values if v not in to_remove), 191 to_add 192 )) 193 if value: 194 node.set(attribute, value) 195 elif attribute in node.attrib: 196 del node.attrib[attribute] 197 elif pos == 'inside': 198 add_text_inside(node, spec.text) 199 for child in spec: 200 if child.get('position') == 'move': 201 child = extract(child) 202 node.append(child) 203 elif pos == 'after': 204 # add a sentinel element right after node, insert content of 205 # spec before the sentinel, then remove the sentinel element 206 sentinel = E.sentinel() 207 node.addnext(sentinel) 208 add_text_before(sentinel, spec.text) 209 for child in spec: 210 if child.get('position') == 'move': 211 child = extract(child) 212 sentinel.addprevious(child) 213 remove_element(sentinel) 214 elif pos == 'before': 215 add_text_before(node, spec.text) 216 for child in spec: 217 if child.get('position') == 'move': 218 child = extract(child) 219 node.addprevious(child) 220 else: 221 raise ValueError( 222 _("Invalid position attribute: '%s'") % 223 pos 224 ) 225 226 else: 227 attrs = ''.join([ 228 ' %s="%s"' % (attr, spec.get(attr)) 229 for attr in spec.attrib 230 if attr != 'position' 231 ]) 232 tag = "<%s%s>" % (spec.tag, attrs) 233 raise ValueError( 234 _("Element '%s' cannot be located in parent view", tag) 235 ) 236 237 return source 238