From b88bb72beb65bab9d86abedfddf8fd97eb8110b5 Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Mon, 9 Aug 2021 13:02:27 +0000 Subject: [PATCH] [IMP] website: search inside translated pages Before this commit the search on pages was only done in the main language. After this commit the search on pages is based on the translated "arch" field. task-2379555 https://github.com/odoo/odoo/pull/65871 Part-of: odoo/odoo#65871 --- addons/website/models/website.py | 93 +++++++++++++++++---------- addons/website/models/website_page.py | 47 +++++++++++++- 2 files changed, 104 insertions(+), 36 deletions(-) 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.