Add MultiSelection field
issue7851 review249911002
This commit is contained in:
1 parent
57e0526c00
commit
91f63609e8
7 files changed
+156
-17
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Support multiselection field
|
||||
* Support dot notation on PYSON Eval
|
||||
* Order Dict keys by sequence
|
||||
* Support relation fields in domain parser
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user