Add global search

review582002
This commit is contained in:
Cédric Krier committed 2012-12-05 13:25:18 +01:00
1 parent 290791e0df
commit c1fd506f77
10 files changed
+149

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Add global search
* Replace view shortcut by menu favorite
* Store default language in database
* Add icon attribute on fields
+5
View File
@@ -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``.
+2
View File
@@ -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
==============
+1
View File
@@ -71,6 +71,7 @@ setup(name=PACKAGE,
'timezone': ['pytz'],
'simplejson': ['simplejson'],
'cdecimal': ['cdecimal'],
'Levenshtein': ['python-Levenshtein'],
},
zip_safe=False,
test_suite='trytond.tests',
+42
View File
@@ -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"
+9
View File
@@ -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):
+2
View File
@@ -8,6 +8,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="model"/>
<label name="module"/>
<field name="module"/>
<label name="global_search_p"/>
<field name="global_search_p"/>
<separator name="info" colspan="6"/>
<field name="info" colspan="6"/>
<field name="fields" colspan="6" readonly="1"/>
+1
View File
@@ -5,5 +5,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="name"/>
<field name="model"/>
<field name="info"/>
<field name="global_search_p"/>
<field name="module"/>
</tree>
+11
View File
@@ -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):
'''
+75
View File
@@ -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