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

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

Changes in _write:
- Replace _set_ids (to be deprecated) call by _upsert_translations as it works
  in batch
- Call _upsert_translations for any language, including en_US
  In case an English translation already existed for a record, only the master
  value (on the reference record) was updated but the user kept seeing the
  translation value (was revealed 73a7534bfc).
- Read src_trans without language
  Similar as above, if an English translation already existed, the translation
  was used for the result of the read and not the new value that has just been
  inserted into the database
- Add _set_source method
  When updating a master record of a translated field, the src field must be
  updated, including in different languages.
  Before it was ok that the src field was out of date as the source was computed

Changes in copy_translation:
- set src as the new value without lang
  update the comment to reflect reality since 489494e733
  src will contain the English version if no changes were made and it will
  contain the modified value if copy was overriden

Changes in upsert_translations:
- Do not force a module, comment, state
  Only src, res_id, name, value and lang must be given. Optional values will no
  longer be set to null if not given
This commit is contained in:
Martin Trigaux
2019-08-01 09:06:25 +00:00
parent 7aadcf1181
commit 66ef641f33
6 changed files with 112 additions and 64 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)
+22 -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'])
+65
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,57 @@ 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 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>
+18 -5
View File
@@ -3410,6 +3410,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 not single_lang:
self.env['ir.translation']._set_source(tname, self.ids, val)
updated.append(name)
else:
other_fields.append(field)
@@ -3440,6 +3443,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):
@@ -3449,15 +3453,23 @@ Fields:
elif has_translation 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]
self.env['ir.translation']._upsert_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 +4295,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]