diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 12229c50a0a..e8ff6cd21ce 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -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 diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 7a0cf23bd55..9ece6cf4c3d 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -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': '

COMPARE

', + '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, '

COMPARE

', "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': '

COMPARE EDITED

', + })]) + + 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, '

COMPARE EDITED

', "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): diff --git a/odoo/models.py b/odoo/models.py index 96c6d5c3d76..2c3b475d31e 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -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