[FIX] website: handle COW'd views during module install/update
Since multi-website, we COW (copy on write) views when editing a view on a
website.
This is causing 2 issues related to module install/update:
1. During a module install, when creating an inherited view, it will create
that view as expected on the generic tree.
But that generic tree may not be used on website(s). Indeed, if the view
was edited, we are now using a copy of the view instead of the original
XML view (with an xml_id).
That copied view won't get the new inherited view in its view tree.
Now, we correctly copy the new view under every specific view tree (if
exists).
2. During a module update, when updating a view, it will only update the view
having the xml_id, so the generic one.
But most of the time, the generic view is not used as it was usually COW'd
and the specific view now replace the generic one.
Now, we also apply the update on the COW'd view. Note that only unmodified
field will be updated, this behavior basically mimic the noupdate behavior
on view's ir.model.data.
Step to reproduce for bug 1:
- Install website_sale module
- Go to a product page and then enter edit mode
- Make a modification to trigger COW, eg edit the information area under the
price "30-day money-back guarantee".
- Now install website_sale_comparison module
- The product page won't have the comparison button as the comparison view
inheriting the product view was created on the generic tree and not copied
on the specific tree (COW'd before the comparison module install).
Step to reproduce for bug 2:
- Repeat first 3 steps of bug 1
- Now make a modification in a view from the XML file in
website_sale_comparison module and update the module
- Only the view with the xml_id is updated, not it's COW'd views. This mean
that only the generic view is updated and not the specific one(s) really
used on the website.
task-1920487
closes odoo/odoo#29956
This commit is contained in:
@@ -93,6 +93,55 @@ class View(models.Model):
|
||||
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _get_specific_views(self):
|
||||
""" Given a view, return a record set containing all the specific views
|
||||
for that view's key.
|
||||
If the given view is already specific, it will also return itself.
|
||||
"""
|
||||
self.ensure_one()
|
||||
domain = [('key', '=', self.key), ('website_id', '!=', False)]
|
||||
return self.with_context(active_test=False).search(domain)
|
||||
|
||||
def _load_records_write(self, values):
|
||||
""" During module update, when updating a generic view, we should also
|
||||
update its specific views (COW'd).
|
||||
Note that we will only update unmodified fields. That will mimic the
|
||||
noupdate behavior on views having an ir.model.data.
|
||||
"""
|
||||
if self.type == 'qweb' and not self.website_id:
|
||||
# Update also specific views
|
||||
for cow_view in self._get_specific_views():
|
||||
authorized_vals = {}
|
||||
for key in values:
|
||||
if cow_view[key] == self[key]:
|
||||
authorized_vals[key] = values[key]
|
||||
cow_view.write(authorized_vals)
|
||||
super(View, self)._load_records_write(values)
|
||||
|
||||
def _load_records_create(self, values):
|
||||
""" During module install, when creating a generic child view, we should
|
||||
also create that view under specific view trees (COW'd).
|
||||
Top level view (no inherit_id) do not need that behavior as they
|
||||
will be shared between websites since there is no specific yet.
|
||||
"""
|
||||
records = super(View, self)._load_records_create(values)
|
||||
for record in records:
|
||||
if record.type == 'qweb' and record.inherit_id and not record.website_id and not record.inherit_id.website_id:
|
||||
specific_parent_views = record.with_context(active_test=False).search([
|
||||
('key', '=', record.inherit_id.key),
|
||||
('website_id', '!=', None),
|
||||
])
|
||||
for specific_parent_view in specific_parent_views:
|
||||
record.copy({
|
||||
# Set key to avoid copy() to generate an unique key as
|
||||
# we want the specific view to have the same key
|
||||
'key': record.key,
|
||||
'inherit_id': specific_parent_view.id,
|
||||
'website_id': specific_parent_view.website_id.id,
|
||||
})
|
||||
return records
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
'''This implements COU (copy-on-unlink). When deleting a generic page
|
||||
|
||||
@@ -637,6 +637,89 @@ class TestCowViewSaving(common.TransactionCase):
|
||||
# Next line should not crash, COW loop on inherit_children_ids should be sorted correctly
|
||||
self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'})
|
||||
|
||||
def test_module_new_inherit_view_on_parent_already_forked(self):
|
||||
""" If a generic parent view is copied (COW) and that another module
|
||||
creates a child view for that generic parent, all the COW views
|
||||
should also get a copy of that new child view.
|
||||
|
||||
Typically, a parent view (website_sale.product) is copied (COW)
|
||||
and then wishlist module is installed.
|
||||
Wishlist views inhering from website_sale.product are added to the
|
||||
generic `website_sale.product`. But it should also be added to the
|
||||
COW `website_sale.product` to activate the module views for that
|
||||
website.
|
||||
"""
|
||||
Website = self.env['website']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# Simulate website_sale product view
|
||||
self.base_view.write({'name': 'Product', 'key': '_website_sale.product'})
|
||||
# Trigger cow on website_sale hierarchy for website 1
|
||||
self.base_view.with_context(website_id=1).write({'name': 'Product (W1)'})
|
||||
|
||||
# Simulate website_sale_comparison install
|
||||
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
|
||||
'name': 'Add to comparison in product page',
|
||||
'mode': 'extension',
|
||||
'inherit_id': self.base_view.id,
|
||||
'arch': '<div position="replace"><p>COMPARE</p></div>',
|
||||
'key': '_website_sale_comparison.product_add_to_compare',
|
||||
})])
|
||||
|
||||
specific_view = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale.product')
|
||||
specific_view_arch = specific_view.read_combined(['arch'])['arch']
|
||||
self.assertEqual(specific_view.website_id.id, 1, "Ensure we got specific view to perform the checks against")
|
||||
self.assertEqual(specific_view_arch, '<p>COMPARE</p>', "When a module creates an inherited view (on a generic tree), it should also create that view in the specific COW'd tree.")
|
||||
|
||||
# Simulate website_sale_comparison update
|
||||
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
|
||||
'arch': '<div position="replace"><p>COMPARE EDITED</p></div>',
|
||||
})])
|
||||
|
||||
specific_view_arch = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale.product').read_combined(['arch'])['arch']
|
||||
self.assertEqual(specific_view_arch, '<p>COMPARE EDITED</p>', "When a module updates an inherited view (on a generic tree), it should also update the copies of that view (COW).")
|
||||
|
||||
# Test fields that should not be COW'd
|
||||
random_view_id = View.search([], limit=1).id
|
||||
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
|
||||
'website_id': None,
|
||||
'inherit_id': random_view_id,
|
||||
})])
|
||||
|
||||
w1_specific_child_view = Website.with_context(load_all_views=True, website_id=1).viewref('_website_sale_comparison.product_add_to_compare')
|
||||
generic_child_view = Website.with_context(load_all_views=True).viewref('_website_sale_comparison.product_add_to_compare')
|
||||
self.assertEqual(w1_specific_child_view.website_id.id, 1, "website_id is a prohibited field when COWing views during _load_records")
|
||||
self.assertEqual(w1_specific_child_view.inherit_id.id != random_view_id, True, "inherit_id is a prohibited field when COWing views during _load_records")
|
||||
self.assertEqual(generic_child_view.inherit_id.id, random_view_id, "prohibited fields only concerned write on COW'd view. Generic should still considere these fields")
|
||||
|
||||
# Set back the generic view as parent for the rest of the test
|
||||
generic_child_view.inherit_id = self.base_view
|
||||
|
||||
# Don't update fields from COW'd view if these fields have been modified from original view
|
||||
new_website = Website.create({'name': 'New Website'})
|
||||
self.base_view.with_context(website_id=new_website.id).write({'name': 'Product (new_website)'})
|
||||
new_website_specific_child_view = Website.with_context(load_all_views=True, website_id=new_website.id).viewref('_website_sale_comparison.product_add_to_compare')
|
||||
new_website_specific_child_view.priority = 6
|
||||
View._load_records([dict(xml_id='_website_sale_comparison.product_add_to_compare', values={
|
||||
'priority': 3,
|
||||
})])
|
||||
self.assertEqual(generic_child_view.priority, 3, "XML update should be written on the Generic View")
|
||||
self.assertEqual(w1_specific_child_view.priority, 3, "XML update should be written on the specific view if the fields have not been modified on that specific view")
|
||||
self.assertEqual(new_website_specific_child_view.priority, 6, "XML update should NOT be written on the specific view if the fields have been modified on that specific view")
|
||||
|
||||
# Simulate website_sale update on top level view
|
||||
self.env['ir.model.data'].create({
|
||||
'module': '_website_sale',
|
||||
'name': 'product',
|
||||
'model': self.base_view._name,
|
||||
'res_id': self.base_view.id,
|
||||
})
|
||||
View._load_records([dict(xml_id='_website_sale.product', values={
|
||||
'website_meta_title': 'A bug got fixed by updating this field',
|
||||
})])
|
||||
all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
|
||||
self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
|
||||
|
||||
|
||||
class Crawler(HttpCase):
|
||||
def setUp(self):
|
||||
|
||||
+8
-2
@@ -3728,6 +3728,12 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
modified_ids = {row[0] for row in cr.fetchall()}
|
||||
self.browse(modified_ids).modified(['parent_path'])
|
||||
|
||||
def _load_records_write(self, values):
|
||||
self.write(values)
|
||||
|
||||
def _load_records_create(self, values):
|
||||
return self.create(values)
|
||||
|
||||
def _load_records(self, data_list, update=False):
|
||||
""" Create or update records of this model, and assign XMLIDs.
|
||||
|
||||
@@ -3785,7 +3791,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
|
||||
# update existing records
|
||||
for data in to_update:
|
||||
data['record'].write(data['values'])
|
||||
data['record']._load_records_write(data['values'])
|
||||
|
||||
# determine existing parents for new records
|
||||
for parent_model, parent_field in self._inherits.items():
|
||||
@@ -3812,7 +3818,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
_logger.warning("Creating record %s in module %s.", data['xml_id'], module)
|
||||
|
||||
# create records
|
||||
records = self.create([data['values'] for data in to_create])
|
||||
records = self._load_records_create([data['values'] for data in to_create])
|
||||
for data, record in pycompat.izip(to_create, records):
|
||||
data['record'] = record
|
||||
|
||||
|
||||
Reference in New Issue
Block a user