From 37fb022bee3c741ef038a66373af57327c8cf763 Mon Sep 17 00:00:00 2001 From: Ernesto Tejeda Date: Wed, 25 Mar 2020 17:09:14 -0400 Subject: [PATCH] base_search_fuzzy: black, isort --- base_search_fuzzy/__manifest__.py | 29 ++-- base_search_fuzzy/models/ir_model.py | 43 +++--- base_search_fuzzy/models/trgm_index.py | 131 ++++++++++-------- .../tests/test_query_generation.py | 73 +++++----- base_search_fuzzy/views/trgm_index.xml | 32 ++--- 5 files changed, 153 insertions(+), 155 deletions(-) diff --git a/base_search_fuzzy/__manifest__.py b/base_search_fuzzy/__manifest__.py index 4a95e43d8..4447e1fc4 100644 --- a/base_search_fuzzy/__manifest__.py +++ b/base_search_fuzzy/__manifest__.py @@ -2,20 +2,17 @@ # Copyright 2016 Serpent Consulting Services Pvt. Ltd. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': "Fuzzy Search", - 'summary': "Fuzzy search with the PostgreSQL trigram extension", - 'category': 'Uncategorized', - 'version': '12.0.1.0.0', - 'website': 'https://github.com/OCA/server-tools', - 'author': 'bloopark systems GmbH & Co. KG, ' - 'Eficent, ' - 'Serpent CS, ' - 'Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'depends': ['base'], - 'data': [ - 'views/trgm_index.xml', - 'security/ir.model.access.csv', - ], - 'installable': True, + "name": "Fuzzy Search", + "summary": "Fuzzy search with the PostgreSQL trigram extension", + "category": "Uncategorized", + "version": "12.0.1.0.0", + "website": "https://github.com/OCA/server-tools", + "author": "bloopark systems GmbH & Co. KG, " + "Eficent, " + "Serpent CS, " + "Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["base"], + "data": ["views/trgm_index.xml", "security/ir.model.access.csv",], + "installable": True, } diff --git a/base_search_fuzzy/models/ir_model.py b/base_search_fuzzy/models/ir_model.py index c89958284..a113c5997 100644 --- a/base_search_fuzzy/models/ir_model.py +++ b/base_search_fuzzy/models/ir_model.py @@ -7,7 +7,6 @@ import logging from odoo import _, api, models from odoo.osv import expression - _logger = logging.getLogger(__name__) @@ -18,26 +17,23 @@ def patch_leaf_trgm(method): left, operator, right = leaf table_alias = '"%s"' % (eleaf.generate_alias()) - if operator == '%': + if operator == "%": - sql_operator = '%%' + sql_operator = "%%" params = [] if left in model._fields: - column = '%s.%s' % (table_alias, expression._quote(left)) - query = '(%s %s %s)' % ( + column = "{}.{}".format(table_alias, expression._quote(left)) + query = "({} {} {})".format( column, sql_operator, model._fields[left].column_format, ) elif left in models.MAGIC_COLUMNS: - query = "(%s.\"%s\" %s %%s)" % ( - table_alias, left, sql_operator) + query = '({}."{}" {} %s)'.format(table_alias, left, sql_operator) params = right else: # Must not happen - raise ValueError(_( - "Invalid field %r in domain term %r" % (left, leaf) - )) + raise ValueError(_("Invalid field {!r} in domain term {!r}".format(left, leaf))) if left in model._fields: params = str(right) @@ -45,8 +41,8 @@ def patch_leaf_trgm(method): if isinstance(params, str): params = [params] return query, params - elif operator == 'inselect': - right = (right[0].replace(' % ', ' %% '), right[1]) + elif operator == "inselect": + right = (right[0].replace(" % ", " %% "), right[1]) eleaf.leaf = (left, operator, right) return method(self, eleaf) @@ -56,11 +52,10 @@ def patch_leaf_trgm(method): def patch_generate_order_by(method): - @api.model def decorate_generate_order_by(self, order_spec, query): - if order_spec and order_spec.startswith('similarity('): - return ' ORDER BY ' + order_spec + if order_spec and order_spec.startswith("similarity("): + return " ORDER BY " + order_spec return method(self, order_spec, query) decorate_generate_order_by.__decorated__ = True @@ -70,22 +65,22 @@ def patch_generate_order_by(method): class IrModel(models.Model): - _inherit = 'ir.model' + _inherit = "ir.model" @api.model_cr def _register_hook(self): # We have to prevent wrapping the function twice to avoid recursion # errors - if not hasattr(expression.expression._expression__leaf_to_sql, - '__decorated__'): + if not hasattr(expression.expression._expression__leaf_to_sql, "__decorated__"): expression.expression._expression__leaf_to_sql = patch_leaf_trgm( - expression.expression._expression__leaf_to_sql) + expression.expression._expression__leaf_to_sql + ) - if '%' not in expression.TERM_OPERATORS: - expression.TERM_OPERATORS += ('%',) + if "%" not in expression.TERM_OPERATORS: + expression.TERM_OPERATORS += ("%",) - if not hasattr(models.BaseModel._generate_order_by, - '__decorated__'): + if not hasattr(models.BaseModel._generate_order_by, "__decorated__"): models.BaseModel._generate_order_by = patch_generate_order_by( - models.BaseModel._generate_order_by) + models.BaseModel._generate_order_by + ) return super(IrModel, self)._register_hook() diff --git a/base_search_fuzzy/models/trgm_index.py b/base_search_fuzzy/models/trgm_index.py index 1d5aaf4d7..90d256458 100644 --- a/base_search_fuzzy/models/trgm_index.py +++ b/base_search_fuzzy/models/trgm_index.py @@ -4,10 +4,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from odoo import _, api, exceptions, fields, models - from psycopg2.extensions import AsIs +from odoo import _, api, exceptions, fields, models + _logger = logging.getLogger(__name__) @@ -15,76 +15,81 @@ class TrgmIndex(models.Model): """Model for Trigram Index.""" - _name = 'trgm.index' - _rec_name = 'field_id' - _description = 'Trigram Index' + _name = "trgm.index" + _rec_name = "field_id" + _description = "Trigram Index" field_id = fields.Many2one( - comodel_name='ir.model.fields', - string='Field', + comodel_name="ir.model.fields", + string="Field", required=True, - help='You can either select a field of type "text" or "char".' + help='You can either select a field of type "text" or "char".', ) index_name = fields.Char( - string='Index Name', + string="Index Name", readonly=True, - help='The index name is automatically generated like ' - 'fieldname_indextype_idx. If the index already exists and the ' - 'index is located in the same table then this index is reused. ' - 'If the index is located in another table then a number is added ' - 'at the end of the index name.' + help="The index name is automatically generated like " + "fieldname_indextype_idx. If the index already exists and the " + "index is located in the same table then this index is reused. " + "If the index is located in another table then a number is added " + "at the end of the index name.", ) index_type = fields.Selection( - selection=[('gin', 'GIN'), ('gist', 'GiST')], - string='Index Type', - default='gin', + selection=[("gin", "GIN"), ("gist", "GiST")], + string="Index Type", + default="gin", required=True, help='Cite from PostgreSQL documentation: "As a rule of thumb, a ' - 'GIN index is faster to search than a GiST index, but slower to ' - 'build or update; so GIN is better suited for static data and ' - 'GiST for often-updated data."' + "GIN index is faster to search than a GiST index, but slower to " + "build or update; so GIN is better suited for static data and " + 'GiST for often-updated data."', ) @api.model_cr def _trgm_extension_exists(self): - self.env.cr.execute(""" + self.env.cr.execute( + """ SELECT name, installed_version FROM pg_available_extensions WHERE name = 'pg_trgm' LIMIT 1; - """) + """ + ) extension = self.env.cr.fetchone() if extension is None: - return 'missing' + return "missing" if extension[1] is None: - return 'uninstalled' + return "uninstalled" - return 'installed' + return "installed" @api.model_cr def _is_postgres_superuser(self): self.env.cr.execute("SHOW is_superuser;") superuser = self.env.cr.fetchone() - return superuser is not None and superuser[0] == 'on' or False + return superuser is not None and superuser[0] == "on" or False @api.model_cr def _install_trgm_extension(self): extension = self._trgm_extension_exists() - if extension == 'missing': - _logger.warning('To use pg_trgm you have to install the ' - 'postgres-contrib module.') - elif extension == 'uninstalled': + if extension == "missing": + _logger.warning( + "To use pg_trgm you have to install the " "postgres-contrib module." + ) + elif extension == "uninstalled": if self._is_postgres_superuser(): self.env.cr.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm;") return True else: - _logger.warning('To use pg_trgm you have to create the ' - 'extension pg_trgm in your database or you ' - 'have to be the superuser.') + _logger.warning( + "To use pg_trgm you have to create the " + "extension pg_trgm in your database or you " + "have to be the superuser." + ) else: return True return False @@ -93,8 +98,10 @@ class TrgmIndex(models.Model): def _auto_init(self): res = super(TrgmIndex, self)._auto_init() if self._install_trgm_extension(): - _logger.info('The pg_trgm is loaded in the database and the ' - 'fuzzy search can be used.') + _logger.info( + "The pg_trgm is loaded in the database and the " + "fuzzy search can be used." + ) return res @api.model_cr @@ -103,18 +110,20 @@ class TrgmIndex(models.Model): new_index_name = index_name + str(inc) else: new_index_name = index_name - self.env.cr.execute(""" + self.env.cr.execute( + """ SELECT tablename, indexname FROM pg_indexes WHERE indexname = %(index)s; - """, {'index': new_index_name}) + """, + {"index": new_index_name}, + ) indexes = self.env.cr.fetchone() if indexes is not None and indexes[0] == table_name: return True, index_name elif indexes is not None: - return self.get_not_used_index(index_name, table_name, - inc + 1) + return self.get_not_used_index(index_name, table_name, inc + 1) return False, new_index_name @@ -123,39 +132,42 @@ class TrgmIndex(models.Model): self.ensure_one() if not self._install_trgm_extension(): - raise exceptions.UserError(_( - 'The pg_trgm extension does not exists or cannot be ' - 'installed.')) + raise exceptions.UserError( + _("The pg_trgm extension does not exists or cannot be " "installed.") + ) table_name = self.env[self.field_id.model_id.model]._table column_name = self.field_id.name index_type = self.index_type - index_name = '%s_%s_idx' % (column_name, index_type) - index_exists, index_name = self.get_not_used_index( - index_name, table_name) + index_name = "{}_{}_idx".format(column_name, index_type) + index_exists, index_name = self.get_not_used_index(index_name, table_name) if not index_exists: - self.env.cr.execute(""" + self.env.cr.execute( + """ CREATE INDEX %(index)s ON %(table)s USING %(indextype)s (%(column)s %(indextype)s_trgm_ops); - """, { - 'table': AsIs(table_name), - 'index': AsIs(index_name), - 'column': AsIs(column_name), - 'indextype': AsIs(index_type) - }) + """, + { + "table": AsIs(table_name), + "index": AsIs(index_name), + "column": AsIs(column_name), + "indextype": AsIs(index_type), + }, + ) return index_name @api.model def index_exists(self, model_name, field_name): - field = self.env['ir.model.fields'].search([ - ('model', '=', model_name), ('name', '=', field_name)], limit=1) + field = self.env["ir.model.fields"].search( + [("model", "=", model_name), ("name", "=", field_name)], limit=1 + ) if not field: return False - trgm_index = self.search([('field_id', '=', field.id)], limit=1) + trgm_index = self.search([("field_id", "=", field.id)], limit=1) return bool(trgm_index) @api.model @@ -167,9 +179,10 @@ class TrgmIndex(models.Model): @api.multi def unlink(self): for rec in self: - self.env.cr.execute(""" + self.env.cr.execute( + """ DROP INDEX IF EXISTS %(index)s; - """, { - 'index': AsIs(rec.index_name), - }) + """, + {"index": AsIs(rec.index_name),}, + ) return super(TrgmIndex, self).unlink() diff --git a/base_search_fuzzy/tests/test_query_generation.py b/base_search_fuzzy/tests/test_query_generation.py index 25dce0bc6..9464f6379 100644 --- a/base_search_fuzzy/tests/test_query_generation.py +++ b/base_search_fuzzy/tests/test_query_generation.py @@ -8,22 +8,20 @@ from odoo.tests.common import TransactionCase, at_install, post_install @at_install(False) @post_install(True) class QueryGenerationCase(TransactionCase): - def setUp(self): super(QueryGenerationCase, self).setUp() - self.ResPartner = self.env['res.partner'] - self.TrgmIndex = self.env['trgm.index'] - self.ResPartnerCategory = self.env['res.partner.category'] + self.ResPartner = self.env["res.partner"] + self.TrgmIndex = self.env["trgm.index"] + self.ResPartnerCategory = self.env["res.partner.category"] def test_fuzzy_where_generation(self): """Check the generation of the where clause.""" # the added fuzzy search operator should be available in the allowed # operators - self.assertIn('%', expression.TERM_OPERATORS) + self.assertIn("%", expression.TERM_OPERATORS) # create new query with fuzzy search operator - query = self.ResPartner._where_calc( - [('name', '%', 'test')], active_test=False) + query = self.ResPartner._where_calc([("name", "%", "test")], active_test=False) from_clause, where_clause, where_clause_params = query.get_sql() # the % parameter has to be escaped (%%) for the string replation @@ -32,70 +30,65 @@ class QueryGenerationCase(TransactionCase): # test the right sql query statement creation # now there should be only one '%' complete_where = self.env.cr.mogrify( - "SELECT FROM %s WHERE %s" % (from_clause, where_clause), - where_clause_params) + "SELECT FROM {} WHERE {}".format(from_clause, where_clause), where_clause_params + ) self.assertEqual( complete_where, - b'SELECT FROM "res_partner" WHERE ' - b'("res_partner"."name" % \'test\')') + b'SELECT FROM "res_partner" WHERE ' b'("res_partner"."name" % \'test\')', + ) def test_fuzzy_where_generation_translatable(self): """Check the generation of the where clause for translatable fields.""" - ctx = {'lang': 'de_DE'} + ctx = {"lang": "de_DE"} # create new query with fuzzy search operator - query = self.ResPartnerCategory.with_context(ctx)\ - ._where_calc([('name', '%', 'Goschaeftlic')], active_test=False) + query = self.ResPartnerCategory.with_context(ctx)._where_calc( + [("name", "%", "Goschaeftlic")], active_test=False + ) from_clause, where_clause, where_clause_params = query.get_sql() # the % parameter has to be escaped (%%) for the string replation - self.assertIn("""SELECT id FROM temp_irt_current WHERE name %% %s""", - where_clause) + self.assertIn( + """SELECT id FROM temp_irt_current WHERE name %% %s""", where_clause + ) complete_where = self.env.cr.mogrify( - "SELECT FROM %s WHERE %s" % (from_clause, where_clause), - where_clause_params) + "SELECT FROM {} WHERE {}".format(from_clause, where_clause), where_clause_params + ) self.assertIn( b"""SELECT id FROM temp_irt_current WHERE name % 'Goschaeftlic'""", - complete_where) + complete_where, + ) def test_fuzzy_order_generation(self): """Check the generation of the where clause.""" order = "similarity(%s.name, 'test') DESC" % self.ResPartner._table - query = self.ResPartner._where_calc( - [('name', '%', 'test')], active_test=False) + query = self.ResPartner._where_calc([("name", "%", "test")], active_test=False) order_by = self.ResPartner._generate_order_by(order, query) - self.assertEqual(' ORDER BY %s' % order, order_by) + self.assertEqual(" ORDER BY %s" % order, order_by) def test_fuzzy_search(self): """Test the fuzzy search itself.""" - if self.TrgmIndex._trgm_extension_exists() != 'installed': + if self.TrgmIndex._trgm_extension_exists() != "installed": return - if not self.TrgmIndex.index_exists('res.partner', 'name'): - field_partner_name = self.env.ref('base.field_res_partner__name') - self.TrgmIndex.create({ - 'field_id': field_partner_name.id, - 'index_type': 'gin', - }) + if not self.TrgmIndex.index_exists("res.partner", "name"): + field_partner_name = self.env.ref("base.field_res_partner__name") + self.TrgmIndex.create( + {"field_id": field_partner_name.id, "index_type": "gin",} + ) - partner1 = self.ResPartner.create({ - 'name': 'John Smith' - }) - partner2 = self.ResPartner.create( - {'name': 'John Smizz'} - ) - partner3 = self.ResPartner.create({ - 'name': 'Linus Torvalds' - }) + partner1 = self.ResPartner.create({"name": "John Smith"}) + partner2 = self.ResPartner.create({"name": "John Smizz"}) + partner3 = self.ResPartner.create({"name": "Linus Torvalds"}) - res = self.ResPartner.search([('name', '%', 'Jon Smith')]) + res = self.ResPartner.search([("name", "%", "Jon Smith")]) self.assertIn(partner1.id, res.ids) self.assertIn(partner2.id, res.ids) self.assertNotIn(partner3.id, res.ids) - res = self.ResPartner.search([('name', '%', 'Smith John')]) + res = self.ResPartner.search([("name", "%", "Smith John")]) self.assertIn(partner1.id, res.ids) self.assertIn(partner2.id, res.ids) self.assertNotIn(partner3.id, res.ids) diff --git a/base_search_fuzzy/views/trgm_index.xml b/base_search_fuzzy/views/trgm_index.xml index e7efa51eb..ce8cb3f63 100644 --- a/base_search_fuzzy/views/trgm_index.xml +++ b/base_search_fuzzy/views/trgm_index.xml @@ -1,6 +1,5 @@ - + - trgm.index.view.form trgm.index @@ -8,27 +7,28 @@
- - - + + +
- trgm.index.view.tree trgm.index - - - + + + - Trigram Index trgm.index @@ -36,10 +36,10 @@ tree,form ir.actions.act_window - - - +