[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'})
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():
+30 -2
View File
@@ -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:
+52 -18
View File
@@ -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 '<osv.Query: "SELECT ... FROM %s WHERE %s" with params: %r>' % self.get_sql()