From 9593a2930fae38a4d8483ee1f23baed343015ef4 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 10 Jun 2015 11:09:54 +0200 Subject: [PATCH] [IMP] model, query: implement sort on translatable fields The methods `BaseModel._inherits_join_calc` and `BaseModel._generate_order_by` are modified to add a JOIN to the translation table for translated fields, so that the translation value is directly accessible in the query itself. For the JOIN with the translation table to work, the method `Query.add_join` is extended to take extra conditions in the join clause, together with parameters. --- openerp/addons/base/tests/test_base.py | 11 ++++ openerp/models.py | 32 +++++++++++- openerp/osv/query.py | 70 +++++++++++++++++++------- 3 files changed, 93 insertions(+), 20 deletions(-) diff --git a/openerp/addons/base/tests/test_base.py b/openerp/addons/base/tests/test_base.py index b289d44bbaf..47923ead732 100644 --- a/openerp/addons/base/tests/test_base.py +++ b/openerp/addons/base/tests/test_base.py @@ -464,6 +464,17 @@ class test_translation(common.TransactionCase): fr_context_cat = self.res_category.browse(cr, uid, self.new_fr_cat_id, context={'lang':'fr_FR'}) self.assertEqual(fr_context_cat.name, 'Clients (copie)', "Did not used default value for translated value") + def test_104_orderby_translated_field(self): + """ Test search ordered by a translated field. """ + # create a category with a French translation + category = self.env['res.partner.category'].create({'name': 'Padawans'}) + category_fr = category.with_context(lang='fr_FR') + category_fr.write({'name': 'Apprentis'}) + # search for categories, and sort them by (translated) name + categories = category_fr.search([('id', 'in', [self.cat_id, category.id])], order='name') + self.assertEqual(categories.ids, [category.id, self.cat_id], + "Search ordered by translated name should return Padawans (Apprentis) before Customers (Clients)") + test_state = None #: Stores state information across multiple test classes def setUpModule(): diff --git a/openerp/models.py b/openerp/models.py index 130525f0943..bd213f8e55c 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -2183,7 +2183,11 @@ class BaseModel(object): implicit=True, ) model, alias = parent_model, parent_alias - return '"%s"."%s"' % (alias, field) + # handle the case where the field is translated + if model._columns[field].translate: + return model._generate_translated_field(alias, field, query) + else: + return '"%s"."%s"' % (alias, field) def _parent_store_compute(self, cr): if not self._parent_store: @@ -4525,6 +4529,27 @@ class BaseModel(object): apply_rule(rule_where_clause, rule_where_clause_params, rule_tables, parent_model=inherited_model) + @api.model + def _generate_translated_field(self, table_alias, field, query): + """ + Add possibly missing JOIN with translations table to ``query`` and + generate the expression for the translated field. + + :return: the qualified field name (or expression) to use for ``field`` + """ + lang = self._context.get('lang') + if lang and lang != 'en_US': + alias, alias_statement = query.add_join( + (table_alias, 'ir_translation', 'id', 'res_id', field), + implicit=False, + outer=True, + extra='"{rhs}"."name" = %s AND "{rhs}"."lang" = %s AND "{rhs}"."value" != %s', + extra_params=["%s,%s" % (self._name, field), lang, ""], + ) + return 'COALESCE("%s"."%s", "%s"."%s")' % (alias, 'value', table_alias, field) + else: + return '"%s"."%s"' % (table_alias, field) + @api.model def _generate_m2o_order_by(self, order_field, query): """ @@ -4593,7 +4618,10 @@ class BaseModel(object): elif order_field in self._columns: order_column = self._columns[order_field] if order_column._classic_read: - inner_clause = '"%s"."%s"' % (self._table, order_field) + if order_column.translate: + inner_clause = self._generate_translated_field(self._table, order_field, query) + else: + inner_clause = '"%s"."%s"' % (self._table, order_field) elif order_column._type == 'many2one': inner_clause = self._generate_m2o_order_by(order_field, query) else: diff --git a/openerp/osv/query.py b/openerp/osv/query.py index 8c52aa8b753..3fbdc476d4e 100644 --- a/openerp/osv/query.py +++ b/openerp/osv/query.py @@ -21,7 +21,7 @@ class Query(object): - etc. """ - def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None): + def __init__(self, tables=None, where_clause=None, where_clause_params=None, joins=None, extras=None): # holds the list of tables joined using default JOIN. # the table names are stored double-quoted (backwards compatibility) @@ -49,6 +49,21 @@ class Query(object): # LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col") self.joins = joins or {} + # holds extra conditions for table joins that should not be in the where + # clause but in the join condition itself. The dict is used as follows: + # + # self.extras = { + # ('table_a', ('table_b', 'table_a_col1', 'table_b_col', 'LEFT JOIN')): + # ('"table_b"."table_b_col3" = %s', [42]) + # } + # + # which should lead to the following SQL: + # + # SELECT ... FROM "table_a" + # LEFT JOIN "table_b" ON ("table_a"."table_a_col1" = "table_b"."table_b_col" AND "table_b"."table_b_col3" = 42) + # ... + self.extras = extras or {} + def _get_table_aliases(self): from openerp.osv.expression import get_alias_from_query return [get_alias_from_query(from_statement)[1] for from_statement in self.tables] @@ -61,7 +76,7 @@ class Query(object): mapping[statement] = table return mapping - def add_join(self, connection, implicit=True, outer=False): + def add_join(self, connection, implicit=True, outer=False, extra=None, extra_params=[]): """ Join a destination table to the current table. :param implicit: False if the join is an explicit join. This allows @@ -69,7 +84,7 @@ class Query(object): OpenERP 7.0. It therefore adds the JOIN specified in ``connection`` If True, the join is done implicitely, by adding the table alias in the from clause and the join condition in the where clause - of the query. Implicit joins do not handle outer parameter. + of the query. Implicit joins do not handle outer, extra, extra_params parameters. :param connection: a tuple ``(lhs, table, lhs_col, col, link)``. The join corresponds to the SQL equivalent of:: @@ -84,6 +99,14 @@ class Query(object): columns so it would not be correct (e.g. for ``_inherits`` or when a domain criterion explicitly adds filtering) + + :param extra: A string with the extra join condition (SQL), or None. + This is used to provide an additional condition to the join + clause that cannot be added in the where clause (e.g., for LEFT + JOIN concerns). The condition string should refer to the table + aliases as "{lhs}" and "{rhs}". + + :param extra_params: a list of parameters for the `extra` condition. """ from openerp.osv.expression import generate_table_alias (lhs, table, lhs_col, col, link) = connection @@ -107,32 +130,43 @@ class Query(object): else: # add JOIN self.tables.append(alias_statement) - self.joins.setdefault(lhs, []).append((alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN')) + join_tuple = (alias, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN') + self.joins.setdefault(lhs, []).append(join_tuple) + if extra: + extra = extra.format(lhs=lhs, rhs=alias) + self.extras[(lhs, join_tuple)] = (extra, extra_params) return alias, alias_statement def get_sql(self): """ Returns (query_from, query_where, query_params). """ from openerp.osv.expression import get_alias_from_query - query_from = '' tables_to_process = list(self.tables) alias_mapping = self._get_alias_mapping() + from_clause = [] + from_params = [] - def add_joins_for_table(table, query_from): - for (dest_table, lhs_col, col, join) in self.joins.get(table, []): - tables_to_process.remove(alias_mapping[dest_table]) - query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \ - (join, alias_mapping[dest_table], table, lhs_col, dest_table, col) - query_from = add_joins_for_table(dest_table, query_from) - return query_from + def add_joins_for_table(lhs): + for (rhs, lhs_col, rhs_col, join) in self.joins.get(lhs, []): + tables_to_process.remove(alias_mapping[rhs]) + from_clause.append(' %s %s ON ("%s"."%s" = "%s"."%s"' % \ + (join, alias_mapping[rhs], lhs, lhs_col, rhs, rhs_col)) + extra = self.extras.get((lhs, (rhs, lhs_col, rhs_col, join))) + if extra: + from_clause.append(' AND ') + from_clause.append(extra[0]) + from_params.extend(extra[1]) + from_clause.append(')') + add_joins_for_table(rhs) - for table in tables_to_process: - query_from += table + for pos, table in enumerate(tables_to_process): + if pos > 0: + from_clause.append(',') + from_clause.append(table) table_alias = get_alias_from_query(table)[1] if table_alias in self.joins: - query_from = add_joins_for_table(table_alias, query_from) - query_from += ',' - query_from = query_from[:-1] # drop last comma - return query_from, " AND ".join(self.where_clause), self.where_clause_params + add_joins_for_table(table_alias) + + return "".join(from_clause), " AND ".join(self.where_clause), from_params + self.where_clause_params def __str__(self): return '' % self.get_sql()