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

-
+
''' @@ -445,12 +445,12 @@ class TestCowViewSaving(common.TransactionCase): }) total_views = View.search_count([]) - second_view.with_context(website_id=1).save('
First oe_structure
' % second_view.id, "/t[1]/t[1]/div[1]/div[1]") - second_view.with_context(website_id=1).save('
Second oe_structure
' % second_view.id, "/t[1]/t[1]/div[1]/div[3]") + second_view.with_context(website_id=1).save('
First editable_part
' % second_view.id, "/t[1]/t[1]/div[1]/div[1]") + second_view.with_context(website_id=1).save('
Second editable_part
' % second_view.id, "/t[1]/t[1]/div[1]/div[3]") self.assertEqual(total_views + 1, View.search_count([]), "Second save should have wrote on the view copied during first save") - total_specific_view = View.search_count([('arch_db', 'like', 'First oe_structure'), ('arch_db', 'like', 'Second oe_structure')]) - self.assertEqual(total_specific_view, 1, "both oe_structure should have been replaced on a created specific view") + total_specific_view = View.search_count([('arch_db', 'like', 'First editable_part'), ('arch_db', 'like', 'Second editable_part')]) + self.assertEqual(total_specific_view, 1, "both editable_part should have been replaced on a created specific view") def test_cow_complete_flow(self): View = self.env['ir.ui.view']