[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.
This commit is contained in:
Denis Ledoux
2015-06-17 13:32:17 +02:00
committed by Raphael Collet
parent 44efcf8066
commit 9593a2930f
3 changed files with 93 additions and 20 deletions
+11
View File
@@ -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'}) 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") 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 test_state = None
#: Stores state information across multiple test classes #: Stores state information across multiple test classes
def setUpModule(): def setUpModule():
+30 -2
View File
@@ -2183,7 +2183,11 @@ class BaseModel(object):
implicit=True, implicit=True,
) )
model, alias = parent_model, parent_alias 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): def _parent_store_compute(self, cr):
if not self._parent_store: if not self._parent_store:
@@ -4525,6 +4529,27 @@ class BaseModel(object):
apply_rule(rule_where_clause, rule_where_clause_params, rule_tables, apply_rule(rule_where_clause, rule_where_clause_params, rule_tables,
parent_model=inherited_model) 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 @api.model
def _generate_m2o_order_by(self, order_field, query): def _generate_m2o_order_by(self, order_field, query):
""" """
@@ -4593,7 +4618,10 @@ class BaseModel(object):
elif order_field in self._columns: elif order_field in self._columns:
order_column = self._columns[order_field] order_column = self._columns[order_field]
if order_column._classic_read: 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': elif order_column._type == 'many2one':
inner_clause = self._generate_m2o_order_by(order_field, query) inner_clause = self._generate_m2o_order_by(order_field, query)
else: else:
+52 -18
View File
@@ -21,7 +21,7 @@ class Query(object):
- etc. - 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. # holds the list of tables joined using default JOIN.
# the table names are stored double-quoted (backwards compatibility) # 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") # LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col")
self.joins = joins or {} 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): def _get_table_aliases(self):
from openerp.osv.expression import get_alias_from_query from openerp.osv.expression import get_alias_from_query
return [get_alias_from_query(from_statement)[1] for from_statement in self.tables] return [get_alias_from_query(from_statement)[1] for from_statement in self.tables]
@@ -61,7 +76,7 @@ class Query(object):
mapping[statement] = table mapping[statement] = table
return mapping 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. """ Join a destination table to the current table.
:param implicit: False if the join is an explicit join. This allows :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`` OpenERP 7.0. It therefore adds the JOIN specified in ``connection``
If True, the join is done implicitely, by adding the table alias If True, the join is done implicitely, by adding the table alias
in the from clause and the join condition in the where clause 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)``. :param connection: a tuple ``(lhs, table, lhs_col, col, link)``.
The join corresponds to the SQL equivalent of:: 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 columns so it would not be correct (e.g. for
``_inherits`` or when a domain criterion explicitly ``_inherits`` or when a domain criterion explicitly
adds filtering) 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 from openerp.osv.expression import generate_table_alias
(lhs, table, lhs_col, col, link) = connection (lhs, table, lhs_col, col, link) = connection
@@ -107,32 +130,43 @@ class Query(object):
else: else:
# add JOIN # add JOIN
self.tables.append(alias_statement) 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 return alias, alias_statement
def get_sql(self): def get_sql(self):
""" Returns (query_from, query_where, query_params). """ """ Returns (query_from, query_where, query_params). """
from openerp.osv.expression import get_alias_from_query from openerp.osv.expression import get_alias_from_query
query_from = ''
tables_to_process = list(self.tables) tables_to_process = list(self.tables)
alias_mapping = self._get_alias_mapping() alias_mapping = self._get_alias_mapping()
from_clause = []
from_params = []
def add_joins_for_table(table, query_from): def add_joins_for_table(lhs):
for (dest_table, lhs_col, col, join) in self.joins.get(table, []): for (rhs, lhs_col, rhs_col, join) in self.joins.get(lhs, []):
tables_to_process.remove(alias_mapping[dest_table]) tables_to_process.remove(alias_mapping[rhs])
query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \ from_clause.append(' %s %s ON ("%s"."%s" = "%s"."%s"' % \
(join, alias_mapping[dest_table], table, lhs_col, dest_table, col) (join, alias_mapping[rhs], lhs, lhs_col, rhs, rhs_col))
query_from = add_joins_for_table(dest_table, query_from) extra = self.extras.get((lhs, (rhs, lhs_col, rhs_col, join)))
return query_from 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: for pos, table in enumerate(tables_to_process):
query_from += table if pos > 0:
from_clause.append(',')
from_clause.append(table)
table_alias = get_alias_from_query(table)[1] table_alias = get_alias_from_query(table)[1]
if table_alias in self.joins: if table_alias in self.joins:
query_from = add_joins_for_table(table_alias, query_from) add_joins_for_table(table_alias)
query_from += ','
query_from = query_from[:-1] # drop last comma return "".join(from_clause), " AND ".join(self.where_clause), from_params + self.where_clause_params
return query_from, " AND ".join(self.where_clause), self.where_clause_params
def __str__(self): def __str__(self):
return '<osv.Query: "SELECT ... FROM %s WHERE %s" with params: %r>' % self.get_sql() return '<osv.Query: "SELECT ... FROM %s WHERE %s" with params: %r>' % self.get_sql()