[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:
committed by
Raphael Collet
parent
44efcf8066
commit
9593a2930f
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user