gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

335 line
14KB

  1. import copy
  2. import itertools
  3. import logging
  4. import re
  5. from lxml import etree
  6. from lxml.builder import E
  7. from odoo.tools.translate import LazyTranslate
  8. from odoo.exceptions import ValidationError
  9. from .misc import SKIPPED_ELEMENT_TYPES, html_escape
  10. __all__ = []
  11. _lt = LazyTranslate('base')
  12. _logger = logging.getLogger(__name__)
  13. RSTRIP_REGEXP = re.compile(r'\n[ \t]*$')
  14. # attribute names that contain Python expressions
  15. PYTHON_ATTRIBUTES = {'readonly', 'required', 'invisible', 'column_invisible', 't-if', 't-elif'}
  16. def add_stripped_items_before(node, spec, extract):
  17. text = spec.text or ''
  18. before_text = ''
  19. prev = node.getprevious()
  20. if prev is None:
  21. parent = node.getparent()
  22. result = parent.text and RSTRIP_REGEXP.search(parent.text)
  23. before_text = result.group(0) if result else ''
  24. fallback_text = None if spec.text is None else ''
  25. parent.text = ((parent.text or '').rstrip() + text) or fallback_text
  26. else:
  27. result = prev.tail and RSTRIP_REGEXP.search(prev.tail)
  28. before_text = result.group(0) if result else ''
  29. prev.tail = (prev.tail or '').rstrip() + text
  30. if len(spec) > 0:
  31. spec[-1].tail = (spec[-1].tail or "").rstrip() + before_text
  32. else:
  33. spec.text = (spec.text or "").rstrip() + before_text
  34. for child in spec:
  35. if child.get('position') == 'move':
  36. tail = child.tail
  37. child = extract(child)
  38. child.tail = tail
  39. node.addprevious(child)
  40. def add_text_before(node, text):
  41. """ Add text before ``node`` in its XML tree. """
  42. if text is None:
  43. return
  44. prev = node.getprevious()
  45. if prev is not None:
  46. prev.tail = (prev.tail or "") + text
  47. else:
  48. parent = node.getparent()
  49. parent.text = (parent.text or "").rstrip() + text
  50. def remove_element(node):
  51. """ Remove ``node`` but not its tail, from its XML tree. """
  52. add_text_before(node, node.tail)
  53. node.tail = None
  54. node.getparent().remove(node)
  55. def locate_node(arch, spec):
  56. """ Locate a node in a source (parent) architecture.
  57. Given a complete source (parent) architecture (i.e. the field
  58. `arch` in a view), and a 'spec' node (a node in an inheriting
  59. view that specifies the location in the source view of what
  60. should be changed), return (if it exists) the node in the
  61. source view matching the specification.
  62. :param arch: a parent architecture to modify
  63. :param spec: a modifying node in an inheriting view
  64. :return: a node in the source matching the spec
  65. """
  66. if spec.tag == 'xpath':
  67. expr = spec.get('expr')
  68. try:
  69. xPath = etree.ETXPath(expr)
  70. except etree.XPathSyntaxError as e:
  71. raise ValidationError(_lt("Invalid Expression while parsing xpath “%s”", expr)) from e
  72. nodes = xPath(arch)
  73. return nodes[0] if nodes else None
  74. elif spec.tag == 'field':
  75. # Only compare the field name: a field can be only once in a given view
  76. # at a given level (and for multilevel expressions, we should use xpath
  77. # inheritance spec anyway).
  78. for node in arch.iter('field'):
  79. if node.get('name') == spec.get('name'):
  80. return node
  81. return None
  82. for node in arch.iter(spec.tag):
  83. if all(node.get(attr) == spec.get(attr) for attr in spec.attrib if attr != 'position'):
  84. return node
  85. return None
  86. def apply_inheritance_specs(source, specs_tree, inherit_branding=False, pre_locate=lambda s: True):
  87. """ Apply an inheriting view (a descendant of the base view)
  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. :param Element source: a parent architecture to modify
  92. :param Element specs_tree: a modifying architecture in an inheriting view
  93. :param bool inherit_branding:
  94. :param pre_locate: function that is executed before locating a node.
  95. This function receives an arch as argument.
  96. This is required by studio to properly handle group_ids.
  97. :return: a modified source where the specs are applied
  98. :rtype: Element
  99. """
  100. # Queue of specification nodes (i.e. nodes describing where and
  101. # changes to apply to some parent architecture).
  102. specs = specs_tree if isinstance(specs_tree, list) else [specs_tree]
  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. _lt("Invalid specification for moved nodes: “%s”", etree.tostring(spec, encoding='unicode'))
  111. )
  112. pre_locate(spec)
  113. to_extract = locate_node(source, spec)
  114. if to_extract is not None:
  115. remove_element(to_extract)
  116. return to_extract
  117. else:
  118. raise ValueError(
  119. _lt("Element “%s” cannot be located in parent view", etree.tostring(spec, encoding='unicode'))
  120. )
  121. while len(specs):
  122. spec = specs.pop(0)
  123. if isinstance(spec, SKIPPED_ELEMENT_TYPES):
  124. continue
  125. if spec.tag == 'data':
  126. specs += [c for c in spec]
  127. continue
  128. pre_locate(spec)
  129. node = locate_node(source, spec)
  130. if node is not None:
  131. pos = spec.get('position', 'inside')
  132. if pos == 'replace':
  133. mode = spec.get('mode', 'outer')
  134. if mode == "outer":
  135. for loc in spec.xpath(".//*[text()='$0']"):
  136. loc.text = ''
  137. copied_node = copy.deepcopy(node)
  138. # TODO: Remove 'inherit_branding' logic if possible;
  139. # currently needed to track node removal for branding
  140. # distribution. Avoid marking root nodes to prevent
  141. # sibling branding issues.
  142. if inherit_branding:
  143. copied_node.set('data-oe-no-branding', '1')
  144. loc.append(copied_node)
  145. if node.getparent() is None:
  146. spec_content = None
  147. comment = None
  148. for content in spec:
  149. if content.tag is not etree.Comment:
  150. spec_content = content
  151. break
  152. else:
  153. comment = content
  154. source = copy.deepcopy(spec_content)
  155. # only keep the t-name of a template root node
  156. t_name = node.get('t-name')
  157. if t_name:
  158. source.set('t-name', t_name)
  159. if comment is not None:
  160. text = source.text
  161. source.text = None
  162. comment.tail = text
  163. source.insert(0, comment)
  164. else:
  165. # TODO ideally the notion of 'inherit_branding' should
  166. # not exist in this function. Given the current state of
  167. # the code, it is however necessary to know where nodes
  168. # were removed when distributing branding. As a stable
  169. # fix, this solution was chosen: the location is marked
  170. # with a "ProcessingInstruction" which will not impact
  171. # the "Element" structure of the resulting tree.
  172. # Exception: if we happen to replace a node that already
  173. # has xpath branding (root level nodes), do not mark the
  174. # location of the removal as it will mess up the branding
  175. # of siblings elements coming from other views, after the
  176. # branding is distributed (and those processing instructions
  177. # removed).
  178. if inherit_branding and not node.get('data-oe-xpath'):
  179. node.addprevious(etree.ProcessingInstruction('apply-inheritance-specs-node-removal', node.tag))
  180. for child in spec:
  181. if child.get('position') == 'move':
  182. child = extract(child)
  183. node.addprevious(child)
  184. node.getparent().remove(node)
  185. elif mode == "inner":
  186. # Replace the entire content of an element
  187. for child in node:
  188. node.remove(child)
  189. node.text = None
  190. for child in spec:
  191. node.append(copy.deepcopy(child))
  192. node.text = spec.text
  193. else:
  194. raise ValueError(_lt("Invalid mode attribute: “%s”", mode))
  195. elif pos == 'attributes':
  196. for child in spec.getiterator('attribute'):
  197. # The element should only have attributes:
  198. # - name (mandatory),
  199. # - add, remove, separator
  200. # - any attribute that starts with data-oe-*
  201. unknown = [
  202. key
  203. for key in child.attrib
  204. if key not in ('name', 'add', 'remove', 'separator')
  205. and not key.startswith('data-oe-')
  206. ]
  207. if unknown:
  208. raise ValueError(_lt(
  209. "Invalid attributes %s in element <attribute>",
  210. ", ".join(map(repr, unknown)),
  211. ))
  212. attribute = child.get('name')
  213. value = None
  214. if child.get('add') or child.get('remove'):
  215. if child.text:
  216. raise ValueError(_lt(
  217. "Element <attribute> with 'add' or 'remove' cannot contain text %s",
  218. repr(child.text),
  219. ))
  220. value = node.get(attribute, '')
  221. add = child.get('add', '')
  222. remove = child.get('remove', '')
  223. separator = child.get('separator')
  224. if attribute in PYTHON_ATTRIBUTES or attribute.startswith('decoration-'):
  225. # attribute containing a python expression
  226. separator = separator.strip()
  227. if separator not in ('and', 'or'):
  228. raise ValueError(_lt(
  229. "Invalid separator %(separator)s for python expression %(expression)s; "
  230. "valid values are 'and' and 'or'",
  231. separator=repr(separator), expression=repr(attribute),
  232. ))
  233. if remove:
  234. if re.match(rf'^\(*{remove}\)*$', value):
  235. value = ''
  236. else:
  237. patterns = [
  238. f"({remove}) {separator} ",
  239. f" {separator} ({remove})",
  240. f"{remove} {separator} ",
  241. f" {separator} {remove}",
  242. ]
  243. for pattern in patterns:
  244. index = value.find(pattern)
  245. if index != -1:
  246. value = value[:index] + value[index + len(pattern):]
  247. break
  248. if add:
  249. value = f"({value}) {separator} ({add})" if value else add
  250. else:
  251. if separator is None:
  252. separator = ','
  253. elif separator == ' ':
  254. separator = None # squash spaces
  255. values = (s.strip() for s in value.split(separator))
  256. to_add = filter(None, (s.strip() for s in add.split(separator)))
  257. to_remove = {s.strip() for s in remove.split(separator)}
  258. value = (separator or ' ').join(itertools.chain(
  259. (v for v in values if v and v not in to_remove),
  260. to_add
  261. ))
  262. else:
  263. value = child.text or ''
  264. if value:
  265. node.set(attribute, value)
  266. elif attribute in node.attrib:
  267. del node.attrib[attribute]
  268. elif pos == 'inside':
  269. # add a sentinel element at the end, insert content of spec
  270. # before the sentinel, then remove the sentinel element
  271. sentinel = E.sentinel()
  272. node.append(sentinel)
  273. add_stripped_items_before(sentinel, spec, extract)
  274. remove_element(sentinel)
  275. elif pos == 'after':
  276. # add a sentinel element right after node, insert content of
  277. # spec before the sentinel, then remove the sentinel element
  278. sentinel = E.sentinel()
  279. node.addnext(sentinel)
  280. if node.tail is not None: # for lxml >= 5.1
  281. sentinel.tail = node.tail
  282. node.tail = None
  283. add_stripped_items_before(sentinel, spec, extract)
  284. remove_element(sentinel)
  285. elif pos == 'before':
  286. add_stripped_items_before(node, spec, extract)
  287. else:
  288. raise ValueError(_lt("Invalid position attribute: '%s'", pos))
  289. else:
  290. attrs = ''.join([
  291. ' %s="%s"' % (attr, html_escape(spec.get(attr)))
  292. for attr in spec.attrib
  293. if attr != 'position'
  294. ])
  295. tag = "<%s%s>" % (spec.tag, attrs)
  296. raise ValueError(
  297. _lt("Element '%s' cannot be located in parent view", tag)
  298. )
  299. return source
上海开阖软件有限公司 沪ICP备12045867号-1