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. -->
+
+
diff --git a/trytond/ir/view/model_list.xml b/trytond/ir/view/model_list.xml
index 2b127b8489..4d56a1d67d 100644
--- a/trytond/ir/view/model_list.xml
+++ b/trytond/ir/view/model_list.xml
@@ -5,5 +5,6 @@ this repository contains the full copyright notices and license terms. -->
+
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 371bcce5ea..f1096903de 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -414,6 +414,17 @@ class ModelStorage(Model):
return []
return [(rec_name,) + clause[1:]]
+ @classmethod
+ def search_global(cls, text):
+ '''
+ Yield tuples (id, rec_name, icon) for text
+ '''
+ # TODO improve search clause
+ for record in cls.search([
+ ('rec_name', 'ilike', '%%%s%%' % text),
+ ]):
+ yield record.id, record.rec_name, None
+
@classmethod
def browse(cls, ids):
'''
diff --git a/trytond/tools/StringMatcher.py b/trytond/tools/StringMatcher.py
new file mode 100644
index 0000000000..2258a99113
--- /dev/null
+++ b/trytond/tools/StringMatcher.py
@@ -0,0 +1,75 @@
+#This file is part of Tryton. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+
+# Code come from python-Levenshtein
+
+from Levenshtein import *
+from warnings import warn
+
+
+class StringMatcher:
+ """A SequenceMatcher-like class built on the top of Levenshtein"""
+
+ def _reset_cache(self):
+ self._ratio = self._distance = None
+ self._opcodes = self._editops = self._matching_blocks = None
+
+ def __init__(self, isjunk=None, seq1='', seq2=''):
+ if isjunk:
+ warn("isjunk not NOT implemented, it will be ignored")
+ self._str1, self._str2 = seq1, seq2
+ self._reset_cache()
+
+ def set_seqs(self, seq1, seq2):
+ self._str1, self._str2 = seq1, seq2
+ self._reset_cache()
+
+ def set_seq1(self, seq1):
+ self._str1 = seq1
+ self._reset_cache()
+
+ def set_seq2(self, seq2):
+ self._str2 = seq2
+ self._reset_cache()
+
+ def get_opcodes(self):
+ if not self._opcodes:
+ if self._editops:
+ self._opcodes = opcodes(self._editops, self._str1, self._str2)
+ else:
+ self._opcodes = opcodes(self._str1, self._str2)
+ return self._opcodes
+
+ def get_editops(self):
+ if not self._editops:
+ if self._opcodes:
+ self._editops = editops(self._opcodes, self._str1, self._str2)
+ else:
+ self._editops = editops(self._str1, self._str2)
+ return self._editops
+
+ def get_matching_blocks(self):
+ if not self._matching_blocks:
+ self._matching_blocks = matching_blocks(self.get_opcodes(),
+ self._str1, self._str2)
+ return self._matching_blocks
+
+ def ratio(self):
+ if not self._ratio:
+ self._ratio = ratio(self._str1, self._str2)
+ return self._ratio
+
+ def quick_ratio(self):
+ # This is usually quick enough :o)
+ if not self._ratio:
+ self._ratio = ratio(self._str1, self._str2)
+ return self._ratio
+
+ def real_quick_ratio(self):
+ len1, len2 = len(self._str1), len(self._str2)
+ return 2.0 * min(len1, len2) / (len1 + len2)
+
+ def distance(self):
+ if not self._distance:
+ self._distance = distance(self._str1, self._str2)
+ return self._distance