Get directly the string value of Selection fields in non editable views

Closes #10331
This commit is contained in:
Nicolas Évrard
2023-01-29 01:12:24 +01:00
parent 51d2a1ad1a
commit 1fdfe31b1e
14 changed files with 250 additions and 66 deletions
+1
View File
@@ -1,3 +1,4 @@
* Eagerly read string value of selection fields
* Do not select first record by default
* Use ``&`` and ``|`` as boolean operator in search widget
* Use toggle button and label on translate dialog
+13 -3
View File
@@ -671,6 +671,10 @@
var fdescription = this.model.fields[fname].description;
if (~rec_named_fields.indexOf(fdescription.type))
fnames_to_fetch.push(fname + '.rec_name');
else if ((fdescription.type == 'selection') &&
((fdescription.loading || 'lazy') == 'eager')) {
fnames_to_fetch.push(fname + ':string');
}
}
if (!~fnames.indexOf('rec_name')) {
fnames_to_fetch.push('rec_name');
@@ -830,10 +834,16 @@
if (this.model.fields[name] instanceof Sao.field.One2Many) {
later[name] = value;
}
if ((this.model.fields[name] instanceof Sao.field.Many2One) ||
(this.model.fields[name] instanceof Sao.field.Reference)) {
var related = name + '.';
const field = this.model.fields[name];
var related;
if ((field instanceof Sao.field.Many2One) ||
(field instanceof Sao.field.Reference)) {
related = name + '.';
this._values[related] = values[related] || {};
} else if ((field instanceof Sao.field.Selection) &&
(name + ':string' in values)){
related = name + ':string';
this._values[related] = values[related];
}
this.model.fields[name].set(this, value);
this._loaded[name] = true;
+49 -32
View File
@@ -2529,38 +2529,49 @@
Sao.View.Tree.SelectionColumn._super.init.call(this, model,
attributes);
Sao.common.selection_mixin.init.call(this);
this.init_selection();
if (!this.tree || this.tree.editable) {
this.init_selection();
}
},
init_selection: function(key) {
Sao.common.selection_mixin.init_selection.call(this, key);
},
update_selection: function(record, callback) {
if (!this.selection) {
this.init_selection();
}
Sao.common.selection_mixin.update_selection.call(this, record,
this.field, callback);
},
update_text: function(cell, record) {
this.update_selection(record, () => {
var value = this.field.get(record);
var prm, text, found = false;
for (const option of this.selection) {
if (option[0] === value) {
found = true;
text = option[1];
break;
if (!this.tree.editable &&
(this.field.name + ':string' in record._values)) {
var text_value = record._values[this.field.name + ':string'];
cell.text(text_value).attr('title', text_value);
} else {
this.update_selection(record, () => {
var value = this.field.get(record);
var prm, text, found = false;
for (const option of this.selection) {
if (option[0] === value) {
found = true;
text = option[1];
break;
}
}
}
if (!found) {
prm = Sao.common.selection_mixin.get_inactive_selection
.call(this, value).then(function(inactive) {
return inactive[1];
});
} else {
prm = jQuery.when(text);
}
prm.done(text_value => {
cell.text(text_value).attr('title', text_value);
if (!found) {
prm = Sao.common.selection_mixin.get_inactive_selection
.call(this, value).then(function(inactive) {
return inactive[1];
});
} else {
prm = jQuery.when(text);
}
prm.done(text_value => {
cell.text(text_value).attr('title', text_value);
});
});
});
}
}
});
@@ -2580,18 +2591,24 @@
this.field, callback);
},
update_text: function(cell, record) {
this.update_selection(record, () => {
var values = this.field.get_eval(record).map(value => {
for (const option of this.selection) {
if (option[0] === value) {
return option[1];
}
}
return '';
});
var text_value = values.join(';');
if (!this.tree.editable &&
(this.field_name + ':string' in record._value)) {
var text_value = record._values[this.field_name + ':string'];
cell.text(text_value).attr('title', text_value);
});
} else {
this.update_selection(record, () => {
var values = this.field.get_eval(record).map(value => {
for (const option of this.selection) {
if (option[0] === value) {
return option[1];
}
}
return '';
});
var text_value = values.join(';');
cell.text(text_value).attr('title', text_value);
});
}
},
});
+1
View File
@@ -1,3 +1,4 @@
* Eagerly read string value of selection fields
* Do not select first record by default
* Use ``&`` and ``|`` as boolean operator in search widget
* Use toggle button and label on translate dialog
+2
View File
@@ -48,6 +48,8 @@ class SelectionMixin(object):
def update_selection(self, record, field):
if not field:
return
if not self.selection:
self.init_selection()
domain = field.domain_get(record)
if 'relation' not in self.attrs:
@@ -74,9 +74,13 @@ class Record:
fnames = [fname for fname, field in fields
if fname not in self._loaded
and (not views or (views & field.views))]
fnames.extend(('%s.rec_name' % fname for fname in fnames[:]
if self.group.fields[fname].attrs['type']
in ('many2one', 'one2one', 'reference')))
for fname in list(fnames):
f_attrs = self.group.fields[fname].attrs
if f_attrs['type'] in {'many2one', 'one2one', 'reference'}:
fnames.append('%s.rec_name' % fname)
elif (f_attrs['type'] == 'selection'
and f_attrs.get('loading', 'lazy') == 'eager'):
fnames.append('%s:string' % fname)
if 'rec_name' not in fnames:
fnames.append('rec_name')
fnames.extend(['_timestamp', '_write', '_delete'])
@@ -464,13 +468,17 @@ class Record:
if fieldname == 'rec_name':
self.value['rec_name'] = value
continue
if isinstance(self.group.fields[fieldname], fields.O2MField):
field = self.group.fields[fieldname]
if isinstance(field, fields.O2MField):
later[fieldname] = value
continue
if isinstance(self.group.fields[fieldname], (fields.M2OField,
fields.ReferenceField)):
if isinstance(field, (fields.M2OField, fields.ReferenceField)):
related = fieldname + '.'
self.value[related] = val.get(related) or {}
elif (isinstance(field, fields.SelectionField)
and fieldname + ':string' in val):
related = fieldname + ':string'
self.value[related] = val[related]
self.group.fields[fieldname].set(self, value)
self._loaded.add(fieldname)
fieldnames.append(fieldname)
@@ -1044,16 +1044,22 @@ class Selection(GenericText, SelectionMixin, PopdownMixin):
if 'renderer' not in kwargs:
kwargs['renderer'] = CellRendererCombo
super(Selection, self).__init__(*args, **kwargs)
self.init_selection()
# Use a variable let Python holding reference when calling set_property
model = self.get_popdown_model(self.selection)[0]
self.renderer.set_property('model', model)
self.renderer.set_property('text-column', 0)
if self.view and self.view.editable:
self.init_selection()
# Use a variable let Python holding reference when calling
# set_property
model = self.get_popdown_model(self.selection)[0]
self.renderer.set_property('model', model)
self.renderer.set_property('text-column', 0)
def get_value(self, record, field):
return field.get(record)
def get_textual_value(self, record):
related = self.attrs['name'] + ':string'
if not self.view.editable and related in record.value:
return record.value[related]
field = record[self.attrs['name']]
self.update_selection(record, field)
value = self.get_value(record, field)
@@ -1118,7 +1124,8 @@ class MultiSelection(GenericText, SelectionMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.init_selection()
if self.view and self.view.editable:
self.init_selection()
@realized
@CellCache.cache
@@ -1131,6 +1138,10 @@ class MultiSelection(GenericText, SelectionMixin):
callback()
def get_textual_value(self, record):
related = self.attrs['name'] + ':string'
if not self.view.editable and related in record.value:
return ";".join(record.value[related])
field = record[self.attrs['name']]
self.update_selection(record, field)
selection = dict(self.selection)
+1
View File
@@ -1,3 +1,4 @@
* Support reading string value of selection fields
* Add ButtonActionException and RPCReturnException
* Rename ModelStorage.count into ModelStorage.estimated_count
* Add border type to images
+26
View File
@@ -589,6 +589,22 @@ Selection
A dictionary mapping the selection value with its help string.
Class methods:
.. classmethod:: Selection.get_selection(model, name, inst)
Returns a :py:class`dictionary <dict>` mapping the selection value to its
human-readable value.
.. classmethod:: Selection.get_selection_string(selection, value)
Returns the human-readable form of ``value`` in regard to ``selection``.
``selection`` is acquired thanks to :meth:`Selection.get_selection`.
..note::
This method should be used instead of relying on dictionary access
because this method take into account the internal representation of the
``Selection`` value.
Instance methods:
.. method:: Selection.translated([name])
@@ -624,6 +640,16 @@ MultiSelection
Same as :attr:`Selection.help_selection`.
Class methods:
.. classmethod:: MultiSelection.get_selection(model, name, inst)
Same as :meth:`Selection.get_selection`
.. classmethod:: MultiSelection.get_selection_string(selection, value)
Same as :meth:`Selection.get_selection_string`
Instance methods:
.. method:: MultiSelection.translated([name])
+4
View File
@@ -296,6 +296,10 @@ Class methods:
``.``.
The number of *dots* in the name is not limited.
``fields_names`` can also contain ``<field>:string`` for
:class:`~fields.Selection` or :class:`~fields.MultiSelection` fields.
Their human-readable value are returned.
The virtual fields ``_write`` and ``_delete`` can be used the read the
writeable and deleteable state of the records.
Regarding the ``_timestamp`` virtual fields it contains a timestamp that is
+26 -17
View File
@@ -71,6 +71,27 @@ class SelectionMixin(Field):
(name, 'selection', language, source))
return super().definition_translations(model, language) + selection
@classmethod
def get_selection(cls, model, name, inst):
selection = model.fields_get([name])[name]['selection']
if isinstance(selection, str):
sel_func = getattr(model, selection)
if not is_instance_method(model, selection):
selection = sel_func()
else:
selection = sel_func(inst)
return dict(selection)
@classmethod
def get_selection_string(cls, selection, value):
# None and '' are equivalent
if value is None or value == '':
if value not in selection:
switch_value = {None: '', '': None}[value]
if switch_value in selection:
value = switch_value
return selection[value]
class Selection(SelectionMixin, Field):
'''
@@ -157,29 +178,17 @@ class TranslatedSelection(object):
from ..model import Model
if inst is None:
return self
field = cls._fields[self.name]
with Transaction().set_context(getattr(inst, '_context', {})):
selection = cls.fields_get([self.name])[self.name]['selection']
if not isinstance(selection, (tuple, list)):
sel_func = getattr(cls, selection)
if not is_instance_method(cls, selection):
selection = sel_func()
else:
selection = sel_func(inst)
selection = dict(selection)
selection = field.get_selection(cls, self.name, inst)
value = getattr(inst, self.name)
# None and '' are equivalent
if value is None or value == '':
if value not in selection:
switch_value = {None: '', '': None}[value]
if switch_value in selection:
value = switch_value
# Use Model __name__ for Reference field
elif isinstance(value, Model):
if isinstance(value, Model):
value = value.__name__
if isinstance(value, (list, tuple)):
values = []
for item in value:
values.append(selection.get(item, item))
values.append(field.get_selection_string(selection, item))
return values
else:
return selection.get(value, value)
return field.get_selection_string(selection, value)
+18 -2
View File
@@ -899,6 +899,9 @@ class ModelSQL(ModelStorage):
if '.' in field_name:
field_name, field_related = field_name.split('.', 1)
fields_related[field_name].add(field_related)
if field_name.endswith(':string'):
field_name = field_name[:-len(':string')]
fields_related[field_name]
field = cls._fields[field_name]
if hasattr(field, 'datetime_field') and field.datetime_field:
extra_fields.add(field.datetime_field)
@@ -1116,6 +1119,17 @@ class ModelSQL(ModelStorage):
for target in row[name]:
if target is not None:
values.append(targets[target])
elif field._type in {'selection', 'multiselection'}:
key = name + ':string'
for row, target in zip(rows, targets):
selection = field.get_selection(cls, name, target)
if field._type == 'selection':
row[key] = field.get_selection_string(
selection, row[name])
else:
row[key] = [
field.get_selection_string(selection, v)
for v in row[name]]
else:
for row in rows:
value = row[name]
@@ -1147,7 +1161,9 @@ class ModelSQL(ModelStorage):
ctx.update(PYSONDecoder(row).decode(pyson_context))
if datetime_field:
ctx['_datetime'] = row.get(datetime_field)
if field._type == 'reference':
if field._type in {'selection', 'multiselection'}:
Target = None
elif field._type == 'reference':
value = row[fname]
if not value:
Target = None
@@ -1171,7 +1187,7 @@ class ModelSQL(ModelStorage):
field, Target, rows, list(fields_related[fname]))
targets = {t['id']: t for t in targets}
else:
targets = {}
targets = cls.browse([r['id'] for r in rows])
add_related(field, rows, targets)
for row, field in product(result, to_del):
@@ -319,6 +319,45 @@ class FieldMultiSelectionTestCase(unittest.TestCase):
self.assertEqual(foo_baz, [])
self.assertEqual(empty, [selection])
@with_transaction()
def test_read_string(self):
"Test reading value and string"
Selection = Pool().get('test.multi_selection')
foo, foo_bar, null = Selection.create([{
'selects': ['foo'],
}, {
'selects': ['foo', 'bar'],
}, {
'selects': [],
}])
foo_read, foo_bar_read, null_read = Selection.read(
[foo.id, foo_bar.id, null.id], ['selects:string'])
self.assertEqual(foo_read['selects:string'], ["Foo"])
self.assertEqual(foo_bar_read['selects:string'], ["Bar", "Foo"])
self.assertEqual(null_read['selects:string'], [])
@with_transaction()
def test_read_string_dynamic_selection(self):
"Test reading value and string of a dynamic selection"
Selection = Pool().get('test.multi_selection')
foo, bar, null = Selection.create([{
'selects': ['foo'],
'dyn_selects': ['foo', 'foobar'],
}, {
'dyn_selects': ['bar', 'baz'],
}, {
'dyn_selects': [],
}])
foo_read, bar_read, null_read = Selection.read(
[foo.id, bar.id, null.id], ['dyn_selects:string'])
self.assertEqual(foo_read['dyn_selects:string'], ["Foo", "FooBar"])
self.assertEqual(bar_read['dyn_selects:string'], ["Bar", "Baz"])
self.assertEqual(null_read['dyn_selects:string'], [])
@unittest.skipIf(
backend.name != 'postgresql', 'jsonb only supported by postgresql')
@@ -242,3 +242,42 @@ class FieldSelectionTestCase(unittest.TestCase):
}])
self.assertEqual(selection.dyn_select_static_string, '1')
@with_transaction()
def test_read_string(self):
"Test reading value and string"
Selection = Pool().get('test.selection')
arabic, null = Selection.create([
{
'select': 'arabic',
},
{
'select': None,
},
])
arabic_read, null_read = Selection.read(
[arabic.id, null.id], ['select:string'])
self.assertEqual(arabic_read['select:string'], 'Arabic')
self.assertEqual(null_read['select:string'], '')
@with_transaction()
def test_read_string_dynamic_selection(self):
"Test reading value and string of dynamic selection"
Selection = Pool().get('test.selection')
one, null = Selection.create([
{
'select': 'arabic',
'dyn_select': '1'
},
{
'select': None,
},
])
one_read, null_read = Selection.read(
[one.id, null.id], ['dyn_select:string'])
self.assertEqual(one_read['dyn_select:string'], '1')
self.assertEqual(null_read['dyn_select:string'], '')