From 8ddfb1888b9f5ef20f00f2a4c9d5fe3a16c319cf Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 24 Jun 2015 10:24:33 +0200 Subject: [PATCH] [IMP] models, fields: handle the case where field.translate is a callable Using `field.translate(callback, value)` returns the translation of the given field value by using the function `callback` to translate simple terms in `value`. In practice, the function `callback` uses a translation mapping retrieved from `ir.translation`. --- addons/base_gengo/ir_translation.py | 19 ++++++- openerp/addons/base/ir/ir_translation.py | 31 ++++++++++++ .../addons/base/ir/ir_translation_view.xml | 3 +- openerp/fields.py | 46 +++++++++++++++-- openerp/models.py | 32 +++++++++--- openerp/osv/expression.py | 2 +- openerp/tools/translate.py | 49 +++++-------------- 7 files changed, 133 insertions(+), 49 deletions(-) diff --git a/addons/base_gengo/ir_translation.py b/addons/base_gengo/ir_translation.py index 9885c24df65..f1297f96e97 100644 --- a/addons/base_gengo/ir_translation.py +++ b/addons/base_gengo/ir_translation.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from openerp.osv import fields, osv, api from openerp.tools.translate import _ from openerp.exceptions import UserError @@ -84,3 +84,20 @@ class ir_translation(osv.Model): """ params += ('machine', 'standard', 'ultra', 'pro',) return (query, params) + + @api.model + def _get_terms_query(self, field, records): + query, params = super(ir_translation, self)._get_terms_query(field, records) + # order translations from worst to best + query += """ + ORDER BY + CASE + WHEN gengo_translation=%s then 10 + WHEN gengo_translation=%s then 20 + WHEN gengo_translation=%s then 30 + WHEN gengo_translation=%s then 40 + ELSE 0 + END ASC + """ + params += ('machine', 'standard', 'ultra', 'pro') + return query, params diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index fb0fbab55c6..46502a2f358 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -363,6 +363,37 @@ class ir_translation(osv.osv): res_id = tuple(res_id) return self.__get_source(cr, uid, name, types, lang, source, res_id) + @api.model + def _get_terms_query(self, field, records): + """ Utility function that makes the query for field terms. """ + query = """ SELECT id, res_id, src, value FROM ir_translation + WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s """ + name = "%s,%s" % (field.model_name, field.name) + params = (records.env.lang, 'model', name, tuple(records.ids)) + return query, params + + @api.model + def _get_terms_mapping(self, field, records): + """ Return a function mapping a (trans_id, source, value) to a value. + This method is called before querying the database for translations. + """ + return lambda tid, src, value: value + + @api.model + def _get_terms_translations(self, field, records): + """ Return the terms and translations of a given `field` on `records`. + + :return: {record_id: {source: value}} + """ + result = {rid: {} for rid in records.ids} + if records: + map_trans = self._get_terms_mapping(field, records) + query, params = self._get_terms_query(field, records) + self._cr.execute(query, params) + for trans_id, res_id, src, value in self._cr.fetchall(): + result[res_id][src] = map_trans(trans_id, src, value) + return result + @api.model @tools.ormcache_context('model_name', keys=('lang',)) def get_field_string(self, model_name): diff --git a/openerp/addons/base/ir/ir_translation_view.xml b/openerp/addons/base/ir/ir_translation_view.xml index 3fd80f90da9..6bf3d783040 100644 --- a/openerp/addons/base/ir/ir_translation_view.xml +++ b/openerp/addons/base/ir/ir_translation_view.xml @@ -8,7 +8,7 @@ + domain="['|','|',('state','=','to_translate'),('value', '=', False),('value','=','')]"/> @@ -61,6 +61,7 @@ + diff --git a/openerp/fields.py b/openerp/fields.py index 6c0253b5ea7..89cb9325f88 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -1110,15 +1110,50 @@ class _String(Field): _column_translate = property(attrgetter('translate')) _related_translate = property(attrgetter('translate')) - _description_translate = property(attrgetter('translate')) + + def _description_translate(self, env): + return bool(self.translate) + + def get_trans_terms(self, value): + """ Return the sequence of terms to translate found in `value`. """ + if not callable(self.translate): + return [value] if value else [] + terms = [] + self.translate(terms.append, value) + return terms + + def get_trans_func(self, records): + """ Return a translation function `translate` for `self` on the given + records; the function call `translate(record_id, value)` translates the + field value to the language given by the environment of `records`. + """ + if callable(self.translate): + rec_src_trans = records.env['ir.translation']._get_terms_translations(self, records) + + def translate(record_id, value): + src_trans = rec_src_trans[record_id] + return self.translate(src_trans.get, value) + + else: + rec_trans = records.env['ir.translation']._get_ids( + '%s,%s' % (self.model_name, self.name), 'model', records.env.lang, records.ids) + + def translate(record_id, value): + return rec_trans.get(record_id) or value + + return translate class Char(_String): """ Basic string field, can be length-limited, usually displayed as a - single-line string in clients + single-line string in clients. :param int size: the maximum size of values stored for that field - :param bool translate: whether the values of this field can be translated + + :param translate: enable the translation of the field's values; use + `translate=True` to translate field values as a whole; `translate` may + also be a callable such that `translate(callback, value)` translates + `value` by using `callback(term)` to retrieve the translation of terms. """ type = 'char' _slots = { @@ -1143,7 +1178,10 @@ class Text(_String): """ Very similar to :class:`~.Char` but used for longer contents, does not have a size and usually displayed as a multiline text box. - :param translate: whether the value of this field can be translated + :param translate: enable the translation of the field's values; use + `translate=True` to translate field values as a whole; `translate` may + also be a callable such that `translate(callback, value)` translates + `value` by using `callback(term)` to retrieve the translation of terms. """ type = 'text' diff --git a/openerp/models.py b/openerp/models.py index a147615bc54..4a6f380feba 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -2135,7 +2135,8 @@ class BaseModel(object): ) model, alias = parent_model, parent_alias # handle the case where the field is translated - if model._columns[field].translate: + translate = model._columns[field].translate + if translate and not callable(translate): return model._generate_translated_field(alias, field, query) else: return '"%s"."%s"' % (alias, field) @@ -3222,6 +3223,7 @@ class BaseModel(object): field for field in fields if field.base_field.column._classic_write + if not (field.inherited and callable(field.base_field.column.translate)) ] # the query may involve several tables: we need fully-qualified names @@ -3254,8 +3256,18 @@ class BaseModel(object): result.extend(cr.dictfetchall()) ids = [vals['id'] for vals in result] + fetched = self.browse(ids) if ids: + # translate the fields if necessary + if context.get('lang'): + for field in fields_pre: + if not field.inherited and callable(field.column.translate): + f = field.name + translate = field.get_trans_func(fetched) + for vals in result: + vals[f] = translate(vals['id'], vals[f]) + # apply the symbol_get functions of the fields we just read for field in fields_pre: symbol_get = field.base_field.column._symbol_get @@ -3312,7 +3324,6 @@ class BaseModel(object): record._cache.update(record._convert_to_cache(vals, validate=False)) # store failed values in cache for the records that could not be read - fetched = self.browse(ids) missing = self - fetched if missing: extras = fetched - self @@ -3795,7 +3806,7 @@ class BaseModel(object): upd_todo = [] updend = [] direct = [] - totranslate = context.get('lang', False) and (context['lang'] != 'en_US') + totranslate = context.get('lang') and context['lang'] != 'en_US' for field in vals: ffield = self._fields.get(field) if ffield and ffield.deprecated: @@ -3805,7 +3816,7 @@ class BaseModel(object): if hasattr(column, 'selection') and vals[field]: self._check_selection_field_value(cr, user, field, vals[field], context=context) if column._classic_write and not hasattr(column, '_fnct_inv'): - if (not totranslate) or not column.translate: + if not (totranslate and column.translate): updates.append((field, '%s', column._symbol_set[1](vals[field]))) direct.append(field) else: @@ -3833,7 +3844,16 @@ class BaseModel(object): if totranslate: # TODO: optimize for f in direct: - if self._columns[f].translate: + if callable(self._columns[f].translate): + # We do not handle the update of a field where translate + # is a callable. One should update the value without a + # language, or update translated terms separately. + raise ValidationError(_( + "The translation of the field '%s' cannot be " + "directly updated. You can either update its value " + "in English, or update translated terms separately." + ) % self._fields[f].string) + elif self._columns[f].translate: src_trans = self.pool[self._name].read(cr, user, ids, [f])[0][f] if not src_trans: src_trans = vals[f] @@ -4555,7 +4575,7 @@ class BaseModel(object): elif order_field in self._columns: order_column = self._columns[order_field] if order_column._classic_read: - if order_column.translate: + if order_column.translate and not callable(order_column.translate): inner_clause = self._generate_translated_field(self._table, order_field, query) else: inner_clause = '"%s"."%s"' % (self._table, order_field) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 171ff9e4047..bbc373e7f0e 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -1026,7 +1026,7 @@ class expression(object): right += ' 00:00:00' push(create_substitution_leaf(leaf, (left, operator, right), model)) - elif column.translate and right: + elif column.translate and not callable(column.translate) and right: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) if need_wildcard: diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 2f8d779215a..9028a235acd 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -723,33 +723,14 @@ def trans_generate(lang, modules, cr): except AttributeError, exc: _logger.error("name error in %s: %s", xml_name, str(exc)) continue - objmodel = registry.get(obj.model) - if (objmodel is None or field_name not in objmodel._columns - or not objmodel._translate): + field_model = registry.get(obj.model) + if (field_model is None or not field_model._translate or + field_name not in field_model._fields): continue - field_def = objmodel._columns[field_name] - - name = "%s,%s" % (encode(obj.model), field_name) - push_translation(module, 'field', name, 0, encode(field_def.string)) - - if field_def.help: - push_translation(module, 'help', name, 0, encode(field_def.help)) - - if field_def.translate: - ids = objmodel.search(cr, uid, []) - obj_values = objmodel.read(cr, uid, ids, [field_name]) - for obj_value in obj_values: - res_id = obj_value['id'] - if obj.name in ('ir.model', 'ir.ui.menu'): - res_id = 0 - model_data_ids = model_data_obj.search(cr, uid, [ - ('model', '=', model), - ('res_id', '=', res_id), - ]) - if not model_data_ids: - push_translation(module, 'model', name, 0, encode(obj_value[field_name])) + field_def = field_model._fields[field_name] if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)): + name = "%s,%s" % (encode(obj.model), field_name) for dummy, val in field_def.selection: push_translation(module, 'selection', name, 0, encode(val)) @@ -761,9 +742,7 @@ def trans_generate(lang, modules, cr): parse_func = trans_parse_rml report_type = "report" elif obj.report_xsl: - fname = obj.report_xsl - parse_func = trans_parse_xsl - report_type = "xsl" + continue if fname and obj.report_type in ('pdf', 'xsl'): try: report_file = misc.file_open(fname) @@ -776,17 +755,15 @@ def trans_generate(lang, modules, cr): except (IOError, etree.XMLSyntaxError): _logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname) - for field_name, field_def in obj._columns.items(): - if model == 'ir.model' and field_name == 'name' and obj.name == obj.model: - # ignore model name if it is the technical one, nothing to translate - continue - if field_def.translate: + for field_name, field_def in obj._fields.iteritems(): + if getattr(field_def, 'translate', None): name = model + "," + field_name try: - term = obj[field_name] or '' - except: - term = '' - push_translation(module, 'model', name, xml_name, encode(term)) + value = obj[field_name] or '' + except Exception: + continue + for term in set(field_def.get_trans_terms(value)): + push_translation(module, 'model', name, xml_name, encode(term)) # End of data for ir.model.data query results