[IMP] web_editor: use inheriting views for oe_structure content

Before this commit, when using the website editor, modifications were
saved in place in the original view. The view was also marked as non
updatable so that when an user migrates its database, he does not lose
its modifications.

The problem with this system is that users were able to keep their
modifications at the cost of having views which are not up-to-date with
the latest odoo updates.

Now, the views' content which is inside an oe_structure element is saved
in an inheriting view instead of the original view. The original
view is not marked as non updatable anymore.

Note 1: we only consider elements with the 'oe_structure' class which
also have an id containing the 'oe_structure' string.

Note 2: if a non oe_structure but editable content is edited, the
original view is still marked as non updatable (however, with the new
multi-website feature, this may not be necessary as the edited view will
be duplicated to be website-specific... we may want to disable the
behavior in a future update).
This commit is contained in:
qsm-odoo
2018-09-30 19:55:17 +02:00
parent 4f15c939a7
commit dd139948f0
3 changed files with 90 additions and 13 deletions
+77 -6
View File
@@ -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):
+6
View File
@@ -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()
+7 -7
View File
@@ -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 = '''<t name="Second View" t-name="website.second_view">
<t t-call="website.layout">
<div id="wrap">
<div class="oe_structure"/>
<div class="editable_part"/>
<div class="container">
<h1>Second View</h1>
</div>
<div class="oe_structure"/>
<div class="editable_part"/>
</div>
</t>
</t>'''
@@ -445,12 +445,12 @@ class TestCowViewSaving(common.TransactionCase):
})
total_views = View.search_count([])
second_view.with_context(website_id=1).save('<div class="oe_structure" data-oe-id="%s" data-oe-xpath="/t[1]/t[1]/div[1]/div[1]" data-oe-field="arch" data-oe-model="ir.ui.view">First oe_structure</div>' % second_view.id, "/t[1]/t[1]/div[1]/div[1]")
second_view.with_context(website_id=1).save('<div class="oe_structure" data-oe-id="%s" data-oe-xpath="/t[1]/t[1]/div[1]/div[3]" data-oe-field="arch" data-oe-model="ir.ui.view">Second oe_structure</div>' % second_view.id, "/t[1]/t[1]/div[1]/div[3]")
second_view.with_context(website_id=1).save('<div class="editable_part" data-oe-id="%s" data-oe-xpath="/t[1]/t[1]/div[1]/div[1]" data-oe-field="arch" data-oe-model="ir.ui.view">First editable_part</div>' % second_view.id, "/t[1]/t[1]/div[1]/div[1]")
second_view.with_context(website_id=1).save('<div class="editable_part" data-oe-id="%s" data-oe-xpath="/t[1]/t[1]/div[1]/div[3]" data-oe-field="arch" data-oe-model="ir.ui.view">Second editable_part</div>' % 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']