[MERGE] base: remove field source of ir.translation

Instead, make sure the field src is always up to date

Change a bit the behaviour of `_write` to always try to update potential
translations when in monolanguage (was only done in multi-language if different
than en_US) and always create translations when writing on a translatable field
when in multi-language (was skipped in en_US)

Add tests

Task-id: 2031752
Pad: https://pad.odoo.com/p/r.dbf835d0aae979d858f73446dcbfb1df

closes odoo/odoo#34598

Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
This commit is contained in:
Robot Odoo
2019-08-01 12:02:44 +02:00
committed by GitHub
7 changed files with 231 additions and 67 deletions
+2 -2
View File
@@ -63,7 +63,7 @@ class IrTranslation(models.Model):
project_modules[module] = tx_project
for translation in self:
if not translation.module or not translation.source or translation.lang == 'en_US':
if not translation.module or not translation.src or translation.lang == 'en_US':
# custom or source term
translation.transifex_url = False
continue
@@ -85,6 +85,6 @@ class IrTranslation(models.Model):
'lang': lang_code,
'module': translation.module,
'src': "text:'" + werkzeug.url_quote_plus(
translation.source[:50].replace("\n", "").replace("'", "")
translation.src[:50].replace("\n", "").replace("'", "")
) + "'",
}
+1 -1
View File
@@ -14,7 +14,7 @@ class WebsiteGengo(http.Controller):
result = {"done": 0}
gengo_translation_ids = request.env['ir.translation'].search([('id', 'in', translated_ids), ('gengo_translation', '!=', False)])
for trans in gengo_translation_ids:
result['done'] += len(trans.source.split())
result['done'] += len(trans.src.split())
return result
@http.route('/website/check_gengo_set', type='json', auth='user', website=True)
+46 -52
View File
@@ -171,8 +171,6 @@ class IrTranslation(models.Model):
lang = fields.Selection(selection='_get_languages', string='Language', validate=False)
type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility
source = fields.Text(string='Source term', compute='_compute_source',
inverse='_inverse_source', search='_search_source')
value = fields.Text(string='Translation Value')
module = fields.Char(index=True, help="Module this term belongs to")
@@ -196,55 +194,6 @@ class IrTranslation(models.Model):
langs = self.env['res.lang'].search([])
return [(lang.code, lang.name) for lang in langs]
@api.depends('type', 'name', 'res_id')
def _compute_source(self):
''' Get source name for the translation. If object type is model, return
the value stored in db. Otherwise, return value store in src field.
'''
for record in self:
record.source = record.src
if record.type != 'model':
continue
model_name, field_name = record.name.split(',')
if model_name not in self.env:
continue
model = self.env[model_name]
field = model._fields.get(field_name)
if field is None:
continue
if not callable(field.translate):
# Pass context without lang, need to read real stored field, not translation
try:
result = model.browse(record.res_id).with_context(lang=None).read([field_name])
except AccessError:
# because we can read self but not the record,
# that means we would get an access error when accessing the translations
# so instead we defer the access right to the "check" method
result = [{field_name: _("Cannot be translated; record not accessible.")}]
record.source = result[0][field_name] if result else False
def _inverse_source(self):
''' When changing source term of a translation, change its value in db
for the associated object, and the src field.
'''
self.ensure_one()
if self.type == 'model':
model_name, field_name = self.name.split(',')
model = self.env[model_name]
field = model._fields[field_name]
if not callable(field.translate):
# Make a context without language information, because we want
# to write on the value stored in db and not on the one
# associated with the current language. Also not removing lang
# from context trigger an error when lang is different.
model.browse(self.res_id).with_context(lang=None).write({field_name: self.source})
if self.src != self.source:
self.write({'src': self.source})
def _search_source(self, operator, value):
''' the source term is stored on 'src' field '''
return [('src', operator, value)]
def _auto_init(self):
res = super(IrTranslation, self)._auto_init()
# Add separate md5 index on src (no size limit on values, and good performance).
@@ -329,6 +278,22 @@ class IrTranslation(models.Model):
])
return len(ids)
def _set_source(self, name, ids, src):
""" Update the translation source of records.
:param name: a string defined as "<model_name>,<field_name>"
:param ids: the ids of the given records
:param src: the source of the translation
"""
self._cr.execute("""UPDATE ir_translation
SET src=%s
WHERE type=%s AND name=%s AND res_id IN %s
RETURNING id""",
(src, 'model', name, tuple(ids)))
existing_ids = [row[0] for row in self._cr.fetchall()]
# invalidate src for updated translations
self.invalidate_cache(fnames=['src'], ids=existing_ids)
@api.model
def _get_source_query(self, name, types, lang, source, res_id):
if source:
@@ -645,6 +610,8 @@ class IrTranslation(models.Model):
This method is used for creations of translations where the given
``vals_list`` is trusted to be the right values and potential
conflicts should be updated to the new given value.
Mandatory values: name, lang, res_id, src, type
The other keys are ignored during update if not present
"""
rows_by_type = defaultdict(list)
for vals in vals_list:
@@ -661,7 +628,10 @@ class IrTranslation(models.Model):
ON CONFLICT (type, lang, name, res_id) WHERE type='model'
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) =
(EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type,
EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
EXCLUDED.value,
COALESCE(EXCLUDED.module, ir_translation.module),
COALESCE(EXCLUDED.state, ir_translation.state),
COALESCE(EXCLUDED.comments, ir_translation.comments))
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
""".format(", ".join(["%s"] * len(rows_by_type['model'])))
self.env.cr.execute(query, rows_by_type['model'])
@@ -679,6 +649,30 @@ class IrTranslation(models.Model):
""".format(", ".join(["%s"] * len(rows_by_type['model_terms'])))
self.env.cr.execute(query, rows_by_type['model_terms'])
def _update_translations(self, vals_list):
""" Update translations of type 'model' or 'model_terms'.
This method is used for update of translations where the given
``vals_list`` is trusted to be the right values
No new translation will be created
"""
grouped_rows = {}
for vals in vals_list:
key = (vals['lang'], vals['type'], vals['name'])
grouped_rows.setdefault(key, [vals['value'], vals['src'], vals['state'], []])
grouped_rows[key][3].append(vals['res_id'])
for where, values in grouped_rows.items():
self._cr.execute(
""" UPDATE ir_translation
SET value=%s,
src=%s,
state=%s
WHERE lang=%s AND type=%s AND name=%s AND res_id in %s
""",
(values[0], values[1], values[2], where[0], where[1], where[2], tuple(values[3]))
)
@api.model
def translate_fields(self, model, id, field=None):
""" Open a view for translating the field(s) of the record (model, id). """
+151
View File
@@ -275,6 +275,13 @@ class TestTranslation(TransactionCase):
category_fr = category.with_context({'lang': 'fr_FR'})
self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value")
translation_fr = self.env['ir.translation'].search([
('name', '=', 'res.partner.category,name'),
('res_id', '=', category.id),
('lang', '=', 'fr_FR'),
])
self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source")
def test_103_duplicate_record_fr(self):
category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'})
@@ -284,6 +291,13 @@ class TestTranslation(TransactionCase):
category_fr = category.with_context({'lang': 'fr_FR'})
self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value")
translation_fr = self.env['ir.translation'].search([
('name', '=', 'res.partner.category,name'),
('res_id', '=', category.id),
('lang', '=', 'fr_FR'),
])
self.assertEqual(translation_fr.src, 'Clients (copie)', "Did not set new name as source")
def test_104_orderby_translated_field(self):
""" Test search ordered by a translated field. """
# create a category with a French translation
@@ -328,6 +342,143 @@ class TestTranslation(TransactionCase):
self.assertEqual(len(translations), 1, "Translations were not duplicated after `translate_fields` call")
self.assertEqual(translations.value, "Apprenti", "The first translation must stay")
def test_106_en_us_translation(self):
""" Test synchronizing translations with duplicated source """
# create a category with a French translation
cheese = self.env['res.partner.category'].create({'name': 'Cheese'})
self.env['ir.translation'].translate_fields('res.partner.category', cheese.id, 'name')
translations = self.env['ir.translation'].search([('name', '=', 'res.partner.category,name'), ('res_id', '=', cheese.id)], order='lang')
self.assertEqual(len(translations), 2)
self.assertRecordValues(translations,
[{'lang': 'en_US', 'src': 'Cheese', 'value': 'Cheese'},
{'lang': 'fr_FR', 'src': 'Cheese', 'value': 'Cheese'}])
# Translate in both language
translations[0].value = 'The Cheese'
translations[1].value = 'Fromage'
# lang=None bypass translation system
self.assertEqual(cheese.with_context(lang=None).name, 'Cheese')
self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage')
self.assertEqual(cheese.with_context(lang='en_US').name, 'The Cheese')
# set a new master value
cheese.with_context(lang='en_US').write({'name': 'Delicious Cheese'})
# every src must be updated
self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese')
self.assertRecordValues(translations,
[{'lang': 'en_US', 'src': 'Delicious Cheese', 'value': 'Delicious Cheese'},
{'lang': 'fr_FR', 'src': 'Delicious Cheese', 'value': 'Fromage'}])
self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese')
self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage')
self.assertEqual(cheese.with_context(lang='en_US').name, 'Delicious Cheese')
def test_107_duplicate_record_en(self):
category = self.customers.with_context({'lang': 'en_US'}).copy()
category_no = category.with_context({})
self.assertEqual(category_no.name, 'Customers', "Duplication did not set untranslated value")
category_fr = category.with_context({'lang': 'fr_FR'})
self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value")
translation_fr = self.env['ir.translation'].search([
('name', '=', 'res.partner.category,name'),
('res_id', '=', category.id),
('lang', '=', 'fr_FR'),
])
self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source")
class TestTranslationWrite(TransactionCase):
def setUp(self):
super().setUp()
self.category = self.env['res.partner.category'].create({'name': 'Reblochon'})
def test_01_en(self):
langs = self.env['res.lang'].get_installed()
self.assertEqual([('en_US', 'English (US)')], langs, "Test did not started with expected languages")
self.category.with_context(lang='en_US').write({'name': 'English Name'})
name = self.category.with_context(lang=None).read(['name'])
self.assertEqual(name[0]['name'], "English Name", "Reference field not updated")
translations = self.env['ir.translation'].search([
('name', '=', 'res.partner.category,name'),
('res_id', '=', self.category.id),
('lang', '=', 'en_US'),
])
self.assertEqual(len(translations), 0, "No English translation should be created when writing in English")
def test_02_en_translated(self):
langs = self.env['res.lang'].get_installed()
self.assertEqual([('en_US', 'English (US)')], langs, "Test did not started with expected languages")
translation = self.env['ir.translation'].create({
'type': 'model',
'name': 'res.partner.category,name',
'lang': 'en_US',
'res_id': self.category.id,
'src': 'Reblochon',
'value': 'Translated Name',
'state': 'translated',
})
self.category.with_context(lang='en_US').write({'name': 'English Name'})
translation_value = translation.read(['value'])
self.assertEqual(translation_value[0]['value'], "English Name", "Existing translation was not updated")
source_name = self.category.with_context(lang=None).read(['name'])
self.assertEqual(source_name[0]['name'], "English Name", "Reference field not updated")
def test_03_fr_single(self):
self.env['res.lang'].load_lang('fr_FR')
self.env['res.users'].with_context(active_test=False).search([]).write({'lang': 'fr_FR'})
self.env.ref('base.lang_en').active = False
langs = self.env['res.lang'].get_installed()
self.assertEqual([('fr_FR', 'French / Français')], langs, "Test did not started with expected languages")
self.category.with_context(lang='fr_FR').write({'name': 'French Name'})
source_name = self.category.with_context(lang=None).read(['name'])
self.assertEqual(source_name[0]['name'], "French Name", "Reference field not updated")
translations = self.env['ir.translation'].search([
('name', '=', 'res.partner.category,name'),
('res_id', '=', self.category.id),
('lang', '=', 'fr_FR'),
])
self.assertEqual(len(translations), 0, "No French translation should be created when writing in French")
def test_04_fr_multi(self):
self.env['res.lang'].load_lang('fr_FR')
langs = self.env['res.lang'].get_installed()
self.assertEqual([('en_US', 'English (US)'), ('fr_FR', 'French / Français')], langs,
"Test did not started with expected languages")
self.env['ir.translation'].create({
'type': 'model',
'name': 'res.partner.category,name',
'lang': 'en_US',
'res_id': self.category.id,
'src': 'Reblochon',
'value': 'Translated Name',
'state': 'translated',
})
self.category.with_context(lang='fr_FR').write({'name': 'French Name'})
self.category.with_context(lang='en_US').write({'name': 'English Name'})
translations = self.env['ir.translation'].search([
('name', '=', 'res.partner.category,name'),
('res_id', '=', self.category.id),
], order='lang')
self.assertRecordValues(translations, [
{'src': 'English Name', 'value': 'English Name', 'lang': 'en_US'},
{'src': 'English Name', 'value': 'French Name', 'lang': 'fr_FR'}
])
class TestXMLTranslation(TransactionCase):
def setUp(self):
@@ -11,7 +11,7 @@
string="Web-only translations"
domain="[('comments', 'like', 'openerp-web')]"/>
<field name="lang"/>
<field name="source"/>
<field name="src"/>
<field name="value"/>
<field name="name" filter_domain="('|', ('name', '=', self), ('name', 'ilike', self + ','))" string="Model"/>
<field name="res_id" filter_domain="('&amp;', ('name', '=', 'ir.ui.view,arch_db'), ('res_id', '=', self))" string="View ID"/>
@@ -38,7 +38,7 @@
<field name="res_id"/>
</group>
<group string="Source Term">
<field name="source" nolabel="1" height="400"/>
<field name="src" nolabel="1" height="400"/>
</group>
<group string="Translation">
<field name="value" nolabel="1" height="400"/>
@@ -55,7 +55,7 @@
<field name="model">ir.translation</field>
<field name="arch" type="xml">
<tree string="Translations" editable="top">
<field name="source"/>
<field name="src"/>
<field name="value"/>
<field name="name"/>
<field name="lang"/>
@@ -71,7 +71,7 @@
<tree string="Translations" editable="top" create="false">
<field name="lang" readonly="1"/>
<field name="name" readonly="1"/>
<field name="source"/>
<field name="src"/>
<field name="value"/>
<field name="state"/>
</tree>
@@ -1230,8 +1230,7 @@ class TestFields(common.TransactionCase):
email = self.env.ref('test_new_api.emailmessage_0_0')
self.assertEqual(email.message, message)
french = self.env['res.lang']._lang_get('fr_FR')
french.active = True
self.env['res.lang'].load_lang('fr_FR')
def count(msg):
# return the number of translations of msg.label
+26 -6
View File
@@ -3397,6 +3397,12 @@ Fields:
other_fields = [] # list of non-column fields
single_lang = len(self.env['res.lang'].get_installed()) <= 1
has_translation = self.env.lang and self.env.lang != 'en_US'
# when there is only one language, update existing translations but
# do not create new ones
if single_lang:
process_translations = self.env['ir.translation']._update_translations
else:
process_translations = self.env['ir.translation']._upsert_translations
for name, val in vals.items():
field = self._fields[name]
@@ -3410,6 +3416,9 @@ Fields:
# val is not a translation: update the table
val = field.convert_to_column(val, self, vals)
columns.append((name, field.column_format, val))
tname = "%s,%s" % (self._name, name)
if field.translate is True and self.env.lang:
self.env['ir.translation']._set_source(tname, self.ids, val)
updated.append(name)
else:
other_fields.append(field)
@@ -3440,6 +3449,7 @@ Fields:
+ '\n\n({} {}, {} {})'.format(_('Records:'), sub_ids[:6], _('User:'), self._uid)
)
translation_values = []
for name in updated:
field = self._fields[name]
if callable(field.translate):
@@ -3447,17 +3457,26 @@ Fields:
# synchronize translated terms when possible.
self.env['ir.translation']._sync_terms_translations(field, self)
elif has_translation and field.translate:
elif self.env.lang and field.translate:
# The translated value of a field has been modified.
src_trans = self.read([name])[0][name]
src_trans = self.with_context(lang=None).read([name])[0][name]
if not src_trans:
# Insert value to DB
src_trans = vals[name]
self.with_context(lang=None).write({name: src_trans})
tname = "%s,%s" % (self._name, name)
val = field.convert_to_column(vals[name], self, vals)
self.env['ir.translation']._set_ids(
tname, 'model', self.env.lang, self.ids, val, src_trans)
translation_values += [dict(
src=src_trans,
value=val,
name=tname,
lang=self.env.lang,
type='model',
state='translated',
res_id=res_id) for res_id in self.ids]
if translation_values:
process_translations(translation_values)
# mark fields to recompute; do this before setting other fields, because
# the latter can require the value of computed fields, e.g., a one2many
@@ -4283,11 +4302,12 @@ Fields:
vals_list = []
for vals in Translation.search_read(domain):
del vals['id']
del vals['source'] # remove source to avoid triggering _set_src
del vals['module'] # duplicated vals is not linked to any module
vals['res_id'] = target_id
if not callable(field.translate):
vals['src'] = new_wo_lang[name]
if vals['lang'] == old.env.lang and field.translate is True:
# force a source if the new_val was not changed by copy override
# update master record if the new_val was not changed by copy override
if new_val == old[name]:
new_wo_lang[name] = old_wo_lang[name]
vals['src'] = old_wo_lang[name]