diff --git a/CHANGELOG b/CHANGELOG index 6f2fdf7a65..11b1bddee0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* New search widget * Improve memory management of Binary fields * Support buffer for Binary fields * Remove delete on Escape in editable tree diff --git a/doc/usage.rst b/doc/usage.rst index 389c46a600..2caabd13bc 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -92,9 +92,9 @@ Figure: Tryton client application:: | |-+ | |-----------------------------------------------|| Tool bar | | |- | | New Save Switch Reload | Prev Next | Attach v || | | |- | |-----------------------------------------------|| - | + | | || - | |-+ | | || - | | |- | | || + | + | | ____________________ || + Search widget | |-+ | | Search | | <- -> Find Clear || + | | |- | |-----------------------------------------------|| | | |- | | || | + | | || View | |-+ | | || @@ -874,6 +874,101 @@ Attachment: On click it opens the attachments :term:`dialog`. The default dialog shows a list view of the attached files and links. +Search Widget +************* + +The search widget adds the ability to easily search for records on the current +tab. This widget is visible only on :term:`tree view`. + +The Syntax +^^^^^^^^^^ + +A query is composed of search clauses. +A clause is composed of a field name (with `:` at the end), an operator and a value. +The field name is optional and defaults to the record name. +The operator is also optional and defaults to `like` or `equal` depending on +the type of the field. The default operator is `=` except for fields of type +`char`, `text` and `many2one` which is `ilike`. + +Field Names +^^^^^^^^^^^ + +All field names shown in the :term:`tree view` can be searched. Field names +must be followed by a `:` + + For example: ``Name:`` + +If the field name contains spaces, it is possible to +escape it using double quotes. + + For example: ``"Receivable Today":`` + +Operators +^^^^^^^^^ + +The following operators can be used: + + * `=`: equal to + * `<`: less then + * `<=`: less then or equal to + * `>`: greater then + * `>=`: greater then or equal to + * `!=`: not equal + * `!`: not equal or not like (depending of the type of field) + + For example: ``Name: != Dwight`` + +.. note:: The `ilike` operator is never explicit and `%` is appended to the + value to make it behaves like `starts with` + +Values +^^^^^^ + +The format of the value depends on the type of the field. +A list of values can be set using `;` as separator. + + For example: ``Name: Michael; Pam`` + + It will find all records having the `Name` starting with `Michael` or + `Pam`. + +A range of number values can be set using `..`. + + For example: ``Amount: 100..500`` + + It will find all records with `Amount` between `100` and `500` (left + included and right excluded) + +There are two wildcards: + + * `%`: matches any string of zero or more characters. + * `_`: matches any single character. + +It is possible to escape special characters in values by using double quotes. + + For example: ``Name: "Michael:Scott"`` + + Here it will search with the value `Michael:Scott`. + +Clause composition +^^^^^^^^^^^^^^^^^^ + +The clauses can be composed using the two boolean operators `and` and `or`. +By default, there is an implicit `and` between each clause if no operator is +specified. + + For example: ``Name: Michael Amount: 100`` + + is the same as ``Name: Michael and Amount: 100`` + +The `and` operator has a highest precedence than `or` but you can change it by +using parenthesis. + + For example: ``(Name: Michael or Name: Pam) and Amount: 100`` + + is different than ``Name: Michael or Name: Pam and Amount: 100`` + + which is evaluated as ``Name: Michael or (Name: Pam and Amount: 100)`` Appendix ******** diff --git a/tryton/common/date_widget.py b/tryton/common/date_widget.py index 54432e7077..e6dbd054a1 100644 --- a/tryton/common/date_widget.py +++ b/tryton/common/date_widget.py @@ -161,6 +161,8 @@ class DateEntry(gtk.Entry): return False match = self.regex.match(text) + if not match: + return False for i in range(len(match.groups())): val = match.group(i + 1) n = len(val) diff --git a/tryton/common/tdp.py b/tryton/common/tdp.py new file mode 100644 index 0000000000..8dbc98bb1c --- /dev/null +++ b/tryton/common/tdp.py @@ -0,0 +1,1476 @@ +# -*- coding: utf-8 -*- +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +# For explanation of this parser see +# http://effbot.org/zone/simple-top-down-parsing.htm + +import datetime +import gettext +import re +import time +from decimal import Decimal +import decimal +import locale +import gtk + +from tryton.translate import date_format +from tryton.common import datetime_strftime, HM_FORMAT, timezoned_date +from tryton.common.date_widget import DateEntry + +_ = gettext.gettext + +OPERATORS = set(( + '=', + '>', + '<', + '<=', + '>=', + '!=', + 'in', + 'not in', + 'ilike', + 'not ilike')) + +def operator(field): + type_ = field['type'] if field else '' + if type_ in ('char', 'text', 'many2one') or not field: + return 'ilike' + else: + return '=' + + +def cast(field, value): + type_ = field['type'] if field else '' + if type_ == 'boolean': + return any(test.lower().startswith(value.lower()) for test in (_('y'), + _('yes'), _('true'), '1')) + elif type_ == 'integer': + for conv in (int, float): + try: + return conv(value) + except ValueError: + continue + return False + elif type_ == 'float': + try: + return float(value) + except ValueError: + return False + elif type_ == 'numeric': + try: + return Decimal(value) + except decimal.InvalidOperation: + return False + elif type_ in ('selection', 'reference'): + for key, text in field['selection']: + if text == value: + return key + return value + elif type_ == 'datetime': + value = value.replace(' : ', ':') # Parser add spaces arround : + try: + value = datetime.datetime(*time.strptime(value, + date_format() + ' ' + HM_FORMAT)[:6]) + except ValueError: + try: + value = datetime.datetime(*time.strptime(value, + date_format())[:6]) + except ValueError: + return False + return timezoned_date(value) + elif type_ == 'date': + try: + return datetime.date(*time.strptime(value, date_format())[:3]) + except ValueError: + return False + else: + return value + +def quote(value): + for test in (':', ' '): + if test in value: + return '"%s"' % value + return value + + +class Base(object): + value = None + + fmt = '%s %s %s' + + def __init__(self, parser): + self.parser = parser + self.pos = None + self.parent = None + self._left = None + self._right = None + + def _get_left(self): + return self._left + + def _set_left(self, value): + self._left = value + value.pos = 'left' + value.parent = self + + left = property(_get_left, _set_left) + + def _get_right(self): + return self._right + + def _set_right(self, value): + self._right = value + value.pos = 'right' + value.parent = self + + right = property(_get_right, _set_right) + + def domain(self, parent_field=None): + raise NotImplementedError("domain method is missing on %s" % \ + self.__class__) + + def complete_fmt(self, left, value, right): + return (self.fmt % (left, value, right)).strip() + + def complete(self, parent_field=None): + left = self.left and tuple(self.left.complete()) or ('',) + right = self.right and tuple(self.right.complete(parent_field)) or ('',) + + for lvalue in left: + for rvalue in right: + yield self.complete_fmt(lvalue, self.value, rvalue) + + def split(self, words=""): + field, value = None, ' '.join(words) + for i in xrange(len(words)): + candidate = ' '.join(words[-i-1:]) + if candidate.lower() in self.parser.dom_fields: + field = self.parser.dom_fields[candidate.lower()] + value = ' '.join(words[:-i-1]) + + return field, value + + def __str__(self, prefix=''): + res = '%s[%s] %s'% (prefix, self.pos or 'root', self.value) + res = res.ljust(30) + str(type(self)) + if self.left: + res += "\n" + self.left.__str__(' ' + prefix) + if self.right: + res += "\n" + self.right.__str__(' ' + prefix) + return res + + +class Literal(Base): + lbp = 80 + + def __init__(self, parser, value): + super(Literal, self).__init__(parser) + self.value = value + + def nud(self): + return self + + def led(self, left): + self.left = left + return self + + def flatten(self): + words = [self.value] + node = self.left + while node: + if not isinstance(node, Literal): + break + words.insert(0, node.value) + node = node.left + return words + + def split(self, words=""): + words = self.flatten() + return super(Literal, self).split(words) + + def domain(self, parent_field=None): + value = ' '.join(self.flatten()) + return [('rec_name', 'ilike', value + '%')] + + def suggest(self, words, suggestions): + candidate = '' + # Traking previous suggestion avoid duplicates when several + # fields have the same ending (ex: "Menu" and "Parent Menu"): + previous = set() + for pos, word in enumerate(words): + candidate = word + candidate + for field in suggestions: + if field.lower().startswith(candidate.lower()): + if words[pos+1:]: + new_item = ' '.join(reversed(words[pos+1:])) + new_item += ' ' + field + else: + new_item = field + if new_item not in previous: + previous.add(new_item) + yield new_item + candidate = ' ' + candidate + + def suggest_ltr(self, words, suggestions): + words = list(reversed(words)) + + for field in suggestions: + for i in xrange(len(words), 0, -1): + candidate = ' '.join(words[:i]) + + if field.lower().startswith(candidate.lower()): + new_item = quote(field) + if (i-len(words) < 0) and words[i-len(words):]: + find = False + for item in self.suggest(words[i - len(words):], + self.parser.sugg_fields): + yield new_item + ' ' + item + (':' + if self.pos == 'right' else '') + find = True + if find: + break + new_item += ' ' + ' '.join(words[i-len(words):]) + yield new_item + break + + def complete(self, parent_field=None): + item = self + words = [] + while item: + words.append(item.value) + item = item.left + + orig = ' '.join(reversed(words)).lower() + extra = values = [] + if parent_field: + if parent_field['type'] in ('selection', 'reference', 'boolean'): + if parent_field['type'] in ('selection', 'reference'): + suggestions = tuple(x[1] for x in parent_field['selection']) + else: + suggestions = (_('Y'), _('Yes'), _('True'), '1', _('N'), + _('No'), _('False'), '0') + for suggestion in self.suggest_ltr(words, suggestions): + if suggestion != orig: + extra.append(suggestion) + elif parent_field['type'] in ('date', 'datetime'): + format_ = date_format() + if parent_field['type'] == 'datetime': + format_ += ' ' + HM_FORMAT + entry = DateEntry(format_) + entry.set_text(entry.initial_value) + gtk.Entry.insert_text(entry, orig, 0) + default = datetime_strftime(datetime.datetime.combine( + datetime.date.today(), datetime.time.min), format_) + value = entry.compute_date(entry.get_text(), default) or '' + if value: + if value.endswith(' 00:00:00'): + value = value[:-9] + if len(value) > len(orig): + extra.append(quote(value)) + + if not extra: + values = tuple(self.suggest(words, self.parser.sugg_fields)) + + if not values and not extra: + values = (quote(' '.join(reversed(words))),) + elif (not self.parent) or self.pos == 'right': + values = extra or tuple(v + ':' for v in values) + + right = self.right and tuple(self.right.complete()) + + if not self.right: + for value in values: + yield value + else: + for value in values: + for rvalue in right: + yield '%s %s' % (value, rvalue) + + +class InfixMixin: + + def nud(self): + self.right = self.parser.expression(self.lbp) + return self + + def led(self, left): + self.left = left + self.right = self.parser.expression(self.lbp) + return self + + +class Colon(Base, InfixMixin): + value = ':' + lbp = 50 + fmt = '%s%s %s' + + def __init__(self, parser): + self.extra_domain = [] + super(Colon, self).__init__(parser) + + def led(self, left): + self.left = left + # decreasing lbp makes Colon right-associative + self.right = self.parser.expression(self.lbp - 1) + return self + + def complete(self, parent_field=None): + assert parent_field is None, parent_field + field, _ = self.left.split() + + left = self.left and tuple(self.left.complete(field)) or ('',) + if not isinstance(self.right, Colon): + right = tuple(self.right.complete(parent_field=field)) + else: + right = self.right and tuple(self.right.complete()) or ('',) + + for lvalue in left: + for rvalue in right: + yield self.complete_fmt(lvalue, self.value, rvalue) + + def domain(self, parent_field=None): + if not self.left: + return [] + field, value = self.left.split() + + #Recurse left + if isinstance(self.left, Literal): + if parent_field: + value = cast(parent_field, value) + if parent_field['type'] in ('char', 'text', 'many2one'): + value += '%' + domain = [(parent_field['name'], operator(parent_field), + value)] + elif value: + domain = [('rec_name', 'ilike', value + '%')] + else: + domain = [] + node = self.left + while node: + if not isinstance(node, Literal): + if node.domain(): + domain = node.domain() + break + node = node.left + else: + domain = self.left.domain(parent_field) + + #Recurse right + if isinstance(self.right, Literal): + if field: + value = cast(field, ' '.join(self.right.flatten())) + if field['type'] in ('char', 'text', 'many2one'): + value += '%' + + domain.append((field['name'] , operator(field), value)) + else: + domain.extend(self.right.domain(field)) + + return domain + +class DoubleDot(Base, InfixMixin): + value = '..' + lbp = 70 + fmt = '%s%s%s' + + def split(self): + assert isinstance(self.right, Literal) + return self.right.split() + + def domain(self, parent_field=None): + res = [] + if parent_field is not None: + field_name = parent_field['name'] + else: + field_name = 'rec_name' + if self.left: + value = cast(parent_field, ' '.join(self.left.flatten())) + res.append((field_name, '>=', value)) + if self.right: + _, value = self.right.split() + value = cast(parent_field, value) + res.append((field_name, '<', value)) + return res + + +class Comma(Base, InfixMixin): + value = ';' + lbp = 70 + fmt = '%s%s %s' + + def split(self, words=""): + return self.right.split() + + def domain(self, parent_field=None): + if parent_field is not None: + field_name = parent_field['name'] + else: + field_name = 'rec_name' + + _, value = self.right.split() + values = [cast(parent_field, value)] + if isinstance(self.right, Comma): + values.append(cast(parent_field, '')) + + def walk(node, values): + while node: + if node.right: + if isinstance(node.right, Literal): + values.append(cast(parent_field, + ' '.join(node.right.flatten()))) + else: + walk(node.right, values) + if isinstance(node, Literal): + values.append(cast(parent_field, + ' '.join(node.flatten()))) + break + if not node.left: + values.append(cast(parent_field, '')) + node = node.left + walk(self.left, values) + + return [(field_name, 'in' , list(reversed(values)))] + + +class Comparator(Base, InfixMixin): + value = '' + lbp = 60 + fmt = '%s %s%s' + + def split(self): + assert isinstance(self.right, Literal) + return self.right.split() + + def domain(self, parent_field=None): + res = [] + if parent_field is not None: + field_name = parent_field['name'] + else: + field_name = 'rec_name' + + if self.left: + res.extend(self.left.domain(parent_field=parent_field)) + + if self.right: + _, value = self.right.split() + value = cast(parent_field, value) + res.append((field_name, self.value, value)) + return res + +class LessThan(Comparator): + value = '<' + +class LessThanOrEqual(Comparator): + value = '<=' + +class BiggerThan(Comparator): + value = '>' + +class BiggerThanOrEqual(Comparator): + value = '>=' + + +class Equal(Base, InfixMixin): + value = '=' + lbp = 60 + fmt = '%s%s%s' + + def domain(self, parent_field=None): + assert not self.left, 'Unexpected left child %s' % self.left + res = [] + if parent_field is not None: + field_name = parent_field['name'] + else: + field_name = 'rec_name' + + if self.right: + _, value = self.right.split() + if parent_field: + value = cast(parent_field, value) + res.append((field_name, '=', value)) + return res + +class Not(Base, InfixMixin): + value = '!' + lbp = 60 + fmt = '%s%s%s' + + def domain(self, parent_field=None): + assert not self.left, 'Unexpected left child %s' % self.left + res = [] + if parent_field is not None: + field_name = parent_field['name'] + else: + field_name = 'rec_name' + + if isinstance(self.right, Comma): + (field_name, oper, value), = self.right.domain(parent_field) + res.append((field_name, 'not %s' % oper, value)) + elif self.right: + _, value = self.right.split() + if parent_field: + value = cast(parent_field, value) + oper = operator(parent_field) + if oper == 'ilike': + oper = 'not ilike' + value += '%' + else: + oper = '!=' + res.append((field_name, oper, value)) + return res + +class NotEqual(Base, InfixMixin): + value = '!=' + lbp = 60 + fmt = '%s%s%s' + + def domain(self, parent_field=None): + assert not self.left, 'Unexpected left child %s' % self.left + res = [] + if parent_field is not None: + field_name = parent_field['name'] + else: + field_name = 'rec_name' + + if self.right: + field, value = self.right.split() + if parent_field: + value = cast(parent_field, value) + res.append((field_name, '!=', value)) + return res + + +class And(Base, InfixMixin): + value = 'and' + lbp = 40 + fmt = '%s %s %s' + + def domain(self, parent_field=None): + result = [] + for token in (self.left, self.right): + if token: + dom = token.domain() + if (isinstance(token, And) + and token.value == self.value): + result.extend(dom) + else: + if len(dom) == 1: + dom, = dom + result.append(dom) + return result + + +class Or(And): + value = 'or' + lbp = 30 + fmt = '%s %s %s' + + def domain(self, parent_field=None): + result = super(Or, self).domain(parent_field) + if result[0] != 'OR': + return ['OR'] + result + else: + return result + + +class CloseParenthesis(Base): + value = ')' + lbp = 10 + fmt = '%s%s %s' + + def nud(self): + return self + + def led(self, left): + self.left = left + return self + + def domain(self, parent_field=None): + if self.left: + return self.left.domain() + return [] + +class OpenParenthesis(Base): + value = '(' + lbp = 10 + fmt = '%s %s%s' + + def nud(self): + expr = self.parser.expression(self.lbp) + self.right = expr + return self + + def led(self, left): + expr = self.parser.expression(self.lbp) + self.right = expr + self.left = left + return self + + def domain(self, parent_field=None): + domain = [] + if self.left: + domain = self.left.domain() + + if self.right: + domain.append(self.right.domain()) + + return domain + + +class End(Base): + value = None + lbp = 0 + + def __init__(self, parser): + super(End, self).__init__(parser) + self.value = '' + + def nud(self): + return self + + def domain(self, parent_field=None): + return [] + + def complete(self, parent_field=None): + if parent_field: + if parent_field['type'] in ('selection', 'reference'): + return (x[1] for x in parent_field['selection']) + elif parent_field['type'] in ('date', 'datetime'): + format_ = date_format() + return (datetime_strftime(datetime.date.today(), format_),) + elif parent_field['type'] == 'boolean': + return (_('True'), _('False')) + return [] + +SPLIT_REGEXP = re.compile( + r'("[^"]*"?)' + '|(\bor\b)' + '|(\band\b)' + '|(\.\.)' + '|(\()' + '|(\))' + '|(;)' + '|([0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})' + '|([0-9]{1,2}:[0-9]{1,2}:)' + '|([0-9]{1,2}:)' + '|(:)' + '|(<)(?!=)' + '|(<=)' + '|(>)(?!=)' + '|(>=)' + '|(=)' + '|(!)(?!=)' + '|(!=)' + '|(\s)' + ) + +TOKENS = dict((t.value, t) for t in [ + And, + Or, + DoubleDot, + Literal, + Colon, + OpenParenthesis, + CloseParenthesis, + Comma, + LessThan, + LessThanOrEqual, + BiggerThan, + BiggerThanOrEqual, + Equal, + Not, + NotEqual, + End] if t.value is not None) + + +class DomainParser: + + def __init__(self, fields): + self.token_flow = None + self.token = None + self.fields = fields + strings = {} + for field in fields.itervalues(): + if not field.get('searchable', True): + continue + strings[field['string']] = field + self.sugg_fields = tuple(strings) + self.dom_fields = dict((key.lower(), value) + for key, value in strings.iteritems()) + + def tokenize(self, query): + for token in SPLIT_REGEXP.split(query): + if token: + token = token.strip() + if not token: + continue + yield token + + def expression(self, rbp=0): + token = self.token + self.token = self.next_token() + left = token.nud() + while self.token.lbp > rbp: + token = self.token + self.token = self.next_token() + left = token.led(left) + return left + + def next_token(self): + try : + new_tk = self.token_flow.next() + except StopIteration: + return End(self) + + if new_tk.startswith('"') or new_tk.endswith('"'): + new_tk = new_tk.strip('"') + + if new_tk in TOKENS: + return TOKENS[new_tk](self) + + return Literal(self, new_tk) + + def parse(self, input_string): + self.token_flow = self.tokenize(input_string) + self.token = self.next_token() + return self.expression() + + def _string_char(self, field, operator, value): + if not operator and isinstance(value, basestring): + if value.endswith('%'): + value = value[:-1] + return '%s: %s' % (field['string'], quote(value)) + elif operator in ('not ilike', 'not'): + operator = '!' + if value.endswith('%'): + value = value[:-1] + if isinstance(value, (list, tuple)): + value = '; '.join(map(quote, value)) + else: + value = quote(value) + return '%s: %s%s' % (field['string'], operator, value) + + _string_text = _string_char + _string_many2one = _string_char + + def _string_integer(self, field, operator, value): + if value is False: + value = '' + if isinstance(value, (list, tuple)): + value = '; '.join(map(str, value)) + return '%s: %s%s' % (field['string'], operator, value) + + def _string_float(self, field, operator, value): + digits = field.get('digits', (16, 2)) + format_ = lambda x: locale.format( + '%.' + str(digits[1]) + 'f', x or 0.0, True) + if isinstance(value, (list, tuple)): + value = '; '.join(map(format_, value)) + else: + value = format_(value) + return '%s: %s%s' % (field['string'], operator, value) + + _string_numeric = _string_float + + def _string_selection(self, field, operator, value): + selections = dict(field['selection']) + if isinstance(value, (list, tuple)): + value = '; '.join(quote(selections.get(x, x)) for x in value) + else: + value = quote(selections.get(value, value)) + return '%s: %s%s' % (field['string'], operator, value) + + _string_reference = _string_selection + + def _string_boolean(self, field, operator, value): + format_ = lambda x: _('True') if x else _('False') + if isinstance(value, (list, tuple)): + value = '; '.join(format_(x) for x in value) + else: + value = format_(value) + return '%s: %s' % (field['string'], value) + + def _string_datetime(self, field, operator, value): + def format_(value): + if not value: + return '' + else: + if (not isinstance(value, datetime.datetime) + or value.time() == datetime.time.min): + format_ = date_format() + else: + format_ = date_format() + ' ' + HM_FORMAT + return quote(datetime_strftime(value, format_)) + if isinstance(value, (list, tuple)): + value = '; '.join(map(format_, value)) + else: + value = format_(value) + return '%s: %s%s' % (field['string'], operator, value) + + def _string_date(self, field, operator, value): + format_ = (lambda x: quote(datetime_strftime(x, date_format())) + if x else '') + if isinstance(value, (list, tuple)): + value = '; '.join(map(format_, value)) + else: + value = format_(value) + return '%s: %s%s' % (field['string'], operator, value) + + def string(self, domain): + if not domain: + return '' + if domain[0] in ('AND', 'OR'): + nary = ' ' if domain[0] == 'AND' else ' or ' + domain = domain[1:] + else: + nary = ' ' + def format(expression): + if (isinstance(expression, (list, tuple)) + and len(expression) > 2 + and isinstance(expression[1], basestring) + and expression[1] in OPERATORS): + field_name, oper, value = expression + if field_name == 'rec_name' and field_name not in self.fields: + if oper == 'ilike': + if value.endswith('%'): + value = value[:-1] + return quote(value) + else: + field = self.fields[field_name] + def_op = operator(field) + if oper in (def_op, '!' + def_op) and value: + oper = oper.rstrip(def_op) + elif oper.endswith('in'): + if oper == 'not in': + oper = '!' + else: + oper = '' + return getattr(self, '_string_%s' % field['type'])(field, + oper, value) + else: + return '(' + self.string(expression) + ')' + return nary.join(format(x) for x in domain) + + +def test_parser(): + fields = { + 'boolean': { + 'string': 'Boolean', + 'name': 'boolean', + 'type': 'boolean', + }, + 'selection': { + 'string': 'Selection', + 'name': 'selection', + 'type': 'selection', + 'selection': [ + ('spam', 'Spam'), + ('ham', 'Ham'), + ('spamham', 'Spamham'), + ], + }, + 'date': { + 'string': 'Date', + 'name': 'date', + 'type': 'date', + }, + 'datetime': { + 'string': 'Date Time', + 'name': 'datetime', + 'type': 'datetime', + }, + 'char': { + 'string': 'Char', + 'name': 'char', + 'type': 'char', + }, + 'numeric': { + 'string': 'Numeric', + 'name': 'numeric', + 'type': 'numeric', + }, + 'integer': { + 'string': 'Integer', + 'name': 'integer', + 'type': 'integer', + }, + 'float': { + 'string': 'Float', + 'name': 'float', + 'type': 'float', + }, + 'notsearchable': { + 'string': 'Not Searchable', + 'name': 'notsearchable', + 'type': 'char', + 'searchable': False, + }, + } + return DomainParser(fields) + +def test_selection_complete(): + parser = test_parser() + assert list(parser.parse('S').complete()) == ['Selection:'] + assert list(parser.parse('Selection:').complete()) == ['Selection: Spam', + 'Selection: Ham', 'Selection: Spamham'] + assert list(parser.parse('Selection: H').complete()) == ['Selection: Ham'] + assert list(parser.parse('Selection: h').complete()) == ['Selection: Ham'] + assert list(parser.parse('Selection: =H').complete()) == [ + 'Selection: =Ham'] + assert list(parser.parse('Selection: Ham').complete()) == [ + 'Selection: Ham'] + assert list(parser.parse('Selection: S').complete()) == [ + 'Selection: Spam', + 'Selection: Spamham', + ] + assert list(parser.parse('Selection: foo').complete()) == [ + 'Selection: foo'] + assert list(parser.parse('Selection: Ham foo').complete()) == [ + 'Selection: Ham foo'] + +def test_selection_domain(): + parser = test_parser() + assert parser.parse('S').domain() == [('rec_name', 'ilike', 'S%')] + assert parser.parse('Selection:').domain() == [] + assert parser.parse('Selection: H').domain() == [('selection', '=', 'H')] + assert parser.parse('Selection: =H').domain() == [('selection', '=', 'H')] + assert parser.parse('Selection: Ham').domain() == [ + ('selection', '=', 'ham')] + assert parser.parse('Selection: Ham; Spam').domain() == [ + ('selection', 'in', ['ham', 'spam'])] + +def test_selection_string(): + parser = test_parser() + assert parser.string([]) == '' + assert parser.string([('rec_name', 'ilike', 'S%')]) == 'S' + assert parser.string([('selection', '=', 'H')]) == 'Selection: H' + assert parser.string([('selection', '=', 'ham')]) == 'Selection: Ham' + assert parser.string([('selection', '!=', 'ham')]) == 'Selection: !Ham' + assert parser.string([('selection', 'in', ['ham', 'spam'])]) == \ + 'Selection: Ham; Spam' + +def test_boolean_complete(): + parser = test_parser() + assert list(parser.parse('b').complete()) == ['Boolean:'] + assert list(parser.parse('boolean:').complete()) == ['Boolean: True', + 'Boolean: False'] + assert list(parser.parse('boolean: true').complete()) == ['Boolean: True'] + assert list(parser.parse('boolean: t').complete()) == ['Boolean: True'] + assert list(parser.parse('boolean: y').complete()) == ['Boolean: Y', + 'Boolean: Yes'] + assert list(parser.parse('boolean: 1').complete()) == ['Boolean: 1'] + assert list(parser.parse('boolean: false').complete()) == ['Boolean: False'] + assert list(parser.parse('boolean: f').complete()) == ['Boolean: False'] + assert list(parser.parse('boolean: n').complete()) == ['Boolean: N', + 'Boolean: No'] + assert list(parser.parse('boolean: 0').complete()) == ['Boolean: 0'] + assert list(parser.parse('boolean: =1').complete()) == ['Boolean: =1'] + +def test_boolean_domain(): + parser = test_parser() + assert parser.parse('B').domain() == [('rec_name', 'ilike', 'B%')] + assert parser.parse('Boolean:').domain() == [] + assert parser.parse('Boolean: true').domain() == [('boolean', '=', True)] + assert parser.parse('Boolean: t').domain() == [('boolean', '=', True)] + assert parser.parse('Boolean: y').domain() == [('boolean', '=', True)] + assert parser.parse('Boolean: 1').domain() == [('boolean', '=', True)] + assert parser.parse('Boolean: true; false').domain() == [ + ('boolean', 'in', [True, False])] + +def test_boolean_string(): + parser = test_parser() + assert parser.string([('boolean', '=', True)]) == 'Boolean: True' + assert parser.string([('boolean', '=', False)]) == 'Boolean: False' + assert parser.string([('boolean', 'in', [True, False])]) == \ + 'Boolean: True; False' + +def test_char_complete(): + parser = test_parser() + assert list(parser.parse('c').complete()) == ['Char:'] + assert list(parser.parse('char:').complete()) == [] + assert list(parser.parse('char: bar').complete()) == ['Char: bar'] + assert list(parser.parse('Char: foo bar').complete()) == [ + 'Char: "foo bar"'] + assert list(parser.parse('char: =foo').complete()) == ['Char: =foo'] + +def test_char_domain(): + parser = test_parser() + assert parser.parse('c').domain() == [('rec_name', 'ilike', 'c%')] + assert parser.parse('Char:').domain() == [] + assert parser.parse('Char: foo bar').domain() == [ + ('char', 'ilike', 'foo bar%')] + assert parser.parse('Char: =foo').domain() == [('char', '=', 'foo')] + assert parser.parse('Char: !foo').domain() == [ + ('char', 'not ilike', 'foo%')] + assert parser.parse('Char: !=foo').domain() == [('char', '!=', 'foo')] + assert parser.parse('Char: != foo').domain() == [('char', '!=', 'foo')] + assert parser.parse('Char: 2; 3').domain() == [('char', 'in', ['2', '3'])] + assert parser.parse('Char:! 2; 3').domain() == [('char', 'not in', ['2', '3'])] + +def test_char_string(): + parser = test_parser() + assert parser.string([('char', 'ilike', 'bar%')]) == 'Char: bar' + assert parser.string([('char', 'ilike', 'foo bar%')]) == 'Char: "foo bar"' + assert parser.string([('char', '=', 'foo')]) == 'Char: =foo' + assert parser.string([('char', 'not ilike', 'foo%')]) == 'Char: !foo' + assert parser.string([('char', '!=', 'foo')]) == 'Char: !=foo' + assert parser.string([('char', 'in', ['2', '3'])]) == 'Char: 2; 3' + assert parser.string([('char', 'not in', ['2', '3'])]) == 'Char: !2; 3' + + +def test_numeric_complete(): + parser = test_parser() + assert list(parser.parse('numeric: 100').complete()) == [ + 'Numeric: 100'] + +def test_numeric_domain(): + parser = test_parser() + assert parser.parse('Numeric: >100').domain() == [ + ('numeric', '>', Decimal(100))] + assert parser.parse('Numeric:>=100').domain() == [ + ('numeric', '>=', Decimal(100))] + assert parser.parse('Numeric: =100').domain() == [ + ('numeric', '=', Decimal(100))] + assert parser.parse('Numeric: 100').domain() == [ + ('numeric', '=', Decimal(100))] + assert parser.parse('Numeric: foo').domain() == [ + ('numeric', '=', False)] + assert parser.parse('Numeric: 2; 3').domain() == [ + ('numeric', 'in', [Decimal(2), Decimal(3)])] + +def test_numeric_string(): + parser = test_parser() + assert parser.string([('numeric', '=', Decimal(100))]) == 'Numeric: 100.00' + assert parser.string([('numeric', 'in', [Decimal(2), Decimal(3)])]) == \ + 'Numeric: 2.00; 3.00' + +def test_integer_complete(): + parser = test_parser() + assert list(parser.parse('integer: 42').complete()) == ['Integer: 42'] + assert list(parser.parse('integer: 2;3').complete()) == ['Integer: 2; 3'] + +def test_integer_domain(): + parser = test_parser() + assert parser.parse('Integer: >42').domain() == [ + ('integer', '>', 42)] + assert parser.parse('Integer: >=42').domain() == [ + ('integer', '>=', 42)] + assert parser.parse('Integer: =42').domain() == [ + ('integer', '=', 42)] + assert parser.parse('Integer: 42').domain() == [ + ('integer', '=', 42)] + assert parser.parse('Integer: 3.14').domain() == [ + ('integer', '=', 3.14)] + assert parser.parse('Integer: foo').domain() == [ + ('integer', '=', False)] + assert parser.parse('Integer: 2; 3').domain() == [ + ('integer', 'in', [2, 3])] + +def test_integer_string(): + parser = test_parser() + assert parser.string([('integer', '=', 42)]) == 'Integer: 42' + assert parser.string([('integer', '=', False)]) == 'Integer: =' + assert parser.string([('integer', 'in', [2, 3])]) == 'Integer: 2; 3' + + +def test_float_complete(): + parser = test_parser() + assert list(parser.parse('float: 3.14').complete()) == ['Float: 3.14'] + +def test_float_domain(): + parser = test_parser() + assert parser.parse('Float: >3.14').domain() == [ + ('float', '>', 3.14)] + assert parser.parse('Float: >=3.14').domain() == [ + ('float', '>=', 3.14)] + assert parser.parse('Float: =3.14').domain() == [ + ('float', '=', 3.14)] + assert parser.parse('Float: 3.14').domain() == [ + ('float', '=', 3.14)] + assert parser.parse('Float: 42').domain() == [ + ('float', '=', 42.0)] + assert parser.parse('Float: foo').domain() == [ + ('float', '=', False)] + +def test_float_string(): + parser = test_parser() + assert parser.string([('float', '=', 3.14)]) == 'Float: 3.14' + assert parser.string([('float', '=', 42)]) == 'Float: 42.00' + assert parser.string([('float', '>', 42)]) == 'Float: >42.00' + assert parser.string([('float', 'not in', [3.14, 42])]) == \ + 'Float: !3.14; 42.00' + +def test_date_complete(): + today = datetime.date.today() + today_str = datetime_strftime(today, date_format()) + parser = test_parser() + assert list(parser.parse('Date:').complete()) == [ + 'Date Time: ' + today_str, + 'Date: ' + today_str, + ] + assert list(parser.parse('Date: ' + today_str[0]).complete()) == [ + 'Date Time: ' + today_str, + 'Date: ' + today_str, + ] + assert list(parser.parse('Date: 12').complete()) == [ + 'Date Time: 12' + today_str[2:], + 'Date: 12' + today_str[2:], + ] + assert list(parser.parse('Date: 12/04').complete()) == [ + 'Date Time: 12/04' + today_str[5:], + 'Date: 12/04' + today_str[5:], + ] + assert list(parser.parse('Date: 12/4').complete()) == [ + 'Date Time: 12/04' + today_str[5:], + 'Date: 12/04' + today_str[5:], + ] + assert list(parser.parse('Date: 12/4/2002').complete()) == [ + 'Date Time: 12/04/2002', + 'Date: 12/04/2002', + ] + +def test_date_domain(): + today = datetime.date.today() + today_str = datetime_strftime(today, date_format()) + parser = test_parser() + assert parser.parse('Date:').domain() == [] + assert parser.parse('Date: ' + today_str[0]).domain() == [ + ('date', '=', False)] + assert parser.parse('Date: >' + today_str[0]).domain() == [ + ('date', '>', False)] + assert parser.parse('Date: 12/04/2002').domain() == [ + ('date', '=', datetime.date(2002, 12, 4))] + assert parser.parse('Date: 12/4/2002').domain() == [ + ('date', '=', datetime.date(2002, 12, 4))] + assert parser.parse('Date: 12/4/2002; 1/1/1970').domain() == [ + ('date', 'in', [datetime.date(2002, 12, 4), datetime.date(1970, 1, 1)])] + +def test_date_string(): + parser = test_parser() + assert parser.string([('date', '=', False)]) == 'Date: =' + assert parser.string([('date', '>=', datetime.date(2002, 12, 4))]) == \ + 'Date: >=12/04/2002' + assert parser.string([('date', 'in', [datetime.date(2002, 12, 4), + datetime.date(1970, 1, 1)])]) == \ + 'Date: 12/04/2002; 01/01/1970' + +def test_datetime_complete(): + today = datetime.date.today() + today_str = datetime_strftime(today, date_format()) + parser = test_parser() + assert list(parser.parse('Date Time: ' + today_str).complete()) == [ + 'Date Time: ' + today_str] + assert list(parser.parse('Date Time: "' + today_str + ' 12:30:00"' + ).complete()) == ['Date Time: "' + today_str + ' 12:30:00"'] + assert list(parser.parse('Date Time: ' + today_str + ' 12:30:00' + ).complete()) == ['Date Time: "' + today_str + ' 12:30:00"'] + +def test_datetime_domain(): + today = datetime.date.today() + today_str = datetime_strftime(today, date_format()) + parser = test_parser() + assert parser.parse('Date Time: ' + today_str).domain() == [ + ('datetime', '=', datetime.datetime.combine(today, datetime.time.min))] + assert parser.parse('Date Time: "' + today_str + ' 12:30:00"').domain() ==\ + [('datetime', '=', datetime.datetime.combine(today, + datetime.time(12, 30)))] + assert parser.parse('Date Time: ' + today_str + ' 12:30:00').domain() == [ + ('datetime', '=', datetime.datetime.combine(today, + datetime.time(12, 30)))] + assert parser.parse('Date Time: foo').domain() == [ + ('datetime', '=', False)] + assert parser.parse('Date Time: 12/4/2002; 1/1/1970').domain() == [ + ('datetime', 'in', [ + datetime.datetime.combine(datetime.date(2002, 12, 4), + datetime.time.min), + datetime.datetime.combine(datetime.date(1970, 1, 1), + datetime.time.min)])] + +def test_datetime_string(): + parser = test_parser() + assert parser.string([('datetime', '=', False)]) == 'Date Time: =' + assert parser.string([('datetime', '=', datetime.datetime(2002, 12, 4, 12, + 30))]) == 'Date Time: "12/04/2002 12:30:00"' + assert parser.string([('datetime', '=', datetime.datetime(2002, 12, 4, 0, + 0))]) == 'Date Time: 12/04/2002' + assert parser.string([('datetime', 'in', [ + datetime.datetime.combine(datetime.date(2002, 12, 4), + datetime.time.min), + datetime.datetime.combine(datetime.date(1970, 1, 1), + datetime.time.min)])]) == \ + 'Date Time: 12/04/2002; 01/01/1970' + +def test_composite_complete(): + today = datetime.date.today() + today_str = datetime_strftime(today, date_format()) + parser = test_parser() + assert list(parser.parse('char: foo boolean: false').complete()) == [ + 'Char: foo Boolean: False'] + assert list(parser.parse('char: foo or char: bar').complete()) == [ + 'Char: foo or Char: bar'] + assert list(parser.parse('char: foo b').complete()) == [ + 'Char: foo Boolean:'] + assert list(parser.parse('(char: foo b').complete()) == [ + '(Char: foo Boolean:'] + assert list(parser.parse('char: foo and b').complete()) == [ + 'Char: foo and Boolean:'] + assert list(parser.parse('(char: foo and b').complete()) == [ + '(Char: foo and Boolean:'] + assert list(parser.parse('selection: Ham c').complete()) == [ + 'Selection: Ham Char:'] + assert list(parser.parse('selection: ham c').complete()) == [ + 'Selection: Ham Char:'] + assert list(parser.parse('date: ' + today_str + ' c').complete()) == [ + 'Date Time: ' + today_str + ' Char:', + 'Date: ' + today_str + ' Char:'] + assert list(parser.parse('Selection: H Selection').complete()) == [ + 'Selection: Ham Selection:'] + assert list(parser.parse('Selection: H or Selection').complete()) == [ + 'Selection: Ham or Selection:'] + assert list(parser.parse('Selection: H Selection:').complete()) == [ + 'Selection: Ham Selection: Spam', + 'Selection: Ham Selection: Ham', + 'Selection: Ham Selection: Spamham'] + assert list(parser.parse('Selection: H or Selection:').complete()) == [ + 'Selection: Ham or Selection: Spam', + 'Selection: Ham or Selection: Ham', + 'Selection: Ham or Selection: Spamham'] + assert list(parser.parse('Selection: H Selection: S').complete()) == [ + 'Selection: Ham Selection: Spam', + 'Selection: Ham Selection: Spamham', + ] + assert list(parser.parse('Selection: H or Selection: S').complete()) == [ + 'Selection: Ham or Selection: Spam', + 'Selection: Ham or Selection: Spamham', + ] + +def test_composite_domain(): + parser = test_parser() + assert parser.parse('char: foo boolean: false').domain() == [ + ('char', 'ilike', 'foo%'), ('boolean', '=', False)] + assert parser.parse('char: foo and char: bar').domain() == [ + ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')] + assert parser.parse('char: foo or char: bar').domain() == [ + 'OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')] + assert parser.parse('char: foo or char: bar and boolean: 1').domain() == [ + 'OR', ('char', 'ilike', 'foo%'), [ + ('char', 'ilike', 'bar%'), ('boolean', '=', True)]] + assert parser.parse('char: foo and char: bar or boolean: 0').domain() == [ + 'OR', [('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')], + ('boolean', '=', False)] + assert parser.parse('char: foo and char: bar and boolean: 0').domain() == [ + ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%'), + ('boolean', '=', False)] + assert parser.parse('char: foo or char: bar or boolean: 0').domain() == [ + 'OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%'), + ('boolean', '=', False)] + assert parser.parse('(char: foo or char: bar) and boolean: 1').domain() == [ + ['OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')], + ('boolean', '=', True)] + assert parser.parse('(char: foo or char: bar) boolean: 1').domain() == [ + ['OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')], + ('boolean', '=', True)] + assert parser.parse('(char: foo or char: bar) and Date Time: 12/4/2002' + ).domain() == [ + ['OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')], + ('datetime', '=', datetime.datetime(2002, 12, 4, 0, 0))] + assert parser.parse('(char: foo or char: bar) Date Time: 12/4/2002').domain() == [ + ['OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')], + ('datetime', '=', datetime.datetime(2002, 12, 4, 0, 0))] + assert parser.parse('char: foo or (char: bar and boolean: 1)').domain() == [ + 'OR', ('char', 'ilike', 'foo%'), [ + ('char', 'ilike', 'bar%'), ('boolean', '=', True)]] + assert parser.parse('char: foo and (char: bar or boolean: 0)').domain() == [ + ('char', 'ilike', 'foo%'), + ['OR', ('char', 'ilike', 'bar%'), ('boolean', '=', False)]] + assert parser.parse('char: foo (char: bar or boolean: 0)').domain() == [ + ('char', 'ilike', 'foo%'), + ['OR', ('char', 'ilike', 'bar%'), ('boolean', '=', False)]] + assert parser.parse('char: foo and ((char: bar or boolean: 0))' + ).domain() == [('char', 'ilike', 'foo%'), + [['OR', ('char', 'ilike', 'bar%'), + ('boolean', '=', False)]]] + assert parser.parse('char: foo ((char: bar or boolean: 0))' + ).domain() == [ + ('char', 'ilike', 'foo%'), + [['OR', ('char', 'ilike', 'bar%'), ('boolean', '=', False)]]] + assert parser.parse('char: foo (char: test (char: bar or boolean: 0))' + ).domain() == [ + ('char', 'ilike', 'foo%'), + [('char', 'ilike', 'test%')], + ['OR', ('char', 'ilike', 'bar%'), ('boolean', '=', False)]] + assert parser.parse('char: foo and (').domain() == [ + ('char', 'ilike', 'foo%'), []] + assert parser.parse('char: foo and ()').domain() == [ + ('char', 'ilike', 'foo%'), []] + assert parser.parse('char: foo and (char: bar or boolean: 0').domain() == [ + ('char', 'ilike', 'foo%'), + ['OR', ('char', 'ilike', 'bar%'), ('boolean', '=', False)]] + +def test_composite_string(): + parser = test_parser() + assert parser.string([ + ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')]) == \ + 'Char: foo Char: bar' + assert parser.string([ + 'AND', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')]) == \ + 'Char: foo Char: bar' + assert parser.string([ + 'OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')]) == \ + 'Char: foo or Char: bar' + assert parser.string([ + 'OR', ('char', 'ilike', 'foo%'), [ + ('char', 'ilike', 'bar%'), ('boolean', '=', True)]]) == \ + 'Char: foo or (Char: bar Boolean: True)' + assert parser.string([ + 'OR', [('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%')], + ('boolean', '=', False)]) == \ + '(Char: foo Char: bar) or Boolean: False' + assert parser.string([ + ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%'), + ('boolean', '=', False)]) == \ + 'Char: foo Char: bar Boolean: False' + assert parser.string([ + 'OR', ('char', 'ilike', 'foo%'), ('char', 'ilike', 'bar%'), + ('boolean', '=', False)]) == \ + 'Char: foo or Char: bar or Boolean: False' + assert parser.string([ + 'OR', ('char', 'ilike', 'foo%'), [ + ('char', 'ilike', 'bar%'), ('boolean', '=', True)]]) == \ + 'Char: foo or (Char: bar Boolean: True)' + assert parser.string([ + ('char', 'ilike', 'foo%'), + ['OR', ('char', 'ilike', 'bar%'), ('boolean', '=', False)]]) == \ + 'Char: foo (Char: bar or Boolean: False)' + +def test_quote_complete(): + parser = test_parser() + assert list(parser.parse('"char"').complete()) == ['Char:'] + assert list(parser.parse('"char:"').complete()) == ['"char:"'] + assert list(parser.parse('"Char:"').complete()) == ['"Char:"'] + assert list(parser.parse('char: "foo bar"').complete()) == [ + 'Char: "foo bar"'] + assert list(parser.parse('Char: "Boolean: false"').complete()) == [ + 'Char: "Boolean: false"'] + +def test_quote_domain(): + parser = test_parser() + assert parser.parse('"char"').domain() == [('rec_name', 'ilike', 'char%')] + assert parser.parse('char: "foo bar"').domain() == [ + ('char', 'ilike', 'foo bar%')] + +def test_quote_string(): + parser = test_parser() + assert parser.string([('rec_name', 'ilike', 'char:%')]) == '"char:"' + assert parser.string([('char', 'ilike', 'foo bar%')]) == 'Char: "foo bar"' + assert parser.string([('char', 'ilike', 'Boolean: false%')]) == \ + 'Char: "Boolean: false"' + +def test_column(): + parser = test_parser() + assert parser.parse(':').domain() == [] + assert parser.parse('::').domain() == [] + assert parser.parse(': foo').domain() == [] + assert parser.parse('foo :').domain() == [('rec_name', 'ilike', 'foo%')] + +def test_double_dot(): + parser = test_parser() + assert parser.parse('Integer: 0..42').domain() == [ + ('integer', '>=', 0), ('integer', '<', 42)] + assert parser.parse('Numeric: 0..42').domain() == [ + ('numeric', '>=', Decimal(0)), ('numeric', '<', Decimal(42))] + assert parser.parse('a..z').domain() == [ + ('rec_name', '>=', 'a'), ('rec_name', '<', 'z')] + assert parser.parse('Char: a..z').domain() == [ + ('char', '>=', 'a'), ('char', '<', 'z')] + assert parser.parse('Integer: 0..42 Char: foo').domain() == [ + ('integer', '>=', 0), ('integer', '<', 42), ('char', 'ilike', 'foo%')] + +def test_comma(): + parser = test_parser() + assert parser.parse('Char: foo; bar').domain() == [ + ('char', 'in', ['foo', 'bar'])] + assert parser.parse('foo; bar').domain() == [ + ('rec_name', 'in', ['foo', 'bar'])] + assert parser.parse('Char: foo; bar Selection: Ham').domain() == [ + ('char', 'in', ['foo', 'bar']), ('selection', '=', 'ham')] + assert parser.parse('Char: foo bar; test').domain() == [ + ('char', 'in', ['foo bar', 'test'])] + assert parser.parse('Char: foo; bar; Selection: Ham').domain() == [ + ('char', 'in', ['foo', 'bar', '']), ('selection', '=', 'ham')] + assert parser.parse('Char: ;foo; bar').domain() == [ + ('char', 'in', ['', 'foo', 'bar'])] + assert parser.parse('Char: foo;; bar').domain() == [ + ('char', 'in', ['foo', '', 'bar'])] + assert parser.parse('Char: foo;;; bar').domain() == [ + ('char', 'in', ['foo', '', 'bar'])] + assert parser.parse('Char: ;;foo;; bar;;').domain() == [ + ('char', 'in', ['', '', 'foo', '', 'bar', '', ''])] + assert parser.parse('Integer: 0; 1; 1; 2; 3; 5').domain() == [ + ('integer', 'in', [0, 1, 1, 2, 3, 5])] + assert parser.parse('Integer: 0; 1;; 2; 3').domain() == [ + ('integer', 'in', [0, 1, 0, 2, 3])] + +def test_comparator(): + parser = test_parser() + assert parser.parse('< foo').domain() == [ + ('rec_name', '<', 'foo')] + assert parser.parse('=foo').domain() == [ + ('rec_name', '=', 'foo')] + assert parser.parse('!foo').domain() == [ + ('rec_name', 'not ilike', 'foo%')] + assert parser.parse('!=foo').domain() == [ + ('rec_name', '!=', 'foo')] + assert parser.parse('Integer:< Selection: Ham').domain() == [ + ('integer', '<', 0), ('selection', '=', 'ham')] + assert parser.parse('Integer: << 10').domain() == [ + ('integer', '<', 10)] + assert parser.parse('Integer: <> 10').domain() == [ + ('integer', '<', 10)] + assert parser.parse('Integer: !10').domain() == [ + ('integer', '!=', 10)] + assert parser.parse('foo <').domain() == [ + ('rec_name', 'ilike', 'foo%'), ('rec_name', '<', '')] + +def test_str(): + parser = test_parser() + assert str(parser.parse('foo bar spam ham =')) == '''\ +[root] = + [left] ham + [left] spam + [left] bar + [left] foo + [right] ''' + +if __name__ == '__main__': + test_selection_complete() + test_selection_domain() + test_selection_string() + test_boolean_complete() + test_boolean_domain() + test_boolean_string() + test_char_complete() + test_char_domain() + test_char_string() + test_numeric_complete() + test_numeric_domain() + test_numeric_string() + test_integer_complete() + test_integer_domain() + test_integer_string() + test_float_complete() + test_float_domain() + test_float_string() + test_date_complete() + test_date_domain() + test_date_string() + test_datetime_complete() + test_datetime_domain() + test_datetime_string() + test_composite_complete() + test_composite_domain() + test_composite_string() + test_quote_complete() + test_quote_domain() + test_quote_string() diff --git a/tryton/gui/main.py b/tryton/gui/main.py index 4728e56f89..ea482c95cc 100644 --- a/tryton/gui/main.py +++ b/tryton/gui/main.py @@ -101,8 +101,6 @@ class Main(object): gtk.gdk.CONTROL_MASK|gtk.gdk.SHIFT_MASK) gtk.accel_map_add_entry('/Form/Delete', gtk.keysyms.D, gtk.gdk.CONTROL_MASK) - gtk.accel_map_add_entry('/Form/Find', gtk.keysyms.F, - gtk.gdk.CONTROL_MASK) gtk.accel_map_add_entry('/Form/Next', gtk.keysyms.Page_Down, 0) gtk.accel_map_add_entry('/Form/Previous', gtk.keysyms.Page_Up, diff --git a/tryton/gui/window/form.py b/tryton/gui/window/form.py index cd97a03cb2..44dcf6905d 100644 --- a/tryton/gui/window/form.py +++ b/tryton/gui/window/form.py @@ -11,7 +11,6 @@ from tryton.action import Action from tryton.config import CONFIG from tryton.gui import Main from tryton.gui.window import Window -from tryton.gui.window.win_search import WinSearch from tryton.gui.window.preference import Preference from tryton.gui.window.win_export import WinExport from tryton.gui.window.win_import import WinImport @@ -59,7 +58,6 @@ class Form(SignalEvent, TabContent): (_('_Delete...'), 'tryton-delete', 'sig_remove', '/Form/Delete'), (None,) * 4, - (_('_Find...'), 'tryton-find', 'sig_search', '/Form/Find'), (_('_Next'), 'tryton-go-next', 'sig_next', '/Form/Next'), (_('_Previous'), 'tryton-go-previous', 'sig_previous', '/Form/Previous'), @@ -442,16 +440,6 @@ class Form(SignalEvent, TabContent): menu.show_all() menu.popup(None, None, menu_position, 0, 0) - def sig_search(self, widget=None): - if not self.modified_save(): - return - def callback(result): - if result: - self.screen.clear() - self.screen.load(result) - WinSearch(self.model, callback, domain=self.domain, - context=self.context) - def message_info(self, message, color='red'): if message: self.info_label.set_label(message) diff --git a/tryton/gui/window/view_form/screen/screen.py b/tryton/gui/window/view_form/screen/screen.py index a3bb57576d..6c2dff31cf 100644 --- a/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/gui/window/view_form/screen/screen.py @@ -9,13 +9,13 @@ import xml.dom.minidom import tryton.rpc as rpc from tryton.gui.window.view_form.model.group import Group from tryton.gui.window.view_form.view.screen_container import ScreenContainer -from tryton.gui.window.view_form.widget_search import Form from tryton.signal_event import SignalEvent from tryton.common import node_attributes from tryton.config import CONFIG import tryton.common as common from tryton.exceptions import TrytonServerError from tryton.jsonrpc import JSONEncoder +from tryton.common.tdp import DomainParser class Screen(SignalEvent): @@ -36,6 +36,8 @@ class Screen(SignalEvent): if domain is None: domain = [] + self.limit = limit or 1000 + self.offset = 0 super(Screen, self).__init__() self.readonly = readonly @@ -62,13 +64,13 @@ class Screen(SignalEvent): self.screen_container.alternate_view = alternate_view self.widget = self.screen_container.widget_get() self.__current_view = 0 - self.limit = limit self.search_value = search_value self.fields_view_tree = None self.sort = sort self.view_to_load = [] self.expanded_nodes = collections.defaultdict( lambda: collections.defaultdict(lambda: None)) + self.domain_parser = None if mode: self.view_to_load = mode[1:] @@ -84,68 +86,86 @@ class Screen(SignalEvent): def search_active(self, active=True): if active and not self.parent: - if not self.filter_widget: - if not self.fields_view_tree: - ctx = {} - ctx.update(rpc.CONTEXT) - ctx.update(self.context) - try: - self.fields_view_tree = rpc.execute('model', - self.model_name, 'fields_view_get', False, - 'tree', ctx) - except TrytonServerError: - return - self.filter_widget = Form(self.fields_view_tree, - self.model_name, self.domain, (self, self.search_filter), - self.context) - self.screen_container.add_filter(self.filter_widget.widget, - self.search_filter, self.search_clear, - self.search_prev, self.search_next) - self.filter_widget.set_limit(self.limit) - self.filter_widget.value = self.search_value + if not self.fields_view_tree: + ctx = {} + ctx.update(rpc.CONTEXT) + ctx.update(self.context) + try: + self.fields_view_tree = rpc.execute('model', + self.model_name, 'fields_view_get', False, + 'tree', ctx) + except TrytonServerError: + return + + fields = self.fields_view_tree['fields'] + for name, props in fields.iteritems(): + if props['type'] not in ('selection', 'reference'): + continue + if isinstance(props['selection'], (tuple, list)): + continue + props['selection'] = self.get_selection(props) + + self.domain_parser = DomainParser( + self.fields_view_tree['fields']) + + self.screen_container.set_screen(self) self.screen_container.show_filter() else: self.screen_container.hide_filter() - def search_prev(self, widget=None): - self.filter_widget.prev() - self.search_filter() + def get_selection(self, props): + try: + selection = rpc.execute('model', + self.model_name, props['selection'], rpc.CONTEXT) + except TrytonServerError, exception: + common.process_exception(exception, None) + selection = [] + selection.sort(lambda x, y: cmp(x[1], y[1])) + return selection - def search_next(self, widget=None): - self.filter_widget.next() - self.search_filter() + def search_prev(self, search_string): + self.offset -= self.limit + self.search_filter(search_string=search_string) - def search_clear(self, widget=None): - self.filter_widget.clear() - self.clear() + def search_next(self, search_string): + self.offset += self.limit + self.search_filter(search_string=search_string) - def search_filter(self, widget=None, only_ids=False): - limit = None - offset = 0 - values = [] - if self.filter_widget: - limit = self.filter_widget.get_limit() - offset = self.filter_widget.get_offset() - values = self.filter_widget.value + def search_complete(self, search_string): + parsed_tree = self.domain_parser.parse(search_string) + res = list(r for r in parsed_tree.complete() \ + if r.strip() != search_string) + return res + + def search_filter(self, search_string=None, only_ids=False): + domain = [] + + if self.domain_parser: + if search_string is not None: + domain = self.domain_parser.parse(search_string or '').domain() + else: + domain = self.search_value + self.screen_container.set_text(self.domain_parser.string(domain)) else: - values = [('id', 'in', [x.id for x in self.group])] + domain = [('id', 'in', [x.id for x in self.group])] + ctx = {} ctx.update(rpc.CONTEXT) ctx.update(self.context) - if values: + if domain: if self.domain: - values = ['AND', values, self.domain] + domain = ['AND', domain, self.domain] else: - values = self.domain - rpc_args = ('model', self.model_name, 'search', values, offset, limit, - self.sort, ctx) + domain = self.domain + rpc_args = ('model', self.model_name, 'search', domain, self.offset, + self.limit, self.sort, ctx) try: ids = rpc.execute(*rpc_args) except TrytonServerError, exception: ids = (common.process_exception(exception, *rpc_args) or []) if not only_ids: - if len(ids) == limit: - rpc_args = ('model', self.model_name, 'search_count', values, + if len(ids) == self.limit: + rpc_args = ('model', self.model_name, 'search_count', domain, ctx) try: self.search_count = rpc.execute(*rpc_args) @@ -154,9 +174,9 @@ class Screen(SignalEvent): *rpc_args) or 0) else: self.search_count = len(ids) - self.screen_container.but_prev.set_sensitive(bool(offset)) - if (len(ids) == limit - and self.search_count > limit + offset): + self.screen_container.but_prev.set_sensitive(bool(self.offset)) + if (len(ids) == self.limit + and self.search_count > self.limit + self.offset): self.screen_container.but_next.set_sensitive(True) else: self.screen_container.but_next.set_sensitive(False) @@ -220,12 +240,8 @@ class Screen(SignalEvent): def __set_current_record(self, record): self.__current_record = record - if self.filter_widget: - offset = int(self.filter_widget.get_offset()) - else: - offset = 0 try: - pos = self.group.index(record) + offset + 1 + pos = self.group.index(record) + self.offset + 1 except ValueError: pos = [] i = record @@ -234,7 +250,7 @@ class Screen(SignalEvent): i = i.parent pos.reverse() pos = tuple(pos) - self.signal('record-message', (pos or 0, len(self.group) + offset, + self.signal('record-message', (pos or 0, len(self.group) + self.offset, self.search_count, record and record.id)) attachment_count = 0 if record and record.attachment_count > 0: diff --git a/tryton/gui/window/view_form/view/screen_container.py b/tryton/gui/window/view_form/view/screen_container.py index 291b453025..344cbc487d 100644 --- a/tryton/gui/window/view_form/view/screen_container.py +++ b/tryton/gui/window/view_form/view/screen_container.py @@ -16,6 +16,7 @@ class ScreenContainer(object): self.filter_vbox = None self.but_prev = None self.but_next = None + self.current_text = '' self.alternate_viewport = gtk.Viewport() self.alternate_viewport.set_shadow_type(gtk.SHADOW_NONE) self.alternate_view = False @@ -23,20 +24,35 @@ class ScreenContainer(object): def widget_get(self): return self.vbox - def add_filter(self, widget, fnct, clear_fnct, prev_fnct, next_fnct): - tooltips = common.Tooltips() + def set_screen(self, screen): + self.screen = screen + tooltips = common.Tooltips() + if self.filter_vbox is not None: + return self.filter_vbox = gtk.VBox(spacing=0) self.filter_vbox.set_border_width(0) hbox = gtk.HBox(homogeneous=False, spacing=0) label = gtk.Label(_('Search')) label.set_alignment(0.0, 0.5) - hbox.pack_start(label, expand=True, fill=True) + hbox.pack_start(label, expand=False, fill=False, padding=5) + + self.search_entry = gtk.Entry() + self.search_entry.set_alignment(0.0) + self.completion = gtk.EntryCompletion() + self.completion.set_model(gtk.ListStore(str)) + self.completion.set_text_column(0) + self.completion.set_match_func(lambda *a: True, None) + self.search_entry.connect_after('activate', self.do_search) + self.search_entry.set_completion(self.completion) + self.search_entry.connect('changed', self.changed) + + hbox.pack_start(self.search_entry, expand=True, fill=True, padding=5) but_prev = gtk.Button() self.but_prev = but_prev tooltips.set_tip(but_prev, _('Previous')) - but_prev.connect('clicked', prev_fnct) + but_prev.connect('clicked', self.search_prev) img_prev = gtk.Image() img_prev.set_from_stock('tryton-go-previous', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -48,7 +64,7 @@ class ScreenContainer(object): but_next = gtk.Button() self.but_next = but_next tooltips.set_tip(but_next, _('Next')) - but_next.connect('clicked', next_fnct) + but_next.connect('clicked', self.search_next) img_next = gtk.Image() img_next.set_from_stock('tryton-go-next', gtk.ICON_SIZE_SMALL_TOOLBAR) @@ -59,7 +75,7 @@ class ScreenContainer(object): but_find = gtk.Button() tooltips.set_tip(but_find, _('Find')) - but_find.connect('clicked', fnct) + but_find.connect('clicked', self.do_search) img_find = gtk.Image() img_find.set_from_stock('tryton-find', gtk.ICON_SIZE_SMALL_TOOLBAR) img_find.set_alignment(0.5, 0.5) @@ -69,7 +85,7 @@ class ScreenContainer(object): but_clear = gtk.Button() tooltips.set_tip(but_clear, _('Clear')) - but_clear.connect('clicked', clear_fnct) + but_clear.connect('clicked', self.clear) img_clear = gtk.Image() img_clear.set_from_stock('tryton-clear', gtk.ICON_SIZE_SMALL_TOOLBAR) img_clear.set_alignment(0.5, 0.5) @@ -83,7 +99,6 @@ class ScreenContainer(object): hseparator = gtk.HSeparator() hseparator.show() self.filter_vbox.pack_start(hseparator, expand=True, fill=False) - self.filter_vbox.pack_start(widget, expand=True, fill=True) self.vbox.pack_start(self.filter_vbox, expand=False, fill=True) @@ -114,3 +129,29 @@ class ScreenContainer(object): self.viewport.remove(self.viewport.get_child()) self.viewport.add(widget) self.viewport.show_all() + + def changed(self, editable): + res = self.screen.search_complete(self.get_text()) + self.current_text = self.get_text() + model = self.completion.get_model() + model.clear() + for r in res: + model.append([r.strip()]) + + def clear(self, widget=None): + self.search_entry.set_text(''); + + def get_text(self): + return self.search_entry.get_text().strip().decode('utf-8') + + def set_text(self, value): + return self.search_entry.set_text(value) + + def search_next(self, widget=None): + self.screen.search_next(self.get_text()) + + def search_prev(self, widget=None): + self.screen.search_prev(self.get_text()) + + def do_search(self, widget=None): + self.screen.search_filter(self.get_text()) diff --git a/tryton/gui/window/view_form/widget_search/__init__.py b/tryton/gui/window/view_form/widget_search/__init__.py deleted file mode 100644 index ff8a6ab9fe..0000000000 --- a/tryton/gui/window/view_form/widget_search/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -from form import * diff --git a/tryton/gui/window/view_form/widget_search/calendar.py b/tryton/gui/window/view_form/widget_search/calendar.py deleted file mode 100644 index 2c79bf056a..0000000000 --- a/tryton/gui/window/view_form/widget_search/calendar.py +++ /dev/null @@ -1,203 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import time -import datetime -import gtk -import gettext -import locale -from interface import Interface -from tryton.common import DT_FORMAT, DHM_FORMAT, TRYTON_ICON -from tryton.common import date_widget, Tooltips, datetime_strftime -from tryton.common import get_toplevel_window -from tryton.translate import date_format -import gobject - -_ = gettext.gettext - - -class Calendar(Interface): - - def __init__(self, name, attrs=None, context=None, - on_change=None): - super(Calendar, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - - tooltips = Tooltips() - self.widget = gtk.HBox(spacing=3) - - self.format = date_format() - - self.liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self.combo = gtk.ComboBox(self.liststore) - cell = gtk.CellRendererText() - self.combo.pack_start(cell, True) - self.combo.add_attribute(cell, 'text', 1) - for oper in (['=', _('is')], - ['between', _('is between')], - ['not between', _('is not between')], - ['!=', _('is not')], - ): - self.liststore.append(oper) - self.combo.set_active(0) - self.widget.pack_start(self.combo, False, False) - self.combo.connect('changed', self._changed) - self.combo.connect('changed', self.on_change) - - self.widget1 = date_widget.ComplexEntry(self.format, spacing=3) - self.widget1.show() - self.entry1 = self.widget1.widget - self.entry1.set_property('width-chars', 10) - self.entry1.set_property('activates_default', True) - self.entry1.connect('key_press_event', self.on_change) - tooltips.set_tip(self.entry1, _('Start date')) - self.widget.pack_start(self.widget1, expand=False, fill=True) - - self.eb1 = gtk.EventBox() - tooltips.set_tip(self.eb1, _('Open the calendar')) - self.eb1.set_events(gtk.gdk.BUTTON_PRESS) - self.eb1.connect('button_press_event', self.cal_open, self.entry1) - img = gtk.Image() - img.set_from_stock('tryton-find', gtk.ICON_SIZE_BUTTON) - img.set_alignment(0.5, 0.5) - self.eb1.add(img) - self.widget.pack_start(self.eb1, expand=False, fill=False) - - self.separator = gtk.Label('-') - self.widget.pack_start(self.separator, expand=False, fill=False) - - self.widget2 = date_widget.ComplexEntry(self.format, spacing=3) - self.entry2 = self.widget2.widget - self.entry2.set_property('width-chars', 10) - self.entry2.set_property('activates_default', True) - self.entry2.connect('key_press_event', self.on_change) - tooltips.set_tip(self.entry2, _('End date')) - self.widget.pack_start(self.widget2, expand=False, fill=True) - - self.eb2 = gtk.EventBox() - tooltips.set_tip(self.eb2, _('Open the calendar')) - self.eb2.set_events(gtk.gdk.BUTTON_PRESS) - self.eb2.connect('button_press_event', self.cal_open, self.entry2) - img = gtk.Image() - img.set_from_stock('tryton-find', gtk.ICON_SIZE_BUTTON) - img.set_alignment(0.5, 0.5) - self.eb2.add(img) - self.widget.pack_start(self.eb2, expand=False, fill=False) - - self.widget.show_all() - self._changed(self.combo) - - tooltips.enable() - - def _changed(self, widget): - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if oper in ('=', '!='): - self.entry2.hide() - self.separator.hide() - self.eb2.hide() - else: - self.entry2.show() - self.separator.show() - self.eb2.show() - - def _date_get(self, value): - try: - date = datetime.date(*time.strptime(value, self.format)[:3]) - except ValueError: - return False - if self.attrs.get('type', 'date') == 'datetime': - return datetime_strftime(datetime.datetime.combine(date, - datetime.time.min), DHM_FORMAT) - return datetime_strftime(date, DT_FORMAT) - - def _value_get(self): - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if oper in ('=', '!='): - value = self._date_get(self.entry1.get_text()) - if value: - return [(self.name, oper, value)] - else: - return [] - else: - res = [] - if oper == 'between': - clause = 'AND' - oper1 = '>=' - oper2 = '<=' - else: - clause = 'OR' - oper1 = '<=' - oper2 = '>=' - res.append(clause) - val = self._date_get(self.entry1.get_text()) - if val: - res.append((self.name, oper1, val)) - val = self._date_get(self.entry2.get_text()) - if val: - res.append((self.name, oper2, val)) - return [res] - - def _value_set(self, value): - def conv(value): - if not value: - return '' - try: - return datetime_strftime(value, self.format) - except ValueError: - return '' - - i = self.liststore.get_iter_root() - while i: - if self.liststore.get_value(i, 0) == value[0]: - self.combo.set_active_iter(i) - break - i = self.liststore.iter_next(i) - - self.entry1.set_text(conv(value[1])) - if len(value) == 2: - self.entry2.clear() - else: - self.entry2.set_text(conv(value[2])) - - value = property(_value_get, _value_set) - - def cal_open(self, widget, event, dest): - parent = get_toplevel_window() - win = gtk.Dialog(_('Date selection'), parent, - gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_OK, gtk.RESPONSE_OK)) - win.set_has_separator(True) - win.set_icon(TRYTON_ICON) - - cal = gtk.Calendar() - cal.display_options(gtk.CALENDAR_SHOW_HEADING | \ - gtk.CALENDAR_SHOW_DAY_NAMES | \ - gtk.CALENDAR_SHOW_WEEK_NUMBERS) - cal.connect('day-selected-double-click', - lambda *x: win.response(gtk.RESPONSE_OK)) - win.vbox.pack_start(cal, expand=True, fill=True) - win.show_all() - - try: - val = self._date_get(dest.get_text()) - if val: - cal.select_month(int(val[5:7])-1, int(val[0:4])) - cal.select_day(int(val[8:10])) - except ValueError: - pass - - response = win.run() - if response == gtk.RESPONSE_OK: - year, month, day = cal.get_date() - date = datetime.date(year, month + 1, day) - dest.set_text(datetime_strftime(date, self.format)) - self.on_change() - win.destroy() - parent.present() - - def clear(self): - self.value = ('=', '') - - def sig_activate(self, fct): - self.entry1.connect_after('activate', fct) - self.entry2.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/char.py b/tryton/gui/window/view_form/widget_search/char.py deleted file mode 100644 index d653883d6b..0000000000 --- a/tryton/gui/window/view_form/widget_search/char.py +++ /dev/null @@ -1,82 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gtk -import gettext -from interface import Interface -import gobject - -_ = gettext.gettext - - -class Char(Interface): - - def __init__(self, name, attrs=None, context=None, on_change=None): - super(Char, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - - self.widget = gtk.HBox() - - self.liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self.combo = gtk.ComboBox(self.liststore) - cell = gtk.CellRendererText() - self.combo.pack_start(cell, True) - self.combo.add_attribute(cell, 'text', 1) - for oper in ( - ['ilike', _('contains')], - ['not ilike', _('does not contain')], - ['=', _('equals')], - ['!=', _('does not equal')], - ['starts', _('starts with')], - ['ends', _('ends with')], - ): - self.liststore.append(oper) - self.combo.set_active(0) - self.combo.connect('changed', self.on_change) - self.widget.pack_start(self.combo, False, False) - - self.entry = gtk.Entry() - self.entry.set_max_length(int(self.attrs.get('size', 0))) - self.entry.set_width_chars(5) - self.entry.set_property('activates_default', True) - self.entry.connect('key_press_event', self.on_change) - self.widget.pack_start(self.entry, True, True) - self.widget.show_all() - - def _value_get(self): - value = self.entry.get_text() - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if value or oper != 'ilike': - if oper == '=' and not value: - value = False - if oper in ('ilike', 'not ilike') \ - and '%' not in value: - return [(self.name, oper, '%' + value + '%')] - elif oper == 'starts': - return [(self.name, 'ilike', value + '%')] - elif oper == 'ends': - return [(self.name, 'ilike', '%' + value)] - return [(self.name, oper, value)] - else: - return [] - - def _value_set(self, value): - i = self.liststore.get_iter_root() - while i: - if self.liststore.get_value(i, 0) == value[0]: - self.combo.set_active_iter(i) - break - i = self.liststore.iter_next(i) - self.entry.set_text(value[1] or '') - - value = property(_value_get, _value_set) - - def clear(self): - self.value = ['ilike', ''] - - def _readonly_set(self, value): - self.combo.set_sensitive(not value) - self.entry.set_editable(not value) - self.entry.set_sensitive(not value) - - def sig_activate(self, fct): - self.entry.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/checkbox.py b/tryton/gui/window/view_form/widget_search/checkbox.py deleted file mode 100644 index 0306d36f10..0000000000 --- a/tryton/gui/window/view_form/widget_search/checkbox.py +++ /dev/null @@ -1,77 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gtk -import gettext -from interface import Interface - -_ = gettext.gettext - - -class CheckBox(Interface): - - def __init__(self, name, attrs=None, context=None, on_change=None): - super(CheckBox, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - - self.widget = gtk.combo_box_entry_new_text() - self.widget.child.set_editable(True) - self.widget.child.set_property('activates_default', True) - self.widget.child.connect('key_press_event', self.sig_key_press) - self.widget.child.connect('key_press_event', self.on_change) - self.widget.set_focus_chain([self.widget.child]) - - if self.name != 'active' or \ - not (self.name == 'active' \ - and self.context.get('active_test', True)): - self.widget.append_text('') - self.widget.child.set_text('') - else: - self.widget.child.set_text(_('Yes')) - self.widget.append_text(_('Yes')) - self.widget.append_text(_('No')) - - completion = gtk.EntryCompletion() - #Only available in PyGTK 2.6 and above. - if hasattr(completion, 'set_inline_selection'): - completion.set_inline_selection(True) - completion.set_model(self.widget.get_model()) - self.widget.child.set_completion(completion) - completion.set_text_column(0) - self.widget.show() - - def sig_key_press(self, widget, event): - if event.type == gtk.gdk.KEY_PRESS \ - and event.state & gtk.gdk.CONTROL_MASK \ - and event.keyval == gtk.keysyms.space: - self.widget.popup() - - def _value_get(self): - val = self.widget.child.get_text() - if not val \ - and self.name == 'active' \ - and self.context.get('active_test', True): - val = _('Yes') - self.widget.child.set_text(val) - if val: - return [(self.name, '=', int(val == _('Yes')))] - self.widget.child.set_text('') - return [] - - def _value_set(self, value): - if value == '': - if self.name != 'active' or \ - (self.name == 'active' \ - and self.context.get('active_test', True)): - self.widget.child.set_text('') - else: - self.widget.child.set_text(_('Yes')) - return - if value: - self.widget.child.set_text(_('Yes')) - else: - self.widget.child.set_text(_('No')) - - value = property(_value_get, _value_set) - - def sig_activate(self, fct): - self.widget.child.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/float.py b/tryton/gui/window/view_form/widget_search/float.py deleted file mode 100644 index ad074338c9..0000000000 --- a/tryton/gui/window/view_form/widget_search/float.py +++ /dev/null @@ -1,88 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import locale -from decimal import Decimal -from integer import Integer -import gettext - -_ = gettext.gettext - - -class Float(Integer): - - def __init__(self, name, attrs=None, context=None, on_change=None): - super(Float, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - if isinstance(self.attrs.get('digits'), basestring): - self.digits = (16, 2) - else: - self.digits = self.attrs.get('digits', (16, 2)) - - def _value_get(self): - try: - value1 = locale.atof(self.entry1.get_text()) - except ValueError: - value1 = False - try: - value2 = locale.atof(self.entry2.get_text()) - except ValueError: - value2 = False - return self._get_clause(value1, value2) - - def _value_set(self, value): - def conv(value): - if value == False: - return '' - else: - return locale.format('%.' + str(self.digits[1]) + 'f', - value or 0.0, True) - - i = self.liststore.get_iter_root() - while i: - if self.liststore.get_value(i, 0) == value[0]: - self.combo.set_active_iter(i) - break - i = self.liststore.iter_next(i) - - self.entry1.set_text(conv(value[1])) - if len(value) == 2: - self.entry2.set_text('') - else: - self.entry2.set_text(conv(value[2])) - - value = property(_value_get, _value_set) - - def clear(self): - self.value = ('=', False, False) - - def sig_insert_text(self, widget, new_text, new_text_length, position): - value = widget.get_text() - position = widget.get_position() - new_value = value[:position] + new_text + value[position:] - if new_value == '-': - return - try: - if len(str(int(locale.atof(new_value)))) > self.digits[0]: - widget.stop_emission('insert-text') - except ValueError: - widget.stop_emission('insert-text') - - -class Numeric(Float): - - def _value_get(self): - try: - value1 = Decimal(str(locale.atof(self.entry1.get_text()))) - except ValueError: - value1 = False - try: - value2 = Decimal(str(locale.atof(self.entry2.get_text()))) - except ValueError: - value2 = False - res = self._get_clause(value1, value2) - return res - - def _value_set(self, value): - super(Numeric, self)._value_set(value) - - value = property(_value_get, _value_set) diff --git a/tryton/gui/window/view_form/widget_search/form.py b/tryton/gui/window/view_form/widget_search/form.py deleted file mode 100644 index fca1902f45..0000000000 --- a/tryton/gui/window/view_form/widget_search/form.py +++ /dev/null @@ -1,307 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gtk -from xml.parsers import expat -import sys -import gettext -from tryton.config import CONFIG - -_ = gettext.gettext - - -class _container(object): - - def __init__(self): - self.cont = [] - self.count = 0 - self.col = 0 - - def new(self, col=6): - self.col = col+1 - table = gtk.Table(1, col) - table.show() - table.set_homogeneous(False) - table.set_col_spacings(3) - table.set_row_spacings(0) - table.set_border_width(1) - self.cont.append( (table, 1, 0) ) - - def get(self): - return self.cont[-1][0] - - def pop(self): - return self.cont.pop()[0] - - def newline(self): - (table, i, j) = self.cont[-1] - if i > 0: - self.cont[-1] = (table, 1, j+1) - table.resize(j + 1, self.col) - - def wid_add(self, widget, length=1, name=None, expand=False, ypadding=0): - self.count += 1 - (table, i, j) = self.cont[-1] - if length > self.col: - length = self.col - if length + i > self.col: - self.newline() - (table, i, j) = self.cont[-1] - if name: - vbox = gtk.VBox(homogeneous=False, spacing=1) - vbox.show() - label = gtk.Label(name) - label.show() - label.set_alignment(0.0, 0.5) - vbox.pack_start(label, expand=False) - vbox.pack_start(widget, expand=expand, fill=True) - wid = vbox - else: - wid = widget - yopt = False - if expand: - yopt = yopt | gtk.EXPAND |gtk.FILL - table.attach(wid, i, i+length, j, j+1, - yoptions=yopt, xoptions=gtk.FILL|gtk.EXPAND, - ypadding=ypadding, xpadding=5) - self.cont[-1] = (table, i+length, j) - return wid - - -class Parse(object): - - def __init__(self, fields, model='', context=None, on_change=None): - self.fields = fields - self.model = model - self.col = CONFIG['client.modepda'] and 1 or 6 - self.focusable = None - self.add_widget_end = [] - self.container = None - self.spin_limit = gtk.SpinButton(climb_rate=1, digits=0) - self.spin_limit.show() - self.spin_offset = gtk.SpinButton(climb_rate=1, digits=0) - self.spin_offset.show() - self.title = 'Form' - self.notebooks = [] - self.dict_widget = {} - self.context = context or {} - self.on_change = on_change - - def _psr_start(self, name, attrs): - - if name in ('form','tree'): - self.title = attrs.get('string', self.title) - self.container.new(self.col) - elif name == 'field': - if attrs['name'] in self.fields: - val = attrs.get('select', None) - if val is None: - val = self.fields[attrs['name']].get('select', 0) - val = int(val) - if val: - if val <= 1: - self.add_widget(attrs, val) - else: - self.add_widget_end.append((attrs, val)) - - def add_widget(self, attrs, val): - ftype = attrs.get('widget', self.fields[str(attrs['name'])]['type']) - self.fields[str(attrs['name'])].update(attrs) - self.fields[str(attrs['name'])]['model']=self.model - if ftype not in WIDGETS_TYPE: - return False - widget_act = WIDGETS_TYPE[ftype][0](str(attrs['name']), - attrs=self.fields[attrs['name']], context=self.context, - on_change=self.on_change) - if 'string' in self.fields[str(attrs['name'])]: - label = self.fields[str(attrs['name'])]['string'] + _(':') - else: - label = None - size = WIDGETS_TYPE[ftype][1] - if not self.focusable: - self.focusable = widget_act.widget - wid = self.container.wid_add(widget_act.widget, size, label, - int(self.fields[str(attrs['name'])].get('expand',0))) - if int(val) <= 1: - wid.show() - self.dict_widget[str(attrs['name'])] = (widget_act, wid, int(val)) - - def add_parameters(self): - hb_param = gtk.HBox(spacing=3) - hb_param.pack_start(gtk.Label(_('Limit:')), expand=False, fill=False) - - self.spin_limit.set_numeric(False) - self.spin_limit.set_adjustment(gtk.Adjustment(value=CONFIG['client.limit'], - lower=1, upper=sys.maxint, step_incr=10, page_incr=100)) - self.spin_limit.set_property('visible', True) - - hb_param.pack_start(self.spin_limit, expand=False, fill=False) - - hb_param.pack_start(gtk.Label(_('Offset:')), expand=False, fill=False) - - self.spin_offset.set_numeric(False) - self.spin_offset.set_adjustment(gtk.Adjustment(value=0, lower=0, - upper=sys.maxint, step_incr=CONFIG['client.limit'], page_incr=100)) - - hb_param.pack_start(self.spin_offset, expand=False, fill=False) - - hb_param.show_all() - return hb_param - - def _psr_end(self, name): - pass - - def _psr_char(self, name): - pass - - def parse(self, xml_data): - psr = expat.ParserCreate() - psr.StartElementHandler = self._psr_start - psr.EndElementHandler = self._psr_end - psr.CharacterDataHandler = self._psr_char - self.container = _container() - - psr.Parse(xml_data) - - vbox = gtk.VBox() - vbox.show() - vbox.pack_start(self.container.pop()) - - expander = gtk.Expander(_('Advanced Search')) - expander.show() - vbox2 = gtk.VBox() - vbox2.show() - - self.container = _container() - self.container.new(self.col) - for i in self.add_widget_end: - self.add_widget(*i) - if ('active' in self.fields) and ('active' not in self.dict_widget): - val = self.fields['active'].get('select', 0) - val = int(val) - self.add_widget(self.fields['active'], val) - - self.add_widget_end = [] - - vbox2.pack_start(self.container.pop()) - - vbox2.pack_start(self.add_parameters()) - expander.add(vbox2) - vbox.pack_start(expander) - - return (self.dict_widget, vbox) - - -class Form(object): - - def __init__(self, view, model=None, domain=None, call=None, context=None): - if domain is None: - domain = [] - if context is None: - context = {} - parser = Parse(view['fields'], model=model, context=context, - on_change=self.on_change) - self.fields = view['fields'] - self.model = model - self.parser = parser - self.call = call - self.context = context - (self.widgets, self.widget) = parser.parse(view['arch']) - self.widget.show() - self.spin_limit = parser.spin_limit - self.spin_limit.connect('value-changed', self.limit_changed) - self.spin_limit.set_activates_default(True) - self.spin_offset = parser.spin_offset - self.spin_offset.set_activates_default(True) - self.focusable = parser.focusable - self.id = 0 - self.name = parser.title - for i in self.widgets.itervalues(): - i[0].sig_activate(self.sig_activate) - self.spin_limit.connect_after('activate', self.sig_activate) - self.spin_offset.connect_after('activate', self.sig_activate) - - def clear(self): - self.id = 0 - for i in self.widgets.itervalues(): - i[0].clear() - self.spin_offset.set_value(0) - - def next(self): - self.spin_offset.set_value(self.spin_offset.get_value() - + self.spin_limit.get_value()) - - def prev(self): - self.spin_offset.set_value(max(self.spin_offset.get_value() - - self.spin_limit.get_value(), 0)) - - def on_change(self, *args): - self.spin_offset.set_value(0) - - def limit_changed(self, widget): - self.spin_offset.set_increments(step=self.spin_limit.get_value(), - page=100) - - def set_limit(self, value): - if not value: - value = CONFIG['client.limit'] - return self.spin_limit.set_value(value) - - def get_limit(self): - return self.spin_limit.get_value() - - def get_offset(self): - return self.spin_offset.get_value() - - def sig_activate(self, *args): - if self.call: - obj, fct = self.call - fct(obj) - - def _value_get(self): - res = [] - for i in self.widgets: - res += self.widgets[i][0].value - return res - - def _value_set(self, value): - if not value: - return - for i in value: - if i in self.widgets: - self.widgets[i][0].value = value[i] - - value = property(_value_get, _value_set) - -import calendar -import float -import integer -import selection -import char -import checkbox -import reference - -WIDGETS_TYPE = { - 'date': (calendar.Calendar, 2), - 'datetime': (calendar.Calendar, 2), - 'float': (float.Float, 2), - 'numeric': (float.Numeric, 2), - 'integer': (integer.Integer, 2), - 'biginteger': (integer.Integer, 2), - 'selection': (selection.Selection, 2), - 'many2one_selection': (selection.Selection, 2), - 'char': (char.Char, 2), - 'boolean': (checkbox.CheckBox, 2), - 'reference': (reference.Reference, 2), - 'text': (char.Char, 2), - 'email': (char.Char, 2), - 'url': (char.Char, 2), - 'many2one': (char.Char, 2), - 'one2many': (char.Char, 2), - 'one2many_form': (char.Char, 2), - 'one2many_list': (char.Char, 2), - 'many2many_edit': (char.Char, 2), - 'many2many': (char.Char, 2), - 'callto': (char.Char, 2), - 'sip': (char.Char, 2), - 'one2one': (char.Char, 2), -} diff --git a/tryton/gui/window/view_form/widget_search/integer.py b/tryton/gui/window/view_form/widget_search/integer.py deleted file mode 100644 index 082cf02450..0000000000 --- a/tryton/gui/window/view_form/widget_search/integer.py +++ /dev/null @@ -1,139 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gtk -from interface import Interface -import locale -import gettext -import gobject - -_ = gettext.gettext - - -class Integer(Interface): - - def __init__(self, name, attrs=None, context=None, on_change=None): - super(Integer, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - self.widget = gtk.HBox(spacing=3) - - self.liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self.combo = gtk.ComboBox(self.liststore) - cell = gtk.CellRendererText() - self.combo.pack_start(cell, True) - self.combo.add_attribute(cell, 'text', 1) - for oper in (['=', _('equals')], - ['between', _('is between')], - ['not between', _('is not between')], - ['!=', _('is different')], - ): - self.liststore.append(oper) - self.combo.set_active(0) - self.widget.pack_start(self.combo, False, False) - self.combo.connect('changed', self._changed) - self.combo.connect('changed', self.on_change) - - self.entry1 = gtk.Entry() - self.entry1.set_max_length(0) - self.entry1.set_width_chars(5) - self.entry1.set_activates_default(True) - self.entry1.set_alignment(1.0) - self.entry1.connect('insert_text', self.sig_insert_text) - self.entry1.connect('key_press_event', self.on_change) - self.widget.pack_start(self.entry1, expand=True, fill=True) - self.separator = gtk.Label('-') - self.widget.pack_start(self.separator, expand=False, fill=False) - self.entry2 = gtk.Entry() - self.entry2.set_max_length(0) - self.entry2.set_width_chars(5) - self.entry2.set_activates_default(True) - self.entry2.set_alignment(1.0) - self.entry2.connect('insert_text', self.sig_insert_text) - self.entry2.connect('key_press_event', self.on_change) - self.widget.pack_start(self.entry2, expand=True, fill=True) - - self.widget.show_all() - self._changed(self.combo) - - def _changed(self, widget): - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if oper in ('=', '!='): - self.entry2.hide() - self.separator.hide() - else: - self.entry2.show() - self.separator.show() - - def _value_get(self): - try: - value1 = locale.atoi(self.entry1.get_text()) - except ValueError: - value1 = False - try: - value2 = locale.atoi(self.entry2.get_text()) - except ValueError: - value2 = False - return self._get_clause(value1, value2) - - def _get_clause(self, value1, value2): - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if oper in ('=', '!='): - if self.entry1.get_text(): - return [(self.name, oper, value1 or 0)] - else: - return [] - else: - res = [] - if oper == 'between': - clause = 'AND' - oper1 = '>=' - oper2 = '<=' - else: - clause = 'OR' - oper1 = '<=' - oper2 = '>=' - res.append(clause) - if value1 is not False: - res.append((self.name, oper1, value1)) - if value2 is not False: - res.append((self.name, oper2, value2)) - return [res] - - def _value_set(self, value): - def conv(value): - if value == False: - return '' - else: - return locale.format('%d', value or 0, True) - - i = self.liststore.get_iter_root() - while i: - if self.liststore.get_value(i, 0) == value[0]: - self.combo.set_active_iter(i) - break - i = self.liststore.iter_next(i) - - self.entry1.set_text(conv(value[1])) - if len(value) == 2: - self.entry2.set_text('') - else: - self.entry2.set_text(conv(value[2])) - - value = property(_value_get, _value_set) - - def clear(self): - self.value = ('=', False, False) - - def sig_activate(self, fct): - self.entry1.connect_after('activate', fct) - self.entry2.connect_after('activate', fct) - - def sig_insert_text(self, widget, new_text, new_text_length, position): - value = widget.get_text() - position = widget.get_position() - new_value = value[:position] + new_text + value[position:] - if new_value == '-': - return - try: - locale.atoi(new_value) - except ValueError: - widget.stop_emission('insert-text') diff --git a/tryton/gui/window/view_form/widget_search/interface.py b/tryton/gui/window/view_form/widget_search/interface.py deleted file mode 100644 index 25e6c97a69..0000000000 --- a/tryton/gui/window/view_form/widget_search/interface.py +++ /dev/null @@ -1,36 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gettext - -_ = gettext.gettext - - -class Interface(object): - "Interface for search widget" - - def __init__(self, name, attrs=None, context=None, on_change=None): - if attrs is None: - attrs = {} - self._value = None - self.name = name - self.model = attrs.get('model', None) - self.attrs = attrs or {} - self.context = context or {} - self.on_change = on_change - - def clear(self): - self.value = '' - - def _value_get(self): - return self._value - - def _value_set(self, value): - self._value = value - - value = property(_value_get, _value_set) - - def _readonly_set(self, value): - pass - - def sig_activate(self, fct): - pass diff --git a/tryton/gui/window/view_form/widget_search/reference.py b/tryton/gui/window/view_form/widget_search/reference.py deleted file mode 100644 index 907637c15e..0000000000 --- a/tryton/gui/window/view_form/widget_search/reference.py +++ /dev/null @@ -1,130 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gtk -import gettext -from interface import Interface -import tryton.rpc as rpc -import tryton.common as common -import gobject -from tryton.exceptions import TrytonServerError - -_ = gettext.gettext - - -class Reference(Interface): - - def __init__(self, name, attrs=None, context=None, on_change=None): - super(Reference, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - - self.widget = gtk.HBox() - - self.liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self.combo = gtk.ComboBox(self.liststore) - cell = gtk.CellRendererText() - self.combo.pack_start(cell, True) - self.combo.add_attribute(cell, 'text', 1) - for oper in (['like', _('equals')], - ['not like', _('is different')], - ): - self.liststore.append(oper) - self.combo.set_active(0) - self.combo.connect('changed', self.on_change) - self.widget.pack_start(self.combo, False, False) - - self.entry = gtk.combo_box_entry_new_text() - self.entry.child.set_editable(True) - self.entry.child.set_property('activates_default', True) - self.entry.child.connect('key_press_event', self.sig_key_press) - self.entry.child.connect('key_press_event', self.on_change) - self.entry.set_focus_chain([self.entry.child]) - self._selection = {} - selection = self.attrs.get('selection', []) - if 'relation' in self.attrs: - try: - result = rpc.execute('model', - self.attrs['relation'], 'search_read', - self.attrs.get('domain', []), - 0, None, None, ['rec_name'], rpc.CONTEXT) - selection = [(x['id'], x['rec_name']) for x in result] - except TrytonServerError, exception: - common.process_exception(exception) - selection = [] - else: - if not isinstance(selection, (list, tuple)): - try: - selection = rpc.execute('model', - self.attrs['model'], selection, rpc.CONTEXT) - except TrytonServerError, exception: - common.process_exception(exception) - selection = [] - selection.sort(lambda x, y: cmp(x[1], y[1])) - self.attrs['selection'] = selection - self.set_popdown(selection) - self.widget.pack_start(self.entry, True, True) - self.widget.show_all() - - def set_popdown(self, selection): - model = self.entry.get_model() - model.clear() - lst = [] - for (i, j) in selection: - name = str(j) - if type(i) == type(1): - name += ' (' + str(i) + ')' - lst.append(name) - self._selection[name] = i - self.entry.append_text('') - for name in lst: - self.entry.append_text(name) - completion = gtk.EntryCompletion() - #Only available in PyGTK 2.6 and above. - if hasattr(completion, 'set_inline_selection'): - completion.set_inline_selection(True) - completion.set_model(model) - self.entry.child.set_completion(completion) - completion.set_text_column(0) - return lst - - def sig_key_press(self, widget, event): - if event.type == gtk.gdk.KEY_PRESS \ - and event.state & gtk.gdk.CONTROL_MASK \ - and event.keyval == gtk.keysyms.space: - self.entry.popup() - - def _value_get(self): - value = self._selection.get(self.entry.child.get_text(), False) - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if value or oper != 'like': - return [(self.name, oper, value + ',%')] - else: - self.entry.child.set_text('') - return [] - - def _value_set(self, value): - i = self.liststore.get_iter_root() - while i: - if self.liststore.get_value(i, 0) == value[0]: - self.combo.set_active_iter(i) - break - i = self.liststore.iter_next(i) - if value[1] == False: - self.entry.child.set_text('') - return - for long_text, sel_value in self._selection.items(): - if sel_value == value[1]: - self.entry.child.set_text(long_text) - break - - value = property(_value_get, _value_set, None, - 'The content of the widget or ValueError if not valid') - - def clear(self): - self.value = ['like', False] - - def _readonly_set(self, value): - self.combo.set_sensitive(not value) - self.entry.set_sensitive(not value) - - def sig_activate(self, fct): - self.entry.child.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/selection.py b/tryton/gui/window/view_form/widget_search/selection.py deleted file mode 100644 index 9399e9fdc0..0000000000 --- a/tryton/gui/window/view_form/widget_search/selection.py +++ /dev/null @@ -1,135 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -import gtk -import operator -from interface import Interface -import tryton.rpc as rpc -import tryton.common as common -import gobject -import gettext -from tryton.pyson import PYSONDecoder -from tryton.exceptions import TrytonServerError - -_ = gettext.gettext - -class Selection(Interface): - - def __init__(self, name, attrs=None, context=None, on_change=None): - super(Selection, self).__init__(name, attrs=attrs, context=context, - on_change=on_change) - - self.widget = gtk.HBox() - - self.liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self.combo = gtk.ComboBox(self.liststore) - cell = gtk.CellRendererText() - self.combo.pack_start(cell, True) - self.combo.add_attribute(cell, 'text', 1) - for oper in (['=', _('equals')], - ['!=', _('is different')], - ): - self.liststore.append(oper) - self.combo.set_active(0) - self.combo.connect('changed', self.on_change) - self.widget.pack_start(self.combo, False, False) - - self.entry = gtk.combo_box_entry_new_text() - self.entry.child.set_editable(True) - self.entry.child.set_property('activates_default', True) - self.entry.child.connect('key_press_event', self.sig_key_press) - self.entry.child.connect('key_press_event', self.on_change) - self.entry.set_focus_chain([self.entry.child]) - self._selection = {} - selection = self.attrs.get('selection', []) - if 'relation' in self.attrs: - if not self.attrs.get('domain'): - domain = [] - else: - domain = PYSONDecoder(rpc.CONTEXT).decode(self.attrs['domain']) - try: - result = rpc.execute('model', self.attrs['relation'], - 'search_read', domain, 0, None, None, - ['rec_name'], rpc.CONTEXT) - selection = [(x['id'], x['rec_name']) for x in result] - except TrytonServerError, exception: - common.process_exception(exception) - selection = [] - else: - if not isinstance(selection, (list, tuple)): - try: - selection = rpc.execute('model', - self.attrs['model'], selection, rpc.CONTEXT) - except TrytonServerError, exception: - common.process_exception(exception) - selection = [] - selection.sort(key=operator.itemgetter(1)) - self.attrs['selection'] = selection - self.set_popdown(selection) - self.widget.pack_start(self.entry, True, True) - self.widget.show_all() - - def set_popdown(self, selection): - model = self.entry.get_model() - model.clear() - self._selection = {} - lst = [] - for (i, j) in selection: - name = str(j) - if type(i) == type(1): - name += ' (' + str(i) + ')' - lst.append(name) - self._selection[name] = i - self.entry.append_text('') - for name in lst: - self.entry.append_text(name) - completion = gtk.EntryCompletion() - #Only available in PyGTK 2.6 and above. - if hasattr(completion, 'set_inline_selection'): - completion.set_inline_selection(True) - completion.set_model(model) - self.entry.child.set_completion(completion) - completion.set_text_column(0) - return lst - - def sig_key_press(self, widget, event): - if event.type == gtk.gdk.KEY_PRESS \ - and event.state & gtk.gdk.CONTROL_MASK \ - and event.keyval == gtk.keysyms.space: - self.entry.popup() - - def _value_get(self): - value = self._selection.get(self.entry.child.get_text(), False) - oper = self.liststore.get_value(self.combo.get_active_iter(), 0) - if value or oper != '=': - return [(self.name, oper, value)] - else: - self.entry.child.set_text('') - return [] - - def _value_set(self, value): - i = self.liststore.get_iter_root() - while i: - if self.liststore.get_value(i, 0) == value[0]: - self.combo.set_active_iter(i) - break - i = self.liststore.iter_next(i) - if value[1] == False: - self.entry.child.set_text('') - return - for long_text, sel_value in self._selection.items(): - if sel_value == value[1]: - self.entry.child.set_text(long_text) - break - - def clear(self): - self.value = ['=', False] - - value = property(_value_get, _value_set, None, - 'The content of the widget or ValueError if not valid') - - def _readonly_set(self, value): - self.combo.set_sensitive(not value) - self.entry.set_sensitive(not value) - - def sig_activate(self, fct): - self.entry.child.connect_after('activate', fct) diff --git a/tryton/gui/window/win_search.py b/tryton/gui/window/win_search.py index 68953e7ccf..6fa18a3a37 100644 --- a/tryton/gui/window/win_search.py +++ b/tryton/gui/window/win_search.py @@ -4,7 +4,6 @@ import gtk import gettext import tryton.rpc as rpc from tryton.gui.window.view_form.screen import Screen -import tryton.gui.window.view_form.widget_search as widget_search from tryton.config import TRYTON_ICON from tryton.gui.window.win_form import WinForm from tryton.gui.window.nomodal import NoModal