diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 48344b90e44..22efc376bad 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -1616,69 +1616,94 @@ class Website(models.Model): model = self.env[model_name] if search_detail.get('requires_sudo'): model = model.sudo() - fields = ['arch_db' if field == 'view_id.arch_db' else field for field in fields if field in model or field == 'view_id.arch_db'] - if model_name == 'website.page': - fields.remove('name') # TODO handle ? + domain = search_detail['base_domain'].copy() + fields = set(fields).intersection(model._fields) unaccent = get_unaccent_sql_wrapper(self.env.cr) similarities = [sql.SQL("word_similarity({search}, {field})").format( search=unaccent(sql.Placeholder('search')), - field=sql.SQL("{table}.{field}").format( - table=sql.Identifier((self.env['ir.ui.view'] if field == 'arch_db' else model)._table), - field=unaccent(sql.Identifier(field)) - ) + # 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) + )) ) for field in fields] 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: from_clause = sql.SQL(""" {from_clause} - LEFT JOIN {view_table} ON {table}.{view_id} = {view_table}.{id} + LEFT JOIN {view_table} ON {table}.view_id = {view_table}.id """).format( from_clause=from_clause, table=sql.Identifier(model._table), - view_id=sql.Identifier('view_id'), view_table=sql.Identifier(self.env['ir.ui.view']._table), - id=sql.Identifier('id'), ) query = sql.SQL(""" - SELECT {table}.{id}, {best_similarity} AS _best_similarity + SELECT {table}.id, {best_similarity} AS _best_similarity {from_clause} ORDER BY _best_similarity desc LIMIT 1000 """).format( table=sql.Identifier(model._table), - id=sql.Identifier('id'), best_similarity=best_similarity, from_clause=from_clause, ) self.env.cr.execute(query, {'search': search}) ids = {row[0] for row in self.env.cr.fetchall() if row[1] >= similarity_threshold} if self.env.lang: - similarity = sql.SQL("word_similarity({search}, {field})").format( - search=unaccent(sql.Placeholder('search')), - field=unaccent(sql.Identifier('value')) - ) - names = ['%s,%s' % ((self.env['ir.ui.view'] if field == 'arch_db' else model)._name, field) for field in fields] - query = sql.SQL(""" - SELECT res_id, {similarity} AS _similarity - FROM ir_translation - WHERE lang = {lang} - AND name = ANY({names}) - AND type = 'model' - ORDER BY _similarity desc - LIMIT 1000 - """).format( - similarity=similarity, - lang=sql.Placeholder('lang'), - names=sql.Placeholder('names'), - ) - self.env.cr.execute(query, {'lang': self.env.lang, 'names' :names, 'search': search}) + # Specific handling for website.page that inherits its arch_db and name fields + # TODO make more generic + if 'arch_db' in fields: + # Look for partial translations + similarity = sql.SQL("word_similarity({search}, {field})").format( + search=unaccent(sql.Placeholder('search')), + field=unaccent(sql.SQL('t.value')) + ) + names = ['%s,%s' % (self.env['ir.ui.view']._name, field) for field in fields] + query = sql.SQL(""" + SELECT {table}.id, {similarity} AS _similarity + FROM {table} + LEFT JOIN ir_ui_view v ON {table}.view_id = v.id + LEFT JOIN ir_translation t ON v.id = t.res_id + WHERE t.lang = {lang} + AND t.name = ANY({names}) + AND t.type = 'model_terms' + ORDER BY _similarity desc + LIMIT 1000 + """).format( + table=sql.Identifier(model._table), + similarity=similarity, + lang=sql.Placeholder('lang'), + names=sql.Placeholder('names'), + ) + else: + similarity = sql.SQL("word_similarity({search}, {field})").format( + search=unaccent(sql.Placeholder('search')), + field=unaccent(sql.SQL('value')) + ) + names = ['%s,%s' % (model._name, field) for field in fields] + query = sql.SQL(""" + SELECT res_id, {similarity} AS _similarity + FROM ir_translation + WHERE lang = {lang} + AND name = ANY({names}) + AND type = 'model' + ORDER BY _similarity desc + LIMIT 1000 + """).format( + similarity=similarity, + lang=sql.Placeholder('lang'), + names=sql.Placeholder('names'), + ) + self.env.cr.execute(query, {'lang': self.env.lang, 'names': names, 'search': search}) ids.update(row[0] for row in self.env.cr.fetchall() if row[1] >= similarity_threshold) - domain = search_detail['base_domain'].copy() domain.append([('id', 'in', list(ids))]) domain = AND(domain) records = model.search_read(domain, fields, limit=limit) @@ -1686,8 +1711,6 @@ class Website(models.Model): for field, value in record.items(): if isinstance(value, str): value = value.lower() - if field == 'arch_db': - value = text_from_html(value) yield from re.findall(match_pattern, value) def _basic_enumerate_words(self, search_details, search, limit): @@ -1709,7 +1732,7 @@ class Website(models.Model): model = model.sudo() domain = search_detail['base_domain'].copy() fields_domain = [] - fields = ['arch_db' if field == 'view_id.arch_db' else field for field in fields if field in model or field == 'view_id.arch_db'] + fields = set(fields).intersection(model._fields) for field in fields: fields_domain.append([(field, '=ilike', '%s%%' % first)]) fields_domain.append([(field, '=ilike', '%% %s%%' % first)]) diff --git a/addons/website/models/website_page.py b/addons/website/models/website_page.py index 3407a84542a..4d8b28201c9 100644 --- a/addons/website/models/website_page.py +++ b/addons/website/models/website_page.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from psycopg2 import sql import re from odoo.addons.http_routing.models.ir_http import slugify from odoo.addons.website.tools import text_from_html from odoo import api, fields, models +from odoo.osv import expression +from odoo.tools import escape_psql from odoo.tools.safe_eval import safe_eval @@ -254,7 +257,7 @@ class Page(models.Model): 'website_url': {'name': 'url', 'type': 'text'}, } if with_description: - search_fields.append('view_id.arch_db') + search_fields.append('arch_db') fetch_fields.append('arch') mapping['description'] = {'name': 'arch', 'type': 'text', 'html': True, 'match': True} return { @@ -271,6 +274,48 @@ class Page(models.Model): def _search_fetch(self, search_detail, search, limit, order): with_description = 'description' in search_detail['mapping'] results, count = super()._search_fetch(search_detail, search, limit, order) + if with_description and search: + # Perform search in translations + # TODO Remove when domains will support xml_translate fields + query = sql.SQL(""" + SELECT {table}.{id} + FROM {table} + LEFT JOIN ir_ui_view v ON {table}.{view_id} = v.{id} + LEFT JOIN ir_translation t ON v.{id} = t.{res_id} + WHERE t.lang = {lang} + AND t.name = ANY({names}) + AND t.type = 'model_terms' + AND t.value ilike {search} + LIMIT {limit} + """).format( + table=sql.Identifier(self._table), + id=sql.Identifier('id'), + view_id=sql.Identifier('view_id'), + res_id=sql.Identifier('res_id'), + lang=sql.Placeholder('lang'), + names=sql.Placeholder('names'), + search=sql.Placeholder('search'), + limit=sql.Placeholder('limit'), + ) + self.env.cr.execute(query, { + 'lang': self.env.lang, + 'names': ['ir.ui.view,arch_db', 'ir.ui.view,name'], + 'search': '%%%s%%' % escape_psql(search), + 'limit': limit, + }) + ids = {row[0] for row in self.env.cr.fetchall()} + ids.update(results.ids) + domains = search_detail['base_domain'].copy() + domains.append([('id', 'in', list(ids))]) + domain = expression.AND(domains) + model = self.sudo() if search_detail.get('requires_sudo') else self + results = model.search( + domain, + limit=limit, + order=search_detail.get('order', order) + ) + count = max(count, len(results)) + def filter_page(search, page, all_pages): # Search might have matched words in the xml tags and parameters therefore we make # sure the terms actually appear inside the text.