[FIX] website, test_website: prevent fuzzy search to crash with inherits
This commit ensures that when searching on a `inherits` field of a model (like `name` of `website.page`), it works properly when `pg_trgm` is activated. Indeed, `name` is a field of `website.page` record but only at the ORM level, not in SQL, due to how `inherits` works. So, when the `pg_trgm` extension is enabled, it will switch from ORM queries to raw SQL query (to use the native SQL similarity feature and not our custom python/orm one, as obviously the SQL one is better, more powerfull/accurate and faster). But this will actually make the code fail when searching on fields from a model which has `inherits` and that you search on that `inherits` model fields. Note that in 15.2, the `pg_trgm` extension is auto installed when possible thanks to [1] and [2]. [1]: https://github.com/odoo/odoo/commit/eedf37d6e286b995c47b946be1a6b66817094eff [2]: https://github.com/odoo/odoo/commit/75e6b645acdc95aece506ce0249fd0760838281c opw-3063592 closes odoo/odoo#107178 X-original-commit: 3d978caad9de2010603581687ee39a5240b74074 Signed-off-by: Romain Derie (rde) <rde@odoo.com> Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import psycopg2
|
||||
|
||||
from odoo.addons.website.controllers.main import Website
|
||||
from odoo.addons.website.tools import MockRequest
|
||||
@@ -72,3 +73,26 @@ class TestAutoComplete(TransactionCase):
|
||||
|
||||
# There are no "X*" records
|
||||
self._autocomplete('XEF1000', 0, "Not found")
|
||||
|
||||
def test_02_pages_search(self):
|
||||
if not self.env.registry.has_trigram:
|
||||
try:
|
||||
self.env.cr.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
|
||||
self.env.registry.has_trigram = True
|
||||
except psycopg2.Error:
|
||||
_logger.warning("pg_trgm extension can't be installed, which is required to run this test")
|
||||
return
|
||||
|
||||
with MockRequest(self.env, website=self.env['website'].browse(1)):
|
||||
# This should not crash. This ensures that when searching on `name`
|
||||
# field of `website.page` model, it works properly when `pg_trgm` is
|
||||
# activated.
|
||||
# Indeed, `name` is a field of `website.page` record but only at the
|
||||
# ORM level, not in SQL, due to how `inherits` works.
|
||||
self.env['website'].browse(1)._search_with_fuzzy(
|
||||
'pages', 'test', limit=5, order='name asc, website_id desc, id', options={
|
||||
'displayDescription': False, 'displayDetail': False,
|
||||
'displayExtraDetail': False, 'displayExtraLink': False,
|
||||
'displayImage': False, 'allowFuzzy': True
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1611,35 +1611,72 @@ class Website(models.Model):
|
||||
fields = set(fields).intersection(model._fields)
|
||||
|
||||
unaccent = get_unaccent_wrapper(self.env.cr)
|
||||
similarities = [sql.SQL("word_similarity({search}, {field})").format(
|
||||
search=unaccent(sql.Placeholder('search')),
|
||||
# Specific handling for website.page that inherits its arch_db and name fields
|
||||
# TODO make more generic
|
||||
field=unaccent(sql.SQL("{table}.{field}").format(
|
||||
table=sql.Identifier((self.env['ir.ui.view'] if field == 'arch_db' or (field == 'name' and 'arch_db' in fields) else model)._table),
|
||||
field=sql.Identifier(field)
|
||||
)) if not (self.env['ir.ui.view'] if field == 'arch_db' or (field == 'name' and 'arch_db' in fields) else model)._fields[field].translate else
|
||||
unaccent(sql.SQL("COALESCE({table}.{field}->>{lang}, {table}.{field}->>'en_US')").format(
|
||||
table=sql.Identifier((self.env['ir.ui.view'] if field == 'arch_db' or (field == 'name' and 'arch_db' in fields) else model)._table),
|
||||
field=sql.Identifier(field),
|
||||
lang=sql.Literal(lang)
|
||||
))
|
||||
) for field in fields]
|
||||
|
||||
# Specific handling for fields being actually part of another model
|
||||
# through the `inherits` mechanism.
|
||||
# It gets the list of fields requested to search upon and that are
|
||||
# actually not part of the requested model itself but part of a
|
||||
# `inherits` model:
|
||||
# {
|
||||
# 'name': {
|
||||
# 'table': 'ir_ui_view',
|
||||
# 'fname': 'view_id',
|
||||
# },
|
||||
# 'url': {
|
||||
# 'table': 'ir_ui_view',
|
||||
# 'fname': 'view_id',
|
||||
# },
|
||||
# 'another_field': {
|
||||
# 'table': 'another_table',
|
||||
# 'fname': 'record_id',
|
||||
# },
|
||||
# }
|
||||
inherits_fields = {
|
||||
inherits_model_fname: {
|
||||
'table': self.env[inherits_model_name]._table,
|
||||
'fname': inherits_field_name,
|
||||
}
|
||||
for inherits_model_name, inherits_field_name in model._inherits.items()
|
||||
for inherits_model_fname in self.env[inherits_model_name]._fields.keys()
|
||||
if inherits_model_fname in fields
|
||||
}
|
||||
similarities = []
|
||||
for field in fields:
|
||||
# Field might belong to another model (`inherits` mechanism)
|
||||
table = inherits_fields[field]['table'] if field in inherits_fields else model._table
|
||||
similarities.append(
|
||||
sql.SQL("word_similarity({search}, {field})").format(
|
||||
search=unaccent(sql.Placeholder('search')),
|
||||
field=unaccent(sql.SQL("{table}.{field}").format(
|
||||
table=sql.Identifier(table),
|
||||
field=sql.Identifier(field)
|
||||
)) if not model._fields[field].translate else
|
||||
unaccent(sql.SQL("COALESCE({table}.{field}->>{lang}, {table}.{field}->>'en_US')").format(
|
||||
table=sql.Identifier(table),
|
||||
field=sql.Identifier(field),
|
||||
lang=sql.Literal(lang)
|
||||
)),
|
||||
)
|
||||
)
|
||||
|
||||
best_similarity = sql.SQL('GREATEST({similarities})').format(
|
||||
similarities=sql.SQL(', ').join(similarities)
|
||||
)
|
||||
|
||||
from_clause = sql.SQL("FROM {table}").format(table=sql.Identifier(model._table))
|
||||
# Specific handling for website.page that inherits its arch_db and name fields
|
||||
# TODO make more generic
|
||||
if 'arch_db' in fields:
|
||||
# Specific handling for fields being actually part of another model
|
||||
# through the `inherits` mechanism.
|
||||
for table_to_join in {
|
||||
field['table']: field['fname'] for field in inherits_fields.values()
|
||||
}.items(): # Removes duplicate inherits model
|
||||
from_clause = sql.SQL("""
|
||||
{from_clause}
|
||||
LEFT JOIN {view_table} ON {table}.view_id = {view_table}.id
|
||||
LEFT JOIN {inherits_table} ON {table}.{inherits_field} = {inherits_table}.id
|
||||
""").format(
|
||||
from_clause=from_clause,
|
||||
table=sql.Identifier(model._table),
|
||||
view_table=sql.Identifier(self.env['ir.ui.view']._table),
|
||||
inherits_table=sql.Identifier(table_to_join[0]),
|
||||
inherits_field=sql.Identifier(table_to_join[1]),
|
||||
)
|
||||
query = sql.SQL("""
|
||||
SELECT {table}.id, {best_similarity} AS _best_similarity
|
||||
|
||||
Reference in New Issue
Block a user