From 919185f9eb8e336d8ede3fd1855b8cca69dcdd50 Mon Sep 17 00:00:00 2001 From: Christophe Simonis <chs@odoo.com> Date: Fri, 8 Nov 2019 09:43:12 +0000 Subject: [PATCH] [FIX] base: view `arch` is language dependent Cache should be invalidated when updating `arch_db` via `arch`. closes odoo/odoo#50512 X-original-commit: 1a14ca163ee256eb26661c900227db49426ec3af Signed-off-by: Raphael Collet (rco) <rco@openerp.com> --- odoo/addons/base/models/ir_ui_view.py | 8 +- odoo/addons/base/tests/test_views.py | 125 ++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 15d98396d04f..892e317d1ead 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -256,7 +256,7 @@ actual arch. """) @api.depends('arch_db', 'arch_fs', 'arch_updated') - @api.depends_context('read_arch_from_file') + @api.depends_context('read_arch_from_file', 'lang') def _compute_arch(self): def resolve_external_ids(arch_fs, view_xml_id): def replacer(m): @@ -294,6 +294,10 @@ actual arch. data['arch_fs'] = '/'.join(path_info[0:2]) data['arch_updated'] = False view.write(data) + # the field 'arch' depends on the context and has been implicitly + # modified in all languages; the invalidation below ensures that the + # field does not keep an old value in another environment + self.invalidate_cache(['arch'], self._ids) @api.depends('arch') @api.depends_context('read_arch_from_file') @@ -371,8 +375,6 @@ actual arch. # Sanity checks: the view should not break anything upon rendering! # Any exception raised below will cause a transaction rollback. for view in self: - if not view.arch: - continue try: view_arch = etree.fromstring(view.arch.encode('utf-8')) view._valid_inheritance(view_arch) diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 10370071f4de..599eb14387b4 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -2279,6 +2279,131 @@ class TestViews(ViewCase): self.assertIn(expected_message, message) +class TestViewTranslations(common.SavepointCase): + # these tests are essentially the same as in test_translate.py, but they use + # the computed field 'arch' instead of the translated field 'arch_db' + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env['res.lang']._activate_lang('fr_FR') + cls.env['res.lang']._activate_lang('nl_NL') + cls.env['ir.translation']._load_module_terms(['base'], ['fr_FR', 'nl_NL']) + + def create_view(self, archf, terms, **kwargs): + view = self.env['ir.ui.view'].create({ + 'name': 'test', + 'model': 'res.partner', + 'arch': archf % terms, + }) + # DLE P70: `_sync_terms_translations`, which delete translations for which there is no value, is called sooner than before + # because it's called in `_write`, which is called by `flush`, which is called by the `search`. + # `arch_db` is in `_write` instead of `create` because `arch_db` is the inverse of `arch`. + # We need to flush `arch_db` before creating the translations otherwise the translation for which there is no value will be deleted, + # while the `test_sync_update` specifically needs empty translations + view.flush() + self.env['ir.translation'].create([ + { + 'type': 'model_terms', + 'name': 'ir.ui.view,arch_db', + 'lang': lang, + 'res_id': view.id, + 'src': src, + 'value': val, + 'state': 'translated', + } + for lang, trans_terms in kwargs.items() + for src, val in zip(terms, trans_terms) + ]) + return view + + def test_sync(self): + """ Check translations of 'arch' after minor change in source terms. """ + archf = '<form string="X">%s</form>' + terms_en = ('Bread and cheeze',) + terms_fr = ('Pain et fromage',) + terms_nl = ('Brood and kaas',) + view = self.create_view(archf, terms_en, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl) + + env_nolang = self.env(context={}) + env_en = self.env(context={'lang': 'en_US'}) + env_fr = self.env(context={'lang': 'fr_FR'}) + env_nl = self.env(context={'lang': 'nl_NL'}) + + self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en) + self.assertEqual(view.with_env(env_en).arch, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr) + self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl) + + # modify source term in view (fixed type in 'cheeze') + terms_en = ('Bread and cheese',) + view.with_env(env_en).write({'arch': archf % terms_en}) + + # check whether translations have been synchronized + self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en) + self.assertEqual(view.with_env(env_en).arch, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr) + self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl) + + view = self.create_view(archf, terms_fr, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl) + # modify source term in view in another language with close term + new_terms_fr = ('Pains et fromage',) + view.with_env(env_fr).write({'arch': archf % new_terms_fr}) + + # check whether translations have been synchronized + self.assertEqual(view.with_env(env_nolang).arch, archf % new_terms_fr) + self.assertEqual(view.with_env(env_en).arch, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch, archf % new_terms_fr) + self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl) + + def test_sync_update(self): + """ Check translations after major changes in source terms. """ + archf = '<form string="X"><div>%s</div><div>%s</div></form>' + terms_src = ('Subtotal', 'Subtotal:') + terms_en = ('', 'Sub total:') + view = self.create_view(archf, terms_src, en_US=terms_en) + + translations = self.env['ir.translation'].search([ + ('type', '=', 'model_terms'), + ('name', '=', "ir.ui.view,arch_db"), + ('res_id', '=', view.id), + ]) + self.assertEqual(len(translations), 2) + + # modifying the arch should sync existing translations without errors + new_arch = archf % ('Subtotal', 'Subtotal:<br/>') + view.write({"arch": new_arch}) + self.assertEqual(view.arch, new_arch) + + translations = self.env['ir.translation'].search([ + ('type', '=', 'model_terms'), + ('name', '=', "ir.ui.view,arch_db"), + ('res_id', '=', view.id), + ]) + # 'Subtotal' being src==value, it will be discared + # 'Subtotal:' will be discarded as it match 'Subtotal' instead of 'Subtotal:<br/>' + self.assertEqual(len(translations), 0) + + def test_cache_consistency(self): + view = self.env["ir.ui.view"].create({ + "name": "test_translate_xml_cache_invalidation", + "model": "res.partner", + "arch": "<form><b>content</b></form>", + }) + view_fr = view.with_context({"lang": "fr_FR"}) + self.assertIn("<b>", view.arch_db) + self.assertIn("<b>", view.arch) + self.assertIn("<b>", view_fr.arch_db) + self.assertIn("<b>", view_fr.arch) + + # write with no lang, and check consistency in other languages + view.write({"arch": "<form><i>content</i></form>"}) + self.assertIn("<i>", view.arch_db) + self.assertIn("<i>", view.arch) + self.assertIn("<i>", view_fr.arch_db) + self.assertIn("<i>", view_fr.arch) + + class ViewModeField(ViewCase): """ This should probably, eventually, be folded back into other test case -- GitLab