From 72a25a0bcb94a9f898f8d54534412d233e9ae378 Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Tue, 21 Jun 2016 14:56:08 +0200 Subject: [PATCH 1/8] [ADD] First implementation of base_trgm_search for creating indexes and allowing the sql parameter %. --- base_trgm_search/__init__.py | 2 + base_trgm_search/__openerp__.py | 17 +++ base_trgm_search/models/__init__.py | 2 + base_trgm_search/models/trgm_index.py | 209 ++++++++++++++++++++++++++ base_trgm_search/views/trgm_index.xml | 46 ++++++ 5 files changed, 276 insertions(+) create mode 100644 base_trgm_search/__init__.py create mode 100644 base_trgm_search/__openerp__.py create mode 100644 base_trgm_search/models/__init__.py create mode 100644 base_trgm_search/models/trgm_index.py create mode 100644 base_trgm_search/views/trgm_index.xml diff --git a/base_trgm_search/__init__.py b/base_trgm_search/__init__.py new file mode 100644 index 00000000000..a0fdc10fe11 --- /dev/null +++ b/base_trgm_search/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/base_trgm_search/__openerp__.py b/base_trgm_search/__openerp__.py new file mode 100644 index 00000000000..3359ac4496d --- /dev/null +++ b/base_trgm_search/__openerp__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +{ + 'name': "PostgreSQL Trigram Search", + 'summary': """PostgreSQL Trigram Search""", + 'category': 'Uncategorized', + 'version': '8.0.1.0.0', + 'website': 'https://odoo-community.org/', + 'author': 'Daniel Reis, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'depends': [ + 'base', + ], + 'data': [ + 'views/trgm_index.xml', + ], + 'installable': True, +} diff --git a/base_trgm_search/models/__init__.py b/base_trgm_search/models/__init__.py new file mode 100644 index 00000000000..83d2dfdc368 --- /dev/null +++ b/base_trgm_search/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import trgm_index diff --git a/base_trgm_search/models/trgm_index.py b/base_trgm_search/models/trgm_index.py new file mode 100644 index 00000000000..c30c13ada28 --- /dev/null +++ b/base_trgm_search/models/trgm_index.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +import logging + +from openerp import api, exceptions, fields, models +from openerp.osv import expression + +from psycopg2.extensions import AsIs + +_logger = logging.getLogger(__name__) + + +class TrgmIndex(models.Model): + + """Model for Trigram Index.""" + + _name = 'trgm.index' + _rec_name = 'field_id' + + field_id = fields.Many2one( + comodel_name='ir.model.fields', + string='Field', + required=True, + help='You can either select a field of type "text" or "char".' + ) + + index_name = fields.Char( + 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 resused. ' + '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', + 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."' + ) + + @api.model + def _trgm_extension_exists(self): + 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' + + if extension[1] is None: + return 'uninstalled' + + return 'installed' + + @api.model + 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 + + @api.model + 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 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.') + else: + return True + return False + + @api.model + def _auto_init(self): + res = super(TrgmIndex, self)._auto_init() + self._install_trgm_extension() + return res + + @api.model + def get_not_used_index(self, index_name, table_name, inc=1): + if inc > 1: + new_index_name = index_name + str(inc) + else: + new_index_name = index_name + self.env.cr.execute(""" + SELECT tablename, indexname + FROM pg_indexes + WHERE indexname = %(index)s; + """, {'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 False, new_index_name + + @api.multi + def create_index(self): + self.ensure_one() + + if not self._install_trgm_extension(): + 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) + + if not index_exists: + 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) + }) + 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) + + if not field: + return False + + trgm_index = self.search([('field_id', '=', field.id)], limit=1) + return bool(trgm_index) + + @api.model + def create(self, vals): + rec = super(TrgmIndex, self).create(vals) + rec.index_name = rec.create_index() + return rec + + @api.multi + def unlink(self): + for rec in self: + self.env.cr.execute(""" + DROP INDEX IF EXISTS %(index)s; + """, { + 'index': AsIs(rec.index_name), + }) + return super(TrgmIndex, self).unlink() + + +def patch_leaf_trgm(method): + def decorate_leaf_to_sql(self, eleaf): + model = eleaf.model + leaf = eleaf.leaf + left, operator, right = leaf + table_alias = '"%s"' % (eleaf.generate_alias()) + + if operator == '%': + sql_operator = '%%' + params = [] + + if left in model._columns: + format = model._columns[left]._symbol_set[0] + column = '%s.%s' % (table_alias, expression._quote(left)) + query = '(%s %s %s)' % (column, sql_operator, format) + elif left in expression.MAGIC_COLUMNS: + query = "(%s.\"%s\" %s %%s)" % ( + table_alias, left, sql_operator) + params = right + else: # Must not happen + raise ValueError( + "Invalid field %r in domain term %r" % (left, leaf)) + + if left in model._columns: + params = model._columns[left]._symbol_set[1](right) + + if isinstance(params, basestring): + params = [params] + return query, params + elif operator == 'inselect': + right = (right[0].replace(' % ', ' %% '), right[1]) + eleaf.leaf = (left, operator, right) + return method(self, eleaf) + return decorate_leaf_to_sql +expression.expression._expression__leaf_to_sql = patch_leaf_trgm( + expression.expression._expression__leaf_to_sql) +expression.TERM_OPERATORS += ('%',) diff --git a/base_trgm_search/views/trgm_index.xml b/base_trgm_search/views/trgm_index.xml new file mode 100644 index 00000000000..b72b0668310 --- /dev/null +++ b/base_trgm_search/views/trgm_index.xml @@ -0,0 +1,46 @@ + + + + + + trgm.index.view.form + trgm.index + +
+ + + + + + + +
+
+
+ + + trgm.index.view.tree + trgm.index + + + + + + + + + + + Trigam Index + trgm.index + form + tree,form + ir.actions.act_window + + + + +
+
From ece8a4cd51956ac36395bfcb5fdc5c0916a45bff Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Wed, 22 Jun 2016 09:36:08 +0200 Subject: [PATCH 2/8] [IMP] Allow to order by similarity. --- base_trgm_search/models/trgm_index.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/base_trgm_search/models/trgm_index.py b/base_trgm_search/models/trgm_index.py index c30c13ada28..28beb769c17 100644 --- a/base_trgm_search/models/trgm_index.py +++ b/base_trgm_search/models/trgm_index.py @@ -204,6 +204,21 @@ def decorate_leaf_to_sql(self, eleaf): eleaf.leaf = (left, operator, right) return method(self, eleaf) return decorate_leaf_to_sql + expression.expression._expression__leaf_to_sql = patch_leaf_trgm( expression.expression._expression__leaf_to_sql) + expression.TERM_OPERATORS += ('%',) + + +def patch_generate_order_by(method): + def decorate_generate_order_by(self, order_spec, query): + if order_spec and order_spec.startswith('similarity('): + from pprint import pprint + pprint(query) + return ' ORDER BY ' + order_spec + return method(self, order_spec, query) + return decorate_generate_order_by + +models.BaseModel._generate_order_by = patch_generate_order_by( + models.BaseModel._generate_order_by) From c4d5d0b7c6dcacda957b40fc79f8988b46868e5a Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Fri, 24 Jun 2016 11:13:19 +0200 Subject: [PATCH 3/8] [IMP] Renamed module base_trgm_search to base_search_fuzzy, added Unit tests, added translations, added access permissions, moved the monkey patching to method _register_hook of ir.model and fixed _auto_init, added README, cleaned up some aprts --- base_search_fuzzy/README.rst | 99 +++++++++++++++++++ base_search_fuzzy/__init__.py | 3 + base_search_fuzzy/__openerp__.py | 20 ++++ base_search_fuzzy/i18n/base_search_fuzzy.pot | 94 ++++++++++++++++++ base_search_fuzzy/i18n/de.po | 94 ++++++++++++++++++ base_search_fuzzy/models/__init__.py | 4 + base_search_fuzzy/models/ir_model.py | 89 +++++++++++++++++ .../models/trgm_index.py | 67 ++----------- .../security/ir.model.access.csv | 5 + base_search_fuzzy/tests/__init__.py | 3 + .../tests/test_query_generation.py | 78 +++++++++++++++ .../views/trgm_index.xml | 5 +- base_trgm_search/__init__.py | 2 - base_trgm_search/__openerp__.py | 17 ---- base_trgm_search/models/__init__.py | 2 - 15 files changed, 499 insertions(+), 83 deletions(-) create mode 100644 base_search_fuzzy/README.rst create mode 100644 base_search_fuzzy/__init__.py create mode 100644 base_search_fuzzy/__openerp__.py create mode 100644 base_search_fuzzy/i18n/base_search_fuzzy.pot create mode 100644 base_search_fuzzy/i18n/de.po create mode 100644 base_search_fuzzy/models/__init__.py create mode 100644 base_search_fuzzy/models/ir_model.py rename {base_trgm_search => base_search_fuzzy}/models/trgm_index.py (70%) create mode 100644 base_search_fuzzy/security/ir.model.access.csv create mode 100644 base_search_fuzzy/tests/__init__.py create mode 100644 base_search_fuzzy/tests/test_query_generation.py rename {base_trgm_search => base_search_fuzzy}/views/trgm_index.xml (91%) delete mode 100644 base_trgm_search/__init__.py delete mode 100644 base_trgm_search/__openerp__.py delete mode 100644 base_trgm_search/models/__init__.py diff --git a/base_search_fuzzy/README.rst b/base_search_fuzzy/README.rst new file mode 100644 index 00000000000..cf2ef92fba9 --- /dev/null +++ b/base_search_fuzzy/README.rst @@ -0,0 +1,99 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========================= +PostgreSQL Trigram Search +========================= + +This addon provides the ability to create GIN or GiST indexes of char and text +fields and also to use the search operator `%` in search domains. Currently +this module doesn't change the backend search or something else. It provides +only the possibilty to do a fuzzy search for external addons. + + +Installation +============ + +First you need to have the ``pg_trgm`` extension available. In debian based +distribution you have to install the `postgresql-contrib` module. Then you have +to either install the ``pg_trgm`` extension to your database or you have to give +your postgresql user the superadmin right (this allows the odoo module to +install the extension to the database). + + +Configuration +============= + +If you installed the odoo module you can define ``GIN`` and ``GiST`` indexes for +`char` and `text` via `Settings -> Database Structure -> Trigram Index`. The +index name will automatically created for new entries. + + +Usage +===== + +For example you can create an index for the `name` field of `res.partner`. Then +in a search you can use + +``self.env['res.partner'].search([('name', '%', 'Jon Miller)])`` + +In this Example it can find existing names like `John Miller` or `John Mill`. + +Which strings can be found depends on the limit which is set (default: 0.3). +Currently you have to set the limit by executing SQL: + +``self.env.cr.execute("SELECT set_limit(0.2);")`` + +Also you can use the ``similarity(column, 'text')`` function in the ``order`` +parameter to order by the similarity. This is just a basic implementation which +doesn't contains validations and has to start with this function. For example +you can define this like: + +``similarity(%s.name, 'John Mil') DESC" % self.env['res.partner']._table`` + +For further questions read the Documentation of the +`pg_trgm `_ module. + +Known issues / Roadmap +====================== + +* Modify the general search parts (e.g. in tree view or many2one fields) +* add better `order by` handling + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Christoph Giesel + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/base_search_fuzzy/__init__.py b/base_search_fuzzy/__init__.py new file mode 100644 index 00000000000..66efa2ce721 --- /dev/null +++ b/base_search_fuzzy/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models diff --git a/base_search_fuzzy/__openerp__.py b/base_search_fuzzy/__openerp__.py new file mode 100644 index 00000000000..e3ef3d02497 --- /dev/null +++ b/base_search_fuzzy/__openerp__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# 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': '8.0.1.0.0', + 'website': 'https://odoo-community.org/', + 'author': 'bloopark systems GmbH & Co. KG, ' + '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/i18n/base_search_fuzzy.pot b/base_search_fuzzy/i18n/base_search_fuzzy.pot new file mode 100644 index 00000000000..03e4c6bb87b --- /dev/null +++ b/base_search_fuzzy/i18n/base_search_fuzzy.pot @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_search_fuzzy +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0alpha1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-06-24 08:47+0000\n" +"PO-Revision-Date: 2016-06-24 08:47+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_search_fuzzy +#: help:trgm.index,index_type:0 +msgid "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.\"" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,create_uid:0 +msgid "Created by" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,create_date:0 +msgid "Created on" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,field_id:0 +msgid "Field" +msgstr "" + +#. module: base_search_fuzzy +#: selection:trgm.index,index_type:0 +msgid "GIN" +msgstr "" + +#. module: base_search_fuzzy +#: selection:trgm.index,index_type:0 +msgid "GiST" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,id:0 +msgid "ID" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,index_name:0 +msgid "Index Name" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,index_type:0 +msgid "Index Type" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,write_uid:0 +msgid "Last Updated by" +msgstr "" + +#. module: base_search_fuzzy +#: field:trgm.index,write_date:0 +msgid "Last Updated on" +msgstr "" + +#. module: base_search_fuzzy +#: model:ir.model,name:base_search_fuzzy.model_ir_model +msgid "Models" +msgstr "" + +#. module: base_search_fuzzy +#: help:trgm.index,index_name:0 +msgid "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 resused. If the index is located in another table then a number is added at the end of the index name." +msgstr "" + +#. module: base_search_fuzzy +#: model:ir.actions.act_window,name:base_search_fuzzy.trgm_index_action +#: model:ir.ui.menu,name:base_search_fuzzy.trgm_index_menu +#: view:trgm.index:base_search_fuzzy.trgm_index_view_form +#: view:trgm.index:base_search_fuzzy.trgm_index_view_tree +msgid "Trigram Index" +msgstr "" + +#. module: base_search_fuzzy +#: help:trgm.index,field_id:0 +msgid "You can either select a field of type \"text\" or \"char\"." +msgstr "" diff --git a/base_search_fuzzy/i18n/de.po b/base_search_fuzzy/i18n/de.po new file mode 100644 index 00000000000..b35b4c01791 --- /dev/null +++ b/base_search_fuzzy/i18n/de.po @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_search_fuzzy +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 9.0alpha1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-06-24 08:49+0000\n" +"PO-Revision-Date: 2016-06-24 08:49+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_search_fuzzy +#: help:trgm.index,index_type:0 +msgid "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.\"" +msgstr "Zitat aus der PostgreSQL Dokumentation: \"Eine Fausregel ist, ein GIN Index ist schneller durchzusuchen als ein GiST Index, aber langsamer aufzubauen und zu aktualisieren; so ist GIN besser geeignet für statische Daten und GiST für oft aktualisierte Daten.\"" + +#. module: base_search_fuzzy +#: field:trgm.index,create_uid:0 +msgid "Created by" +msgstr "Erstellt durch" + +#. module: base_search_fuzzy +#: field:trgm.index,create_date:0 +msgid "Created on" +msgstr "Erstellt am" + +#. module: base_search_fuzzy +#: field:trgm.index,field_id:0 +msgid "Field" +msgstr "Feld" + +#. module: base_search_fuzzy +#: selection:trgm.index,index_type:0 +msgid "GIN" +msgstr "GIN" + +#. module: base_search_fuzzy +#: selection:trgm.index,index_type:0 +msgid "GiST" +msgstr "GiST" + +#. module: base_search_fuzzy +#: field:trgm.index,id:0 +msgid "ID" +msgstr "ID" + +#. module: base_search_fuzzy +#: field:trgm.index,index_name:0 +msgid "Index Name" +msgstr "Index Name" + +#. module: base_search_fuzzy +#: field:trgm.index,index_type:0 +msgid "Index Type" +msgstr "Index Typ" + +#. module: base_search_fuzzy +#: field:trgm.index,write_uid:0 +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert durch" + +#. module: base_search_fuzzy +#: field:trgm.index,write_date:0 +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: base_search_fuzzy +#: model:ir.model,name:base_search_fuzzy.model_ir_model +msgid "Models" +msgstr "Datenmodelle" + +#. module: base_search_fuzzy +#: help:trgm.index,index_name:0 +msgid "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 resused. If the index is located in another table then a number is added at the end of the index name." +msgstr "Der Index Name wird automatisch im Format feldname_indextyp_idx generiert. Falls der Index bereits existiert und der Index sich in der selben Tabelle befindet, dann wird dieser Index wiederverwendet. Falls der Index in einer anderen Tabelle existiert, dann wird eine Zahl an das Ende des Index Namens angefügt." + +#. module: base_search_fuzzy +#: model:ir.actions.act_window,name:base_search_fuzzy.trgm_index_action +#: model:ir.ui.menu,name:base_search_fuzzy.trgm_index_menu +#: view:trgm.index:base_search_fuzzy.trgm_index_view_form +#: view:trgm.index:base_search_fuzzy.trgm_index_view_tree +msgid "Trigram Index" +msgstr "Trigram Index" + +#. module: base_search_fuzzy +#: help:trgm.index,field_id:0 +msgid "You can either select a field of type \"text\" or \"char\"." +msgstr "Sie können entweder Felder vom Typ \"text\" oder \"char\" auswählen." diff --git a/base_search_fuzzy/models/__init__.py b/base_search_fuzzy/models/__init__.py new file mode 100644 index 00000000000..e06aa43611c --- /dev/null +++ b/base_search_fuzzy/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import ir_model +from . import trgm_index diff --git a/base_search_fuzzy/models/ir_model.py b/base_search_fuzzy/models/ir_model.py new file mode 100644 index 00000000000..6c5333268ec --- /dev/null +++ b/base_search_fuzzy/models/ir_model.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, an open source suite of business apps +# This module copyright (C) 2015 bloopark systems (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import logging + +from openerp import models +from openerp.osv import expression + + +_logger = logging.getLogger(__name__) + + +def patch_leaf_trgm(method): + def decorate_leaf_to_sql(self, eleaf): + model = eleaf.model + leaf = eleaf.leaf + left, operator, right = leaf + table_alias = '"%s"' % (eleaf.generate_alias()) + + if operator == '%': + sql_operator = '%%' + params = [] + + if left in model._columns: + format = model._columns[left]._symbol_set[0] + column = '%s.%s' % (table_alias, expression._quote(left)) + query = '(%s %s %s)' % (column, sql_operator, format) + elif left in expression.MAGIC_COLUMNS: + query = "(%s.\"%s\" %s %%s)" % ( + table_alias, left, sql_operator) + params = right + else: # Must not happen + raise ValueError( + "Invalid field %r in domain term %r" % (left, leaf)) + + if left in model._columns: + params = model._columns[left]._symbol_set[1](right) + + if isinstance(params, basestring): + params = [params] + return query, params + elif operator == 'inselect': + right = (right[0].replace(' % ', ' %% '), right[1]) + eleaf.leaf = (left, operator, right) + return method(self, eleaf) + + return decorate_leaf_to_sql + + +def patch_generate_order_by(method): + def decorate_generate_order_by(self, order_spec, query): + if order_spec and order_spec.startswith('similarity('): + return ' ORDER BY ' + order_spec + return method(self, order_spec, query) + + return decorate_generate_order_by + + +class IrModel(models.Model): + + _inherit = 'ir.model' + + def _register_hook(self, cr, ids=None): + expression.expression._expression__leaf_to_sql = patch_leaf_trgm( + expression.expression._expression__leaf_to_sql) + + expression.TERM_OPERATORS += ('%',) + + models.BaseModel._generate_order_by = patch_generate_order_by( + models.BaseModel._generate_order_by) + + return super(IrModel, self)._register_hook(cr) diff --git a/base_trgm_search/models/trgm_index.py b/base_search_fuzzy/models/trgm_index.py similarity index 70% rename from base_trgm_search/models/trgm_index.py rename to base_search_fuzzy/models/trgm_index.py index 28beb769c17..18fbfb9dd61 100644 --- a/base_trgm_search/models/trgm_index.py +++ b/base_search_fuzzy/models/trgm_index.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from openerp import api, exceptions, fields, models -from openerp.osv import expression +from openerp import SUPERUSER_ID, api, exceptions, fields, models from psycopg2.extensions import AsIs @@ -86,10 +86,11 @@ def _install_trgm_extension(self): return True return False - @api.model - def _auto_init(self): - res = super(TrgmIndex, self)._auto_init() - self._install_trgm_extension() + def _auto_init(self, cr, context=None): + res = super(TrgmIndex, self)._auto_init(cr, context) + if self._install_trgm_extension(cr, SUPERUSER_ID, context=context): + _logger.info('The pg_trgm is loaded in the database and the ' + 'fuzzy search can be used.') return res @api.model @@ -168,57 +169,3 @@ def unlink(self): 'index': AsIs(rec.index_name), }) return super(TrgmIndex, self).unlink() - - -def patch_leaf_trgm(method): - def decorate_leaf_to_sql(self, eleaf): - model = eleaf.model - leaf = eleaf.leaf - left, operator, right = leaf - table_alias = '"%s"' % (eleaf.generate_alias()) - - if operator == '%': - sql_operator = '%%' - params = [] - - if left in model._columns: - format = model._columns[left]._symbol_set[0] - column = '%s.%s' % (table_alias, expression._quote(left)) - query = '(%s %s %s)' % (column, sql_operator, format) - elif left in expression.MAGIC_COLUMNS: - query = "(%s.\"%s\" %s %%s)" % ( - table_alias, left, sql_operator) - params = right - else: # Must not happen - raise ValueError( - "Invalid field %r in domain term %r" % (left, leaf)) - - if left in model._columns: - params = model._columns[left]._symbol_set[1](right) - - if isinstance(params, basestring): - params = [params] - return query, params - elif operator == 'inselect': - right = (right[0].replace(' % ', ' %% '), right[1]) - eleaf.leaf = (left, operator, right) - return method(self, eleaf) - return decorate_leaf_to_sql - -expression.expression._expression__leaf_to_sql = patch_leaf_trgm( - expression.expression._expression__leaf_to_sql) - -expression.TERM_OPERATORS += ('%',) - - -def patch_generate_order_by(method): - def decorate_generate_order_by(self, order_spec, query): - if order_spec and order_spec.startswith('similarity('): - from pprint import pprint - pprint(query) - return ' ORDER BY ' + order_spec - return method(self, order_spec, query) - return decorate_generate_order_by - -models.BaseModel._generate_order_by = patch_generate_order_by( - models.BaseModel._generate_order_by) diff --git a/base_search_fuzzy/security/ir.model.access.csv b/base_search_fuzzy/security/ir.model.access.csv new file mode 100644 index 00000000000..0d5bc603845 --- /dev/null +++ b/base_search_fuzzy/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_trgm_index_public,trgm_index group_public,model_trgm_index,base.group_public,1,0,0,0 +access_trgm_index_portal,trgm_index group_portal,model_trgm_index,base.group_portal,1,0,0,0 +access_trgm_index_group_partner_manager,trgm_index group_partner_manager,model_trgm_index,base.group_no_one,1,1,1,1 +access_trgm_index_group_user,trgm_index group_user,model_trgm_index,base.group_user,1,0,0,0 diff --git a/base_search_fuzzy/tests/__init__.py b/base_search_fuzzy/tests/__init__.py new file mode 100644 index 00000000000..b45665e4216 --- /dev/null +++ b/base_search_fuzzy/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_query_generation diff --git a/base_search_fuzzy/tests/test_query_generation.py b/base_search_fuzzy/tests/test_query_generation.py new file mode 100644 index 00000000000..54db7d07516 --- /dev/null +++ b/base_search_fuzzy/tests/test_query_generation.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp.osv import expression +from openerp.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'] + + 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) + + # create new query with fuzzy search operator + 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 + self.assertEqual(where_clause, """("res_partner"."name" %% %s)""") + + # 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) + self.assertEqual( + complete_where, + 'SELECT FROM "res_partner" WHERE ' + '("res_partner"."name" % \'test\')') + + 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) + order_by = self.ResPartner._generate_order_by(order, query) + 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': + 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', + }) + + 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')]) + 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')]) + self.assertIn(partner1.id, res.ids) + self.assertIn(partner2.id, res.ids) + self.assertNotIn(partner3.id, res.ids) diff --git a/base_trgm_search/views/trgm_index.xml b/base_search_fuzzy/views/trgm_index.xml similarity index 91% rename from base_trgm_search/views/trgm_index.xml rename to base_search_fuzzy/views/trgm_index.xml index b72b0668310..36fb6efcb1b 100644 --- a/base_trgm_search/views/trgm_index.xml +++ b/base_search_fuzzy/views/trgm_index.xml @@ -31,7 +31,7 @@ - Trigam Index + Trigram Index trgm.index form tree,form @@ -40,7 +40,8 @@ + action="trgm_index_action" + groups="base.group_no_one"/> diff --git a/base_trgm_search/__init__.py b/base_trgm_search/__init__.py deleted file mode 100644 index a0fdc10fe11..00000000000 --- a/base_trgm_search/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -from . import models diff --git a/base_trgm_search/__openerp__.py b/base_trgm_search/__openerp__.py deleted file mode 100644 index 3359ac4496d..00000000000 --- a/base_trgm_search/__openerp__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -{ - 'name': "PostgreSQL Trigram Search", - 'summary': """PostgreSQL Trigram Search""", - 'category': 'Uncategorized', - 'version': '8.0.1.0.0', - 'website': 'https://odoo-community.org/', - 'author': 'Daniel Reis, Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'depends': [ - 'base', - ], - 'data': [ - 'views/trgm_index.xml', - ], - 'installable': True, -} diff --git a/base_trgm_search/models/__init__.py b/base_trgm_search/models/__init__.py deleted file mode 100644 index 83d2dfdc368..00000000000 --- a/base_trgm_search/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -from . import trgm_index From 94376b3d9eb93680b88b08ad1e18db8b62981d39 Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Mon, 27 Jun 2016 14:55:18 +0200 Subject: [PATCH 4/8] [IMP] base_search_fuzzy: Updated README, translated UserError Exception, Added one more test --- base_search_fuzzy/README.rst | 49 ++++++++++--------- base_search_fuzzy/i18n/base_search_fuzzy.pot | 6 +++ base_search_fuzzy/i18n/de.po | 6 +++ base_search_fuzzy/models/trgm_index.py | 6 +-- .../tests/test_query_generation.py | 21 ++++++++ 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/base_search_fuzzy/README.rst b/base_search_fuzzy/README.rst index cf2ef92fba9..71a89e8503a 100644 --- a/base_search_fuzzy/README.rst +++ b/base_search_fuzzy/README.rst @@ -8,49 +8,54 @@ PostgreSQL Trigram Search This addon provides the ability to create GIN or GiST indexes of char and text fields and also to use the search operator `%` in search domains. Currently -this module doesn't change the backend search or something else. It provides -only the possibilty to do a fuzzy search for external addons. +this module doesn't change the backend search or anything else. It provides +only the possibilty to perfrom the fuzzy search for external addons. Installation ============ -First you need to have the ``pg_trgm`` extension available. In debian based -distribution you have to install the `postgresql-contrib` module. Then you have -to either install the ``pg_trgm`` extension to your database or you have to give -your postgresql user the superadmin right (this allows the odoo module to -install the extension to the database). +#. The PostgreSQL extension ``pg_trgm`` should be available. In debian based + distribution you have to install the `postgresql-contrib` module. +#. Install the ``pg_trgm`` extension to your database or give your postgresql + user the ``SUEPRUSER`` right (this allows the odoo module to install the + extension to the database). Configuration ============= -If you installed the odoo module you can define ``GIN`` and ``GiST`` indexes for -`char` and `text` via `Settings -> Database Structure -> Trigram Index`. The -index name will automatically created for new entries. +If the odoo module is installed: + +#. You can define ``GIN`` and ``GiST`` indexes for `char` and `text` via + `Settings -> Database Structure -> Trigram Index`. The index name will + automatically created for new entries. Usage ===== -For example you can create an index for the `name` field of `res.partner`. Then -in a search you can use +#. You can create an index for the `name` field of `res.partner`. +#. In the search you can use: -``self.env['res.partner'].search([('name', '%', 'Jon Miller)])`` + ``self.env['res.partner'].search([('name', '%', 'Jon Miller)])`` -In this Example it can find existing names like `John Miller` or `John Mill`. +#. In this example the function will return positive result for `John Miller` or + `John Mill`. -Which strings can be found depends on the limit which is set (default: 0.3). -Currently you have to set the limit by executing SQL: +#. You can tweak the number of strings to be returned by adjusting the set limit + (default: 0.3). NB: Currently you have to set the limit by executing the + following SQL statment: -``self.env.cr.execute("SELECT set_limit(0.2);")`` + ``self.env.cr.execute("SELECT set_limit(0.2);")`` -Also you can use the ``similarity(column, 'text')`` function in the ``order`` -parameter to order by the similarity. This is just a basic implementation which -doesn't contains validations and has to start with this function. For example -you can define this like: +#. Another interesting feature is the use of ``similarity(column, 'text')`` + function in the ``order`` parameter to order by similarity. This module just + contains a basic implementation which doesn't perform validations and has to + start with this function. For example you can define the function as + followed: -``similarity(%s.name, 'John Mil') DESC" % self.env['res.partner']._table`` + ``similarity(%s.name, 'John Mil') DESC" % self.env['res.partner']._table`` For further questions read the Documentation of the `pg_trgm `_ module. diff --git a/base_search_fuzzy/i18n/base_search_fuzzy.pot b/base_search_fuzzy/i18n/base_search_fuzzy.pot index 03e4c6bb87b..deaf7d891e4 100644 --- a/base_search_fuzzy/i18n/base_search_fuzzy.pot +++ b/base_search_fuzzy/i18n/base_search_fuzzy.pot @@ -92,3 +92,9 @@ msgstr "" #: help:trgm.index,field_id:0 msgid "You can either select a field of type \"text\" or \"char\"." msgstr "" + +#. module: base_search_fuzzy +#: code:addons/base_search_fuzzy/models/trgm_index.py:123 +#, python-format +msgid "The pg_trgm extension does not exists or cannot be installed." +msgstr "" diff --git a/base_search_fuzzy/i18n/de.po b/base_search_fuzzy/i18n/de.po index b35b4c01791..51af7d4854d 100644 --- a/base_search_fuzzy/i18n/de.po +++ b/base_search_fuzzy/i18n/de.po @@ -92,3 +92,9 @@ msgstr "Trigram Index" #: help:trgm.index,field_id:0 msgid "You can either select a field of type \"text\" or \"char\"." msgstr "Sie können entweder Felder vom Typ \"text\" oder \"char\" auswählen." + +#. module: base_search_fuzzy +#: code:addons/base_search_fuzzy/models/trgm_index.py:123 +#, python-format +msgid "The pg_trgm extension does not exists or cannot be installed." +msgstr "Die pg_trgm Erweiterung existiert nicht oder kann nicht installiert werden." diff --git a/base_search_fuzzy/models/trgm_index.py b/base_search_fuzzy/models/trgm_index.py index 18fbfb9dd61..f1ea3916fed 100644 --- a/base_search_fuzzy/models/trgm_index.py +++ b/base_search_fuzzy/models/trgm_index.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from openerp import SUPERUSER_ID, api, exceptions, fields, models +from openerp import _, SUPERUSER_ID, api, exceptions, fields, models from psycopg2.extensions import AsIs @@ -119,9 +119,9 @@ def create_index(self): self.ensure_one() if not self._install_trgm_extension(): - raise exceptions.UserError( + raise exceptions.UserError(_( 'The pg_trgm extension does not exists or cannot be ' - 'installed.') + 'installed.')) table_name = self.env[self.field_id.model_id.model]._table column_name = self.field_id.name diff --git a/base_search_fuzzy/tests/test_query_generation.py b/base_search_fuzzy/tests/test_query_generation.py index 54db7d07516..e094b07e73f 100644 --- a/base_search_fuzzy/tests/test_query_generation.py +++ b/base_search_fuzzy/tests/test_query_generation.py @@ -37,6 +37,27 @@ def test_fuzzy_where_generation(self): 'SELECT FROM "res_partner" WHERE ' '("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'} + + # create new query with fuzzy search operator + 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) + + complete_where = self.env.cr.mogrify( + "SELECT FROM %s WHERE %s" % (from_clause, where_clause), + where_clause_params) + + self.assertIn( + """SELECT id FROM temp_irt_current WHERE name % 'Goschaeftlic'""", + complete_where) + def test_fuzzy_order_generation(self): """Check the generation of the where clause.""" order = "similarity(%s.name, 'test') DESC" % self.ResPartner._table From 34cca79e78cf894687e7585f6a3e28390ee9505b Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Mon, 27 Jun 2016 14:56:40 +0200 Subject: [PATCH 5/8] [IMP] base_search_fuzzy: Updated license header --- base_search_fuzzy/models/ir_model.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/base_search_fuzzy/models/ir_model.py b/base_search_fuzzy/models/ir_model.py index 6c5333268ec..ba91cd69d70 100644 --- a/base_search_fuzzy/models/ir_model.py +++ b/base_search_fuzzy/models/ir_model.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Odoo, an open source suite of business apps -# This module copyright (C) 2015 bloopark systems (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging from openerp import models From 2a2c71db48d6bdfef35869368740959b91d77fd1 Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Mon, 27 Jun 2016 15:16:05 +0200 Subject: [PATCH 6/8] [FIX] Fixed missing instance variable after cleanup --- base_search_fuzzy/models/trgm_index.py | 2 +- base_search_fuzzy/tests/test_query_generation.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/base_search_fuzzy/models/trgm_index.py b/base_search_fuzzy/models/trgm_index.py index f1ea3916fed..d4ecc102555 100644 --- a/base_search_fuzzy/models/trgm_index.py +++ b/base_search_fuzzy/models/trgm_index.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from openerp import _, SUPERUSER_ID, api, exceptions, fields, models +from openerp import SUPERUSER_ID, _, api, exceptions, fields, models from psycopg2.extensions import AsIs diff --git a/base_search_fuzzy/tests/test_query_generation.py b/base_search_fuzzy/tests/test_query_generation.py index e094b07e73f..34c7e30b325 100644 --- a/base_search_fuzzy/tests/test_query_generation.py +++ b/base_search_fuzzy/tests/test_query_generation.py @@ -12,6 +12,7 @@ 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'] def test_fuzzy_where_generation(self): """Check the generation of the where clause.""" From e6c37fb7db4c7aae8a67a46ec6a9401c52281b78 Mon Sep 17 00:00:00 2001 From: Christoph Giesel Date: Tue, 19 Jul 2016 10:48:05 +0200 Subject: [PATCH 7/8] base_search_fuzzy: [FIX] Check if functions are already decorated to avoid decorating the function many times whcih leads to recursion errors --- base_search_fuzzy/models/ir_model.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/base_search_fuzzy/models/ir_model.py b/base_search_fuzzy/models/ir_model.py index ba91cd69d70..8be90b8e381 100644 --- a/base_search_fuzzy/models/ir_model.py +++ b/base_search_fuzzy/models/ir_model.py @@ -43,6 +43,8 @@ def decorate_leaf_to_sql(self, eleaf): eleaf.leaf = (left, operator, right) return method(self, eleaf) + decorate_leaf_to_sql.__decorated__ = True + return decorate_leaf_to_sql @@ -52,6 +54,8 @@ def decorate_generate_order_by(self, order_spec, query): return ' ORDER BY ' + order_spec return method(self, order_spec, query) + decorate_generate_order_by.__decorated__ = True + return decorate_generate_order_by @@ -60,12 +64,19 @@ class IrModel(models.Model): _inherit = 'ir.model' def _register_hook(self, cr, ids=None): - expression.expression._expression__leaf_to_sql = patch_leaf_trgm( - expression.expression._expression__leaf_to_sql) - - expression.TERM_OPERATORS += ('%',) - - models.BaseModel._generate_order_by = patch_generate_order_by( - models.BaseModel._generate_order_by) + # We have to prevent wrapping the function twice to avoid recursion + # errors + 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) + + if '%' not in expression.TERM_OPERATORS: + expression.TERM_OPERATORS += ('%',) + + if not hasattr(models.BaseModel._generate_order_by, + '__decorated__'): + models.BaseModel._generate_order_by = patch_generate_order_by( + models.BaseModel._generate_order_by) return super(IrModel, self)._register_hook(cr) From 10cb5aabdc97e16a99bb4a16a3b1e84423031bbc Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 10 Aug 2016 10:02:12 +0100 Subject: [PATCH 8/8] Fix typo --- base_search_fuzzy/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_search_fuzzy/README.rst b/base_search_fuzzy/README.rst index 71a89e8503a..1ee718a49af 100644 --- a/base_search_fuzzy/README.rst +++ b/base_search_fuzzy/README.rst @@ -18,7 +18,7 @@ Installation #. The PostgreSQL extension ``pg_trgm`` should be available. In debian based distribution you have to install the `postgresql-contrib` module. #. Install the ``pg_trgm`` extension to your database or give your postgresql - user the ``SUEPRUSER`` right (this allows the odoo module to install the + user the ``SUPERUSER`` right (this allows the odoo module to install the extension to the database).