Add MultiSelection field

issue7851
review249911002
This commit is contained in:
Cédric Krier committed 2019-09-20 11:32:56 +02:00
1 parent 57e0526c00
commit 91f63609e8
7 files changed
+156 -17

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Support multiselection field
* Support dot notation on PYSON Eval
* Order Dict keys by sequence
* Support relation fields in domain parser
+103 -1
View File
@@ -177,6 +177,8 @@ def default_operator(field):
if field['type'] in ('char', 'text', 'many2one', 'many2many', 'one2many',
'reference'):
return 'ilike'
elif field['type'] == 'multiselection':
return 'in'
else:
return '='
@@ -307,6 +309,7 @@ def convert_value(field, value, context=None):
'integer': convert_integer,
'numeric': convert_numeric,
'selection': convert_selection,
'multiselection': convert_selection,
'reference': convert_selection,
'datetime': convert_datetime,
'date': convert_date,
@@ -563,6 +566,7 @@ def format_value(field, value, target=None, context=None):
'float': format_float,
'numeric': format_float,
'selection': format_selection,
'multiselection': format_selection,
'reference': format_reference,
'datetime': format_datetime,
'date': format_date,
@@ -786,6 +790,7 @@ def complete_value(field, value):
completes = {
'boolean': complete_boolean,
'selection': complete_selection,
'multiselection': complete_selection,
'reference': complete_reference,
'datetime': complete_datetime,
'date': complete_date,
@@ -1023,6 +1028,8 @@ class DomainParser(object):
return all(isinstance(v, types) for v in value)
else:
return isinstance(value, types)
elif field['type'] == 'multiselection':
return not value or isinstance(value, list)
else:
return True
elif name == 'rec_name':
@@ -1270,16 +1277,25 @@ class DomainParser(object):
target, value = split_target_value(field, value)
if target:
field_name += '.rec_name'
elif field['type'] == 'multiselection':
if value is not None and not isinstance(value, list):
value = [value]
if not operator:
operator = default_operator(field)
if isinstance(value, list):
if (isinstance(value, list)
and field['type'] != 'multiselection'):
if operator == '!':
operator = 'not in'
else:
operator = 'in'
if operator == '!':
operator = negate_operator(default_operator(field))
if value is None and operator.endswith('in'):
if operator.startswith('not'):
operator = '!='
else:
operator = '='
if field['type'] in ('integer', 'float', 'numeric',
'datetime', 'date', 'time'):
if value and '..' in value:
@@ -1313,6 +1329,15 @@ def test_stringable():
'string': 'Name',
'type': 'char',
},
'multiselection': {
'string': "MultiSelection",
'type': 'multiselection',
'selection': [
('foo', "Foo"),
('bar', "Bar"),
('baz', "Baz"),
],
},
'relation': {
'string': 'Relation',
'type': 'many2one',
@@ -1336,6 +1361,10 @@ def test_stringable():
assert not dom.stringable(['AND', valid, invalid])
assert dom.stringable([[valid]])
assert not dom.stringable([[valid], [invalid]])
assert dom.stringable([('multiselection', '=', None)])
assert dom.stringable([('multiselection', '=', '')])
assert not dom.stringable([('multiselection', '=', 'foo')])
assert dom.stringable([('multiselection', '=', ['foo'])])
assert dom.stringable([('relation', '=', None)])
assert dom.stringable([('relation', '=', "Foo")])
assert dom.stringable([('relation.rec_name', '=', "Foo")])
@@ -1369,6 +1398,15 @@ def test_string():
('', ''),
],
},
'multiselection': {
'string': "MultiSelection",
'type': 'multiselection',
'selection': [
('foo', "Foo"),
('bar', "Bar"),
('baz', "Baz"),
],
},
'reference': {
'string': 'Reference',
'type': 'reference',
@@ -1426,6 +1464,29 @@ def test_string():
assert dom.string([('selection', '!=', '')]) == 'Selection: !""'
assert dom.string([('selection', '=', 'male')]) == 'Selection: Male'
assert dom.string([('selection', '!=', 'male')]) == 'Selection: !Male'
assert dom.string([('multiselection', '=', None)]) == "MultiSelection: ="
assert dom.string([('multiselection', '=', '')]) == "MultiSelection: ="
assert dom.string([('multiselection', '!=', '')]) == "MultiSelection: !="
assert dom.string([('multiselection', '=', ['foo'])]) == \
"MultiSelection: =Foo"
assert dom.string([('multiselection', '!=', ['foo'])]) == \
"MultiSelection: !=Foo"
assert dom.string([('multiselection', '=', ['foo', 'bar'])]) == \
"MultiSelection: =Foo;Bar"
assert dom.string([('multiselection', '!=', ['foo', 'bar'])]) == \
"MultiSelection: !=Foo;Bar"
assert dom.string([('multiselection', 'in', ['foo'])]) == \
"MultiSelection: Foo"
assert dom.string([('multiselection', 'not in', ['foo'])]) == \
"MultiSelection: !Foo"
assert dom.string([('multiselection', '=', ['foo', 'bar'])]) == \
"MultiSelection: =Foo;Bar"
assert dom.string([('multiselection', '!=', ['foo', 'bar'])]) == \
"MultiSelection: !=Foo;Bar"
assert dom.string([('multiselection', 'in', ['foo', 'bar'])]) == \
"MultiSelection: Foo;Bar"
assert dom.string([('multiselection', 'not in', ['foo', 'bar'])]) == \
"MultiSelection: !Foo;Bar"
assert dom.string([('reference', 'ilike', '%foo%')]) == \
'Reference: foo'
assert dom.string([('reference.rec_name', 'ilike', '%bar%', 'spam')]) == \
@@ -1565,6 +1626,16 @@ def test_parse_clause():
('female', 'Female'),
],
},
'multiselection': {
'string': "MultiSelection",
'name': 'multiselection',
'type': 'multiselection',
'selection': [
('foo', "Foo"),
('bar', "Bar"),
('baz', "Baz"),
],
},
'reference': {
'string': 'Reference',
'name': 'reference',
@@ -1622,6 +1693,37 @@ def test_parse_clause():
== [
('selection', 'in', ['male', 'female'])
]
assert rlist(dom.parse_clause([('MultiSelection', None, None)])) == [
('multiselection', '=', None),
]
assert rlist(dom.parse_clause([('MultiSelection', None, '')])) == [
('multiselection', 'in', ['']),
]
assert rlist(dom.parse_clause([('MultiSelection', '=', '')])) == [
('multiselection', '=', ['']),
]
assert rlist(dom.parse_clause([('MultiSelection', '!', '')])) == [
('multiselection', 'not in', ['']),
]
assert rlist(dom.parse_clause([('MultiSelection', '!=', '')])) == [
('multiselection', '!=', ['']),
]
assert rlist(dom.parse_clause(
[('MultiSelection', None, ['Foo', 'Bar'])])) == [
('multiselection', 'in', ['foo', 'bar']),
]
assert rlist(dom.parse_clause(
[('MultiSelection', '=', ['Foo', 'Bar'])])) == [
('multiselection', '=', ['foo', 'bar']),
]
assert rlist(dom.parse_clause(
[('MultiSelection', '!', ['Foo', 'Bar'])])) == [
('multiselection', 'not in', ['foo', 'bar']),
]
assert rlist(dom.parse_clause(
[('MultiSelection', '!=', ['Foo', 'Bar'])])) == [
('multiselection', '!=', ['foo', 'bar']),
]
assert rlist(dom.parse_clause([('Integer', None, None)])) == [
('integer', '=', None),
]
+14 -2
View File
@@ -208,8 +208,19 @@ class SelectionField(Field):
_default = None
def get_client(self, record):
return record.value.get(self.name)
class MultiSelectionField(Field):
_default = None
def get(self, record):
return super().get(record) or self._default
def get_eval(self, record):
value = super().get_eval(record)
if value is None:
value = []
return value
class DateTimeField(Field):
@@ -1078,6 +1089,7 @@ TYPES = {
'one2many': O2MField,
'reference': ReferenceField,
'selection': SelectionField,
'multiselection': MultiSelectionField,
'boolean': BooleanField,
'datetime': DateTimeField,
'date': DateField,
@@ -23,7 +23,7 @@ class MultiSelection(Widget, SelectionMixin):
self.widget.set_size_request(100, 100)
self.widget.get_accessible().set_name(attrs.get('string', ''))
self.model = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING)
self.model = Gtk.ListStore(GObject.TYPE_PYOBJECT, GObject.TYPE_STRING)
self.tree = self.mnemonic_widget = TreeViewControl()
self.tree.set_model(self.model)
self.tree.set_search_column(1)
@@ -41,7 +41,6 @@ class MultiSelection(Widget, SelectionMixin):
self.nullable_widget = False
self.init_selection()
self.id2path = {}
def _readonly_set(self, readonly):
super(MultiSelection, self)._readonly_set(readonly)
@@ -51,7 +50,7 @@ class MultiSelection(Widget, SelectionMixin):
@property
def modified(self):
if self.record and self.field:
group = set(r.id for r in self.field.get_client(self.record))
group = set(self.field.get_eval(self.record))
value = set(self.get_value())
return value != group
return False
@@ -81,17 +80,14 @@ class MultiSelection(Widget, SelectionMixin):
self.model.clear()
if not self.field:
return
id2path = {}
value2path = {}
for idx, (value, name) in enumerate(self.selection):
self.model.append((value, name))
id2path[value] = idx
value2path[value] = idx
selection.unselect_all()
group = self.field.get_client(self.record)
for element in group:
if (element not in group.record_removed
and element not in group.record_deleted
and element.id in id2path):
selection.select_path(id2path[element.id])
values = self.field.get_eval(self.record)
for value in values:
selection.select_path(value2path[value])
super(MultiSelection, self).display()
finally:
selection.handler_unblock_by_func(self.changed)
+3 -2
View File
@@ -20,8 +20,8 @@ import tryton.common as common
from . import View, XMLViewParser
from .list_gtk.editabletree import EditableTreeView, TreeView
from .list_gtk.widget import (Affix, Char, Text, Int, Boolean, URL, Date,
Time, Float, TimeDelta, Binary, M2O, O2O, O2M, M2M, Selection, Reference,
Dict, ProgressBar, Button, Image)
Time, Float, TimeDelta, Binary, M2O, O2O, O2M, M2M, Selection,
MultiSelection, Reference, Dict, ProgressBar, Button, Image)
_ = gettext.gettext
@@ -271,6 +271,7 @@ class TreeXMLViewParser(XMLViewParser):
'progressbar': ProgressBar,
'reference': Reference,
'selection': Selection,
'multiselection': MultiSelection,
'sip': URL,
'text': Text,
'time': Time,
@@ -985,6 +985,33 @@ class Selection(GenericText, SelectionMixin, PopdownMixin):
return False
class MultiSelection(GenericText, SelectionMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.init_selection()
@realized
@CellCache.cache
def setter(self, column, cell, store, iter_, user_data=None):
super().setter(column, cell, store, iter_, user_data=user_data)
cell.set_property('editable', False)
def value_from_text(self, record, text, callback=None):
if callback:
callback()
def get_textual_value(self, record):
field = record[self.attrs['name']]
self.update_selection(record, field)
selection = dict(self.selection)
values = []
for value in field.get_eval(record):
text = selection.get(value, '')
values.append(text)
return ';'.join(values)
class Reference(M2O):
def __init__(self, view, attrs, renderer=None):
@@ -604,7 +604,7 @@ class ScreenContainer(object):
selections = (_('True'), _('False'))
for selection in selections:
entry.append_text(selection)
elif field['type'] == 'selection':
elif field['type'] in ['selection', 'multiselection']:
selections = tuple(x[1] for x in field['selection'])
entry = Selection(selections)
entry.set_vexpand(True)