diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index ae698ba1c23..c271a35cb1e 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -34,6 +34,10 @@ class IrUiView(models.Model):
def extract_embedded_fields(self, arch):
return arch.xpath('//*[@data-oe-model != "ir.ui.view"]')
+ @api.model
+ def extract_oe_structures(self, arch):
+ return arch.xpath('//*[hasclass("oe_structure")][contains(@id, "oe_structure")]')
+
@api.model
def get_default_lang_code(self):
return False
@@ -54,6 +58,39 @@ class IrUiView(models.Model):
else:
Model.browse(int(el.get('data-oe-id'))).write({field: value})
+ @api.multi
+ def save_oe_structure(self, el):
+ self.ensure_one()
+
+ if el.get('id') in self.key:
+ # Do not inherit if the oe_structure already has its own inheriting view
+ return False
+
+ arch = etree.Element('data')
+ xpath = etree.Element('xpath', expr="//*[hasclass('oe_structure')][@id='{}']".format(el.get('id')), position="replace")
+ arch.append(xpath)
+ structure = etree.Element(el.tag, attrib=el.attrib)
+ xpath.append(structure)
+ for child in el.iterchildren(tag=etree.Element):
+ structure.append(copy.deepcopy(child))
+
+ vals = {
+ 'inherit_id': self.id,
+ 'name': '%s (%s)' % (self.name, el.get('id')),
+ 'arch': self._pretty_arch(arch),
+ 'key': '%s_%s' % (self.key, el.get('id')),
+ 'type': 'qweb',
+ }
+ vals.update(self._save_oe_structure_hook())
+ self.create(vals)
+
+ return True
+
+ @api.model
+ def _save_oe_structure_hook(self):
+ return {}
+
+ @api.model
def _pretty_arch(self, arch):
# remove_blank_string does not seem to work on HTMLParser, and
# pretty-printing with lxml more or less requires stripping
@@ -66,8 +103,24 @@ class IrUiView(models.Model):
return etree.tostring(
arch_no_whitespace, encoding='unicode', pretty_print=True)
+ @api.model
+ def _are_archs_equal(self, arch1, arch2):
+ # Note that comparing the strings would not be ok as attributes order
+ # must not be relevant
+ if arch1.tag != arch2.tag:
+ return False
+ if arch1.text != arch2.text:
+ return False
+ if arch1.tail != arch2.tail:
+ return False
+ if arch1.attrib != arch2.attrib:
+ return False
+ if len(arch1) != len(arch2):
+ return False
+ return all(self._are_archs_equal(arch1, arch2) for arch1, arch2 in pycompat.izip(arch1, arch2))
+
@api.multi
- def replace_arch_section(self, section_xpath, replacement):
+ def replace_arch_section(self, section_xpath, replacement, replace_tail=False):
# the root of the arch section shouldn't actually be replaced as it's
# not really editable itself, only the content truly is editable.
self.ensure_one()
@@ -80,7 +133,9 @@ class IrUiView(models.Model):
[root] = arch.xpath(section_xpath)
root.text = replacement.text
- root.tail = replacement.tail
+ # Note: after a standard edition, the tail *must not* be replaced
+ if replace_tail:
+ root.tail = replacement.tail
# replace all children
del root[:]
for child in replacement:
@@ -99,6 +154,12 @@ class IrUiView(models.Model):
out.tail = el.tail
return out
+ @api.model
+ def to_empty_oe_structure(self, el):
+ out = html.html_parser.makeelement(el.tag, attrib=el.attrib)
+ out.tail = el.tail
+ return out
+
@api.multi
def save(self, value, xpath=None):
""" Update a view section. The view section may embed fields to write
@@ -106,6 +167,7 @@ class IrUiView(models.Model):
:param str xpath: valid xpath to the tag to replace
"""
self.ensure_one()
+
arch_section = html.fromstring(
value, parser=html.HTMLParser(encoding='utf-8'))
@@ -120,11 +182,20 @@ class IrUiView(models.Model):
# transform embedded field back to t-field
el.getparent().replace(el, self.to_field_ref(el))
- for view in self:
- arch = view.replace_arch_section(xpath, arch_section)
- view.write({'arch': view._pretty_arch(arch)})
+ for el in self.extract_oe_structures(arch_section):
+ if self.save_oe_structure(el):
+ # empty oe_structure in parent view
+ empty = self.to_empty_oe_structure(el)
+ if el == arch_section:
+ arch_section = empty
+ else:
+ el.getparent().replace(el, empty)
- self.sudo().mapped('model_data_id').write({'noupdate': True})
+ new_arch = self.replace_arch_section(xpath, arch_section)
+ old_arch = etree.fromstring(self.arch.encode('utf-8'))
+ if not self._are_archs_equal(old_arch, new_arch):
+ self.sudo().model_data_id.write({'noupdate': True}) # TODO check if we remove this
+ self.write({'arch': self._pretty_arch(new_arch)})
@api.model
def _view_obj(self, view_id):
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 6a8856f269a..add519f67cc 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -292,6 +292,12 @@ class View(models.Model):
def _read_template_keys(self):
return super(View, self)._read_template_keys() + ['website_id']
+ @api.model
+ def _save_oe_structure_hook(self):
+ res = super(View, self)._save_oe_structure_hook()
+ res['website_id'] = self.env['website'].get_current_website().id
+ return res
+
@api.multi
def save(self, value, xpath=None):
self.ensure_one()
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index 4b3759cdfb6..b16740ad619 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -410,7 +410,7 @@ class TestCowViewSaving(common.TransactionCase):
def test_cow_generic_view_with_already_existing_specific(self):
""" Writing on a generic view should check if a website specific view already exists
- (The flow of this test will happen when editing a generic view in the front end and changing more than one oe_structure)
+ (The flow of this test will happen when editing a generic view in the front end and changing more than one element)
"""
# 1. Test with calling write directly
View = self.env['ir.ui.view']
@@ -430,11 +430,11 @@ class TestCowViewSaving(common.TransactionCase):
view_arch = '''Second View