[FIX] core: fix cached translations

before this commit:
translations updated by `update_field_translation` api cannot be detected by
t-cache and some cached data whose model overrides `write` with an extra
'clear_caches()'

Step to reproduce:
- Create a mega menu, select any template, `Odoo Menu` for the example
- Install another language on the website
- Go to the translated version of your website and enter translate mode
- Change "Camera" in the mega menu to something else
- Save

The change won't be replicated, looking like it did nothing.
From there, removing or adding `edit_translations=1` in the URL will
use different cache version of the page's views and you will see the
outdated value on one and the correct on the other one.

after this commit:
`update_field_translation` will call `write`
it does the following 4 important things
1. mark field as modified
2. execute logics in the override `write` method
3. update write_date if needed to support t-cache

opw-3305117

X-original-commit: 2beb466668e4eb80d7c3ca3947445fb1cb141cff
Part-of: odoo/odoo#135277
This commit is contained in:
Chong Wang (cwg)
2023-09-13 12:19:10 +00:00
parent c71b571b68
commit 506d0cc4ec
7 changed files with 79 additions and 16 deletions
+34
View File
@@ -2,6 +2,8 @@
import json
from hashlib import sha256
from lxml import html
from unittest.mock import Mock, patch
from werkzeug.urls import url_parse
@@ -301,3 +303,35 @@ class TestMenuHttp(common.HttpCase):
self.assertFalse(self.menu.page_id, "M2o should have been unset as this is an anchor URL.")
self.assertEqual(self.menu.url, self.page_url + '#anchor', "Page URL should have been properly prefixed with the referer url")
self.assertEqual(self.page.url, self.page_url, "Page URL should not have changed")
def test_03_mega_menu_translate(self):
# Setup
fr = self.env['res.lang']._activate_lang('fr_FR')
Menu = self.env['website.menu']
website = self.env['website'].browse(1)
website.language_ids += fr
menu = Menu.create({
'name': 'Test Mega Menu Content Translation Edit Mode',
'mega_menu_content': '<p>something</p>',
'parent_id': website.menu_id.id,
'website_id': website.id,
})
self.env['ir.module.module']._load_module_terms(['website'], [fr.code])
# Load cache
self.url_open('/%s' % fr.url_code)
self.url_open('/%s?edit_translations=1' % fr.url_code)
# Translate
root = html.fromstring(menu.mega_menu_content)
to_translate = root.text_content()
sha = sha256(to_translate.encode()).hexdigest()
menu.update_field_translations_sha('mega_menu_content', {fr.code: {sha: 'french_mega_menu_content'}})
self.assertIn("french_mega_menu_content",
menu.with_context(lang=fr.code, website_id=website.id).mega_menu_content)
# Checks
page = self.url_open('/%s' % fr.url_code)
self.assertIn(b"french_mega_menu_content", page.content)
page = self.url_open('/%s?edit_translations=1' % fr.url_code)
self.assertIn(b"french_mega_menu_content", page.content)
+2 -7
View File
@@ -986,8 +986,9 @@ class IrModelFields(models.Model):
# names of the models to patch
patched_models = set()
if vals and self:
translate_only = all(self._fields[field_name].translate for field_name in vals)
for item in self:
if item.state != 'manual':
if item.state != 'manual' and not translate_only:
raise UserError(_('Properties of base fields cannot be altered in this manner! '
'Please modify them through Python code, '
'preferably through a custom addon!'))
@@ -1056,12 +1057,6 @@ class IrModelFields(models.Model):
return res
def update_field_translations(self, field_name, translations):
res = super().update_field_translations(field_name, translations)
if res:
self.env.registry.clear_cache()
return res
@api.depends('field_description', 'model')
def _compute_display_name(self):
for field in self:
+1 -4
View File
@@ -540,10 +540,7 @@ actual arch.
return super(View, self).unlink()
def _update_field_translations(self, fname, translations, digest=None):
res = super()._update_field_translations(fname, translations, digest)
if fname == 'arch_db' and 'install_filename' not in self._context:
self.write({'arch_updated': True})
return res
return super(View, self.with_context(no_save_prev=True))._update_field_translations(fname, translations, digest)
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
@@ -1749,6 +1749,20 @@ class RelatedTranslation2(models.Model):
parent_id = fields.Many2one('test_new_api.related_translation_1', string='Parent Model')
name = fields.Char('Name Related', related='parent_id.name', readonly=False)
html = fields.Html('HTML Related', related='parent_id.html', readonly=False)
computed_name = fields.Char('Name Computed', compute='_compute_name')
computed_html = fields.Char('HTML Computed', compute='_compute_html')
@api.depends_context('lang')
@api.depends('parent_id.name')
def _compute_name(self):
for record in self:
record.computed_name = record.parent_id.name
@api.depends_context('lang')
@api.depends('parent_id.html')
def _compute_html(self):
for record in self:
record.computed_html = record.parent_id.html
class RelatedTranslation3(models.Model):
@@ -89,6 +89,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Couteau')
self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Couteau')
self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Couteau')
self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Couteau')
self.test1.update_field_translations('name', {'fr_FR': 'Nouveau couteau'})
@@ -96,6 +98,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Nouveau couteau')
self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Nouveau couteau')
self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Nouveau couteau')
self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Nouveau couteau')
@@ -105,6 +109,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Couteau')
self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Couteau')
self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Couteau')
self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Couteau')
self.test3.update_field_translations('name', {'fr_FR': 'Nouveau couteau'})
@@ -112,6 +118,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Nouveau couteau')
self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Nouveau couteau')
self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Nouveau couteau')
self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife')
self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Nouveau couteau')
@@ -136,6 +144,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').computed_html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.test1.update_field_translations('html', {'fr_FR': {'Couteau': 'Nouveau couteau'}})
@@ -143,6 +153,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').computed_html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
@@ -151,6 +163,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').computed_html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.test3.update_field_translations('html', {'fr_FR': {'Couteau': 'Nouveau couteau'}})
@@ -158,6 +172,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase):
self.assertEqual(self.test1.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test2.with_context(lang='en_US').computed_html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
self.assertEqual(self.test3.with_context(lang='en_US').html, '<p>Knife</p><p>Fork</p><p>Spoon</p>')
self.assertEqual(self.test3.with_context(lang='fr_FR').html, '<p>Nouveau couteau</p><p>Fourchette</p><p>Cuiller</p>')
+11 -5
View File
@@ -3490,6 +3490,10 @@ class BaseModel(metaclass=MetaModel):
"""
self.ensure_one()
self.check_access_rights('write')
self.check_field_access_rights('write', [field_name])
self.check_access_rule('write')
valid_langs = set(code for code, _ in self.env['res.lang'].get_installed()) | {'en_US'}
missing_langs = set(translations) - valid_langs
if missing_langs:
@@ -3516,9 +3520,6 @@ class BaseModel(metaclass=MetaModel):
if field.related and not field.store:
related_path, field_name = field.related.rsplit(".", 1)
return self.mapped(related_path)._update_field_translations(field_name, translations, digest)
self.check_access_rights('write')
self.check_field_access_rights('write', [field_name])
self.check_access_rule('write')
if field.translate is True:
# falsy values (except emtpy str) are used to void the corresponding translation
@@ -3545,7 +3546,6 @@ class BaseModel(metaclass=MetaModel):
'{{}}'::jsonb)
WHERE id = %s
''', (Json({'en_US': translation_fallback}), Json(translations), self.id))
self.modified([field_name])
else:
# Note:
# update terms in 'en_US' will not change its value other translated values
@@ -3573,7 +3573,13 @@ class BaseModel(metaclass=MetaModel):
}
new_translations[lang] = field.translate(translation.get, old_value)
self.env.cache.update_raw(self, field, [new_translations], dirty=True)
self.modified([field_name])
# the following write is incharge of
# 1. mark field as modified
# 2. execute logics in the override `write` method
# 3. update write_date of the record if exists to support 't-cache'
# even if the value in cache is the same as the value written
self[field_name] = self[field_name]
return True
def get_field_translations(self, field_name, langs=None):
+1
View File
@@ -1397,6 +1397,7 @@ class TranslationImporter:
self.model_translations.clear()
env.invalidate_all()
env.registry.clear_cache()
if self.verbose:
_logger.info("translations are loaded successfully")