From c1fd506f773580312d2da1feb21cf22a6abfcc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 5 Dec 2012 13:25:18 +0100 Subject: [PATCH] Add global search review582002 --- CHANGELOG | 1 + doc/ref/models/models.rst | 5 +++ doc/topics/install.rst | 2 + setup.py | 1 + trytond/ir/model.py | 42 +++++++++++++++++++ trytond/ir/ui/menu.py | 9 ++++ trytond/ir/view/model_form.xml | 2 + trytond/ir/view/model_list.xml | 1 + trytond/model/modelstorage.py | 11 +++++ trytond/tools/StringMatcher.py | 75 ++++++++++++++++++++++++++++++++++ 10 files changed, 149 insertions(+) create mode 100644 trytond/tools/StringMatcher.py diff --git a/CHANGELOG b/CHANGELOG index 8e504c88f9..16dc655173 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add global search * Replace view shortcut by menu favorite * Store default language in database * Add icon attribute on fields diff --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst index 10f173cf4f..fe75c43bf4 100644 --- a/doc/ref/models/models.rst +++ b/doc/ref/models/models.rst @@ -294,6 +294,11 @@ CLass methods: Searcher for the :class:`trytond.model.fields.Function` field :attr:`rec_name`. +.. classmethod:: ModelStorage.search_global(cls, text) + + Yield tuples (id, rec_name, icon) for records matching text. + It is used for the global search. + .. classmethod:: ModelStorage.browse(ids) Return a list of record instance for the ``ids``. diff --git a/doc/topics/install.rst b/doc/topics/install.rst index 7ac3d2ed2a..4e18982eed 100644 --- a/doc/topics/install.rst +++ b/doc/topics/install.rst @@ -21,6 +21,8 @@ Prerequisites * Optional: sphinx (http://sphinx.pocoo.org/) * Optional: simplejson (http://undefined.org/python/#simplejson) * Optional: cdecimal (http://www.bytereef.org/mpdecimal/index.html) + * Optional: python-Levenshtein + (http://github.com/miohtama/python-Levenshtein) Install Tryton ============== diff --git a/setup.py b/setup.py index 6384c05576..9e5d70927f 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ setup(name=PACKAGE, 'timezone': ['pytz'], 'simplejson': ['simplejson'], 'cdecimal': ['cdecimal'], + 'Levenshtein': ['python-Levenshtein'], }, zip_safe=False, test_suite='trytond.tests', diff --git a/trytond/ir/model.py b/trytond/ir/model.py index 893de624da..a09bf8b690 100644 --- a/trytond/ir/model.py +++ b/trytond/ir/model.py @@ -2,6 +2,8 @@ #this repository contains the full copyright notices and license terms. import datetime import re +import heapq + from ..model import ModelView, ModelSQL, fields from ..report import Report from ..wizard import Wizard, StateView, StateAction, Button @@ -10,6 +12,10 @@ from ..cache import Cache from ..pool import Pool from ..pyson import Bool, Eval from ..rpc import RPC +try: + from ..tools.StringMatcher import StringMatcher +except ImportError: + from difflib import SequenceMatcher as StringMatcher __all__ = [ 'Model', 'ModelField', 'ModelAccess', 'ModelFieldAccess', 'ModelButton', @@ -39,6 +45,7 @@ class Model(ModelSQL, ModelView): depends=['module']) module = fields.Char('Module', help="Module in which this model is defined", readonly=True) + global_search_p = fields.Boolean('Global Search') fields = fields.One2Many('ir.model.field', 'model', 'Fields', required=True) @@ -58,6 +65,7 @@ class Model(ModelSQL, ModelView): cls._order.insert(0, ('model', 'ASC')) cls.__rpc__.update({ 'list_models': RPC(), + 'global_search': RPC(), }) @classmethod @@ -106,6 +114,40 @@ class Model(ModelSQL, ModelView): # Restart the cache of models_get Property._models_get_cache.clear() + @classmethod + def global_search(cls, text, limit): + """ + Search on models for text + Returns a list of tuple (ratio, model, model_name, id, rec_name, icon) + The size of the list is limited to limit + """ + pool = Pool() + ModelAccess = pool.get('ir.model.access') + + if not limit > 0: + raise ValueError('limit must be > 0: %r' % (limit,)) + + models = cls.search(['OR', + ('global_search_p', '=', True), + ('name', '=', 'ir.ui.menu'), + ]) + access = ModelAccess.get_access([m.model for m in models]) + s = StringMatcher() + s.set_seq2(text.decode('utf-8')) + + def generate(): + for model in models: + if not access[model.model]['read']: + continue + Model = pool.get(model.model) + if not hasattr(Model, 'search_global'): + continue + for id_, rec_name, icon in Model.search_global(text): + s.set_seq1(rec_name) + yield (s.ratio(), model.model, model.rec_name, + id_, rec_name, icon) + return heapq.nlargest(int(limit), generate()) + class ModelField(ModelSQL, ModelView): "Model field" diff --git a/trytond/ir/ui/menu.py b/trytond/ir/ui/menu.py index 2cd6768cbc..0186c59aca 100644 --- a/trytond/ir/ui/menu.py +++ b/trytond/ir/ui/menu.py @@ -150,6 +150,15 @@ class UIMenu(ModelSQL, ModelView): #TODO Handle list return [('name',) + tuple(clause[1:])] + @classmethod + def search_global(cls, text): + # TODO improve search clause + for record in cls.search([ + ('rec_name', 'ilike', '%%%s%%' % text), + ]): + if record.action: + yield record.id, record.rec_name, record.icon + @classmethod def search(cls, domain, offset=0, limit=None, order=None, count=False, query_string=False): diff --git a/trytond/ir/view/model_form.xml b/trytond/ir/view/model_form.xml index a568d5f116..9458a41ea8 100644 --- a/trytond/ir/view/model_form.xml +++ b/trytond/ir/view/model_form.xml @@ -8,6 +8,8 @@ this repository contains the full copyright notices and license terms. -->