diff --git a/src/base/tests/test_util.py b/src/base/tests/test_util.py index 97d00dc8c..84e96e08e 100644 --- a/src/base/tests/test_util.py +++ b/src/base/tests/test_util.py @@ -1825,3 +1825,101 @@ def test_convert_field_to_html(self): self.assertEqual(default.json_value, '"

Test text

"') self.assertEqual(partner.x_testx, "

test partner field

") + + +class TestRemoveView(UnitTestCase): + def test_remove_view(self): + test_view_1 = self.env["ir.ui.view"].create( + { + "name": "test_view_1", + "type": "qweb", + "key": "base.test_view_1", + "arch": """ + +
Test View 1 Content
+
+ """, + } + ) + self.env["ir.model.data"].create( + {"name": "test_view_1", "module": "base", "model": "ir.ui.view", "res_id": test_view_1.id} + ) + test_view_2 = self.env["ir.ui.view"].create( + { + "name": "test_view_2", + "type": "qweb", + "key": "base.test_view_2", + "arch": """ + + +
Test View 2 Content
+
+ """, + } + ) + test_view_3 = self.env["ir.ui.view"].create( + { + "name": "test_view_3", + "type": "qweb", + "key": "base.test_view_3", + "arch": """ + + + + + """, + } + ) + self.env["ir.model.data"].create( + {"name": "test_view_3", "module": "base", "model": "ir.ui.view", "res_id": test_view_3.id} + ) + + # call by xml_id + util.remove_view(self.env.cr, xml_id="base.test_view_1") + util.invalidate(test_view_2) + util.invalidate(test_view_3) + self.assertFalse(test_view_1.exists()) + self.assertNotIn('t-call="base.test_view_1"', test_view_2.arch_db) + self.assertNotIn('t-call="base.test_view_1"', test_view_3.arch_db) + + # call by view_id + util.remove_view(self.env.cr, view_id=test_view_2.id) + util.invalidate(test_view_3) + self.assertFalse(test_view_2.exists()) + self.assertNotIn('t-call="base.test_view_2"', test_view_3.arch_db) + + +class TestRenameXMLID(UnitTestCase): + def test_rename_xmlid(self): + test_view_1 = self.env["ir.ui.view"].create( + { + "name": "test_view_1", + "type": "qweb", + "key": "base.test_view_1", + "arch": """ + +
Test View 1 Content
+
+ """, + } + ) + self.env["ir.model.data"].create( + {"name": "test_view_1", "module": "base", "model": "ir.ui.view", "res_id": test_view_1.id} + ) + test_view_2 = self.env["ir.ui.view"].create( + { + "name": "test_view_2", + "type": "qweb", + "key": "base.test_view_2", + "arch": """ + + +
Test View 2 Content
+
+ """, + } + ) + util.rename_xmlid(self.env.cr, "base.test_view_1", "base.rename_view") + util.invalidate(test_view_2) + self.assertIn('t-call="base.rename_view"', test_view_2.arch_db) + self.assertIn('t-name="base.rename_view"', test_view_1.arch_db) diff --git a/src/util/records.py b/src/util/records.py index cc4474cbc..a0691c79f 100644 --- a/src/util/records.py +++ b/src/util/records.py @@ -12,12 +12,12 @@ from psycopg2.extras import Json, execute_values try: - from odoo import release + from odoo import modules, release from odoo.tools.convert import xml_import from odoo.tools.misc import file_open from odoo.tools.translate import xml_translate except ImportError: - from openerp import release + from openerp import modules, release from openerp.tools.convert import xml_import from openerp.tools.misc import file_open @@ -106,6 +106,16 @@ def remove_view(cr, xml_id=None, view_id=None, silent=False, key=None): for [v_id] in cr.fetchall(): remove_view(cr, view_id=v_id, silent=silent, key=xml_id) + if not key and column_exists(cr, "ir_ui_view", "key"): + cr.execute("SELECT key FROM ir_ui_view WHERE id = %s and key != %s", [view_id, xml_id]) + [key] = cr.fetchone() or [None] + + # Occurrences of xml_id and key in the t-call of views are to be found and removed. + if xml_id != "?": + _remove_redundant_tcalls(cr, xml_id) + if key and key != xml_id: + _remove_redundant_tcalls(cr, key) + if not view_id: return @@ -794,6 +804,34 @@ def rename_xmlid(cr, old, new, noupdate=None, on_collision="fail"): # Don't change the view keys unconditionally to avoid changing unrelated views. cr.execute("UPDATE ir_ui_view SET key = %s WHERE key = %s", [new, old]) + # Adapting t-call and t-name references in views + search_pattern = r"""\yt-(call|name)=(["']){}\2""".format(re.escape(old)) + replace_pattern = r"t-\1=\2{}\2".format(new) + if version_gte("16.0"): + arch_col = get_value_or_en_translation(cr, "ir_ui_view", "arch_db") + replace_in_all_jsonb_values( + cr, + "ir_ui_view", + "arch_db", + PGRegexp(search_pattern), + replace_pattern, + extra_filter=cr.mogrify("{} ~ %s".format(arch_col), [search_pattern]).decode(), + ) + else: + arch_col = "arch_db" if column_exists(cr, "ir_ui_view", "arch_db") else "arch" + cr.execute( + format_query( + cr, + """ + UPDATE ir_ui_view + SET {arch} = regexp_replace({arch}, %s, %s, 'g') + WHERE {arch} ~ %s + """, + arch=arch_col, + ), + [search_pattern, replace_pattern, search_pattern], + ) + if model == "ir.ui.menu" and column_exists(cr, "res_users_settings", "homemenu_config"): query = """ UPDATE res_users_settings @@ -1784,3 +1822,47 @@ def remove_act_window_view_mode(cr, model, view_mode): """, [view_mode, default, model, view_mode, view_mode], ) + + +def _remove_redundant_tcalls(cr, match): + """ + Remove t-calls of the removed view. + + This function removes the t-calls to `match`. + + :param str match: t-calls value to remove, typically it would be a view's xml_id or key + """ + arch_col = ( + get_value_or_en_translation(cr, "ir_ui_view", "arch_db") + if column_exists(cr, "ir_ui_view", "arch_db") + else "arch" + ) + cr.execute( + format_query( + cr, + """ + SELECT iv.id, + imd.module, + imd.name + FROM ir_ui_view iv + LEFT JOIN ir_model_data imd + ON iv.id = imd.res_id + AND imd.model = 'ir.ui.view' + WHERE {} ~ %s + """, + sql.SQL(arch_col), + ), + [r"""\yt-call=(["']){}\1""".format(re.escape(match))], + ) + standard_modules = set(modules.get_modules()) - {"studio_customization"} + for vid, module, name in cr.fetchall(): + with edit_view(cr, view_id=vid) as arch: + for node in arch.findall(".//t[@t-call='{}']".format(match)): + node.getparent().remove(node) + if not module or module not in standard_modules: + _logger.info( + "The view %swith ID: %s has been updated, removed t-calls to deprecated %r", + ("`{}.{}` ".format(module, name) if module else ""), + vid, + match, + )