Catch error when rendering cells in treeviews

Closes #11835
This commit is contained in:
Nicolas Évrard
2023-03-01 18:21:10 +01:00
parent b433317374
commit 4dfea2f82c
8 changed files with 151 additions and 48 deletions
+1 -1
View File
@@ -788,7 +788,7 @@
'method': 'model.' + this.attributes.relation +
'.search_read',
'params': [domain, 0, null, null, fields, context]
}, record.model.session);
}, record.model.session, true, false);
prm.done(result => {
var selection = [];
for (const x of result) {
+6 -4
View File
@@ -24,12 +24,14 @@
}
return added;
},
execute: function(method, params, context={}, async=true) {
execute: function(
method, params, context={}, async=true,
process_exception=true) {
var args = {
'method': 'model.' + this.name + '.' + method,
'params': params.concat(context)
};
return Sao.rpc(args, this.session, async);
return Sao.rpc(args, this.session, async, process_exception);
},
copy: function(records, context) {
if (jQuery.isEmptyObject(records)) {
@@ -609,7 +611,7 @@
is_loaded: function(name) {
return ((this.id < 0) || (name in this._loaded));
},
load: function(name, async=true) {
load: function(name, async=true, process_exception=true) {
var fname;
var prm;
if (this.destroyed || this.is_loaded(name)) {
@@ -748,7 +750,7 @@
}
var result = this.model.execute('read', [
Object.keys(id2record).map( e => parseInt(e, 10)),
fnames_to_fetch], context, async);
fnames_to_fetch], context, async, process_exception);
const succeed = (values, exception=false) => {
var id2value = {};
for (const e of values) {
+5 -2
View File
@@ -3,7 +3,7 @@
(function() {
'use strict';
Sao.rpc = function(args, session=null, async=true) {
Sao.rpc = function(args, session=null, async=true, process_exception=true) {
var dfd = jQuery.Deferred(),
result;
if (!session) {
@@ -32,7 +32,10 @@
Sao.common.warning.run('',
Sao.i18n.gettext('Unable to reach the server.'))
.always(dfd.reject);
} else if (data.error) {
} else if (data.error && !process_exception) {
console.debug(`RPC error calling ${args}: ${data.error[0]}: ${data.error[1]}.`);
dfd.reject();
} else if (data.error && process_exception) {
var name, msg, description;
if (data.error[0] == 'UserWarning') {
name = data.error[1][0];
+33 -10
View File
@@ -1389,7 +1389,7 @@
if (field_name && !record.is_loaded(field_name)) {
// Prefetch the first field to prevent promises in
// Cell.render
record.load(field_name).done(redraw);
record.load(field_name, true, false).done(redraw);
return;
} else {
row.redraw(selected, expanded);
@@ -1686,7 +1686,8 @@
}
};
if (!this.record.is_loaded(this.children_field)) {
this.record.load(this.children_field).done(update_expander);
this.record.load(this.children_field, true, false)
.done(update_expander);
} else {
update_expander();
}
@@ -1761,7 +1762,8 @@
this.rows = [];
},
expand_children: function(selected, expanded) {
return this.record.load(this.children_field).done(() => {
return this.record.load(
this.children_field, true, false).done(() => {
if (this.rows.length === 0) {
var children = this.record.field_get_client(
this.children_field);
@@ -1957,7 +1959,8 @@
if (widget) {
var callback = display_callback(widget);
if (!this.record.is_loaded(column.attributes.name)) {
this.record.load(column.attributes.name).done(callback);
this.record.load(column.attributes.name, true, false)
.done(callback);
} else {
callback();
}
@@ -2304,7 +2307,7 @@
}
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name).done(render);
record.load(this.attributes.name, true, false).done(render);
} else {
render();
}
@@ -2353,7 +2356,7 @@
}
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name).done(render);
record.load(this.attributes.name, true, false).done(render);
} else {
render();
}
@@ -2405,10 +2408,21 @@
cell.show();
}
};
const render_error = () => {
var text = Sao.i18n.gettext('#ERROR');
cell.text(text).attr('title', text);
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name).done(render);
record.load(this.attributes.name, true, false)
.done(render)
.fail(render_error);
} else {
render();
if (record.exception) {
render_error();
} else {
render();
}
}
return cell;
},
@@ -2832,10 +2846,19 @@
set_src(null);
}
};
const render_error = () => {
cell.attr('src', null);
};
if (!record.is_loaded(this.attributes.name)) {
record.load(this.attributes.name).done(render);
record.load(this.attributes.name, true, false)
.done(render)
.fail(render_error);
} else {
render();
if (render.exception) {
render_error();
} else {
render();
}
}
return cell;
}
+1 -1
View File
@@ -73,7 +73,7 @@ class SelectionMixin(object):
try:
result = RPCExecute('model', self.attrs['relation'],
'search_read', domain, 0, None, None, fields,
context=context)
context=context, process_exception=False)
except RPCException:
result = False
if isinstance(result, list):
@@ -44,6 +44,11 @@ class Record:
self.destroyed = False
def __getitem__(self, name):
self.load(name)
if name != '*':
return self.group.fields[name]
def load(self, name, process_exception=True):
if not self.destroyed and self.id >= 0 and name not in self._loaded:
id2record = {
self.id: self,
@@ -130,7 +135,8 @@ class Record:
exception = False
try:
values = RPCExecute('model', self.model_name, 'read',
list(id2record.keys()), fnames, context=ctx)
list(id2record.keys()), fnames, context=ctx,
process_exception=process_exception)
except RPCException:
values = [{'id': x} for x in id2record]
default_values = dict((f, None) for f in fnames)
@@ -146,8 +152,6 @@ class Record:
for key in record.modified_fields:
value.pop(key, None)
record.set(value, modified=False)
if name != '*':
return self.group.fields[name]
def __repr__(self):
return '<Record %s@%s at %s>' % (self.id, self.model_name, id(self))
@@ -1,6 +1,7 @@
# 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
import logging
from itertools import chain, cycle, islice
from gi.repository import Gdk, GLib, Gtk
@@ -9,6 +10,7 @@ from tryton.common import MODELACCESS
from tryton.common.datetime_ import Date, Time
_ = gettext.gettext
logger = logging.getLogger(__name__)
def focusable_cells(column, editable=True):
@@ -51,7 +53,14 @@ class TreeView(Gtk.TreeView):
if not column.name:
continue
widget = self.view.get_column_widget(column)
field = record[column.name]
try:
record.load(column.name, process_exception=False)
except Exception:
logger.error(
f"Error loading '{column.name}' for {record}",
exc_info=True)
return (None, None)
field = record.group.fields[column.name]
field.state_set(record, states=('readonly', 'invisible'))
invisible = field.get_state_attrs(record).get('invisible', False)
if not column.get_visible():
@@ -83,7 +92,8 @@ class EditableTreeView(TreeView):
def on_quit_cell(
self, current_record, column, renderer, value, callback=None):
field = current_record[column.name]
current_record.load(column.name, process_exception=False)
field = current_record.group.fields[column.name]
widget = self.view.get_column_widget(column)
# The value has not changed and is valid ... do nothing.
@@ -271,7 +281,7 @@ class EditableTreeView(TreeView):
create=(event.keyval == Gdk.KEY_F3), value=value,
callback=callback)
else:
field = record[column.name]
field = record.group.fields[column.name]
if isinstance(entry, Gtk.Entry):
entry.set_max_length(int(field.attrs.get('size', 0)))
record.modified_fields.setdefault(column.name)
@@ -2,6 +2,7 @@
# this repository contains the full copyright notices and license terms.
import datetime
import gettext
import logging
import os
import webbrowser
from functools import partial, wraps
@@ -31,6 +32,7 @@ from tryton.gui.window.win_form import WinForm
from tryton.gui.window.win_search import WinSearch
_ = gettext.gettext
logger = logging.getLogger(__name__)
COLORS = {n: v for n, v in zip(
['muted', 'success', 'warning', 'danger'],
@@ -50,6 +52,23 @@ def send_keys(renderer, editable, position, treeview):
editable.connect('changed', changed)
def catch_errors(error_value=_('#ERROR')):
def decorator(func):
@wraps(func)
def wrapper(self, record):
if record.exception:
return error_value
try:
return func(self, record)
except Exception:
logger.error(
f"Error calling {func.__name__} for {self} with {record}",
exc_info=True)
return error_value
return wrapper
return decorator
def realized(func):
@wraps(func)
@@ -143,7 +162,7 @@ class Cell(object):
if not store:
store = self.view.treeview.get_model()
record = store.get_value(iter_, 0)
field = record[self.attrs['name']]
field = record.group.fields[self.attrs['name']]
return record, field
def _set_visual(self, cell, record):
@@ -330,9 +349,9 @@ class GenericText(Cell):
callback=None):
raise NotImplementedError
@catch_errors()
def get_textual_value(self, record):
if not record:
return ''
record.load(self.attrs['name'], process_exception=False)
return record[self.attrs['name']].get_client(record)
def value_from_text(self, record, text, callback=None):
@@ -396,9 +415,9 @@ class Int(GenericText):
return [self.renderer_suffix]
return []
@catch_errors()
def get_textual_value(self, record):
if not record:
return ''
record.load(self.attrs['name'], process_exception=False)
return record[self.attrs['name']].get_client(
record, factor=self.factor, grouping=self.grouping)
@@ -464,9 +483,9 @@ class Date(GenericText):
else:
return '%x'
@catch_errors()
def get_textual_value(self, record):
if not record:
return ''
record.load(self.attrs['name'], process_exception=False)
value = record[self.attrs['name']].get_client(record)
if value:
return value.strftime(self.renderer.props.format)
@@ -487,9 +506,9 @@ class Time(Date):
else:
return '%X'
@catch_errors()
def get_textual_value(self, record):
if not record:
return ''
record.load(self.attrs['name'], process_exception=False)
value = record[self.attrs['name']].get_client(record)
if value is not None:
if isinstance(value, datetime.datetime):
@@ -549,7 +568,9 @@ class Binary(GenericText):
def suffixes(self):
return [self.renderer_save, self.renderer_select]
@catch_errors()
def get_textual_value(self, record):
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
if hasattr(field, 'get_size'):
size = field.get_size(record)
@@ -604,6 +625,11 @@ class _BinaryIcon(Cell):
def view(self):
return self.binary.view
@catch_errors(False)
def _fetch_data(self, record):
record.fetch(self.attrs['name'], process_exception=False)
return True
class _BinarySave(_BinaryIcon):
icon_name = 'tryton-save'
@@ -631,6 +657,9 @@ class _BinarySave(_BinaryIcon):
@CellCache.cache
def setter(self, column, cell, store, iter_, user_data=None):
record, field = self._get_record_field_from_iter(iter_, store)
if not self._fetch_data(record):
cell.set_property('visible', False)
return
if hasattr(field, 'get_size'):
size = field.get_size(record)
else:
@@ -671,6 +700,9 @@ class _BinarySelect(_BinaryIcon):
@CellCache.cache
def setter(self, column, cell, store, iter_, user_data=None):
record, field = self._get_record_field_from_iter(iter_, store)
if not self._fetch_data(record):
cell.set_property('visible', False)
return
if hasattr(field, 'get_size'):
size = field.get_size(record)
else:
@@ -710,6 +742,9 @@ class _BinaryOpen(_BinarySave):
def setter(self, column, cell, store, iter_, user_data=None):
super().setter(column, cell, store, iter_)
record, field = self._get_record_field_from_iter(iter_, store)
if not self._fetch_data(record):
cell.set_property('visible', False)
return
filename_field = record.group.fields.get(self.attrs.get('filename'))
filename = filename_field.get(record)
if not filename:
@@ -731,22 +766,33 @@ class Image(GenericText):
@CellCache.cache
def setter(self, column, cell, store, iter_, user_data=None):
record, field = self._get_record_field_from_iter(iter_, store)
value = field.get_client(record)
if isinstance(value, int):
if value > CONFIG['image.max_size']:
value = None
else:
value = field.get_data(record)
value = self._get_data(record)
if value is None:
cell.set_property('pixbuf', None)
return
pixbuf = data2pixbuf(value)
if pixbuf:
pixbuf = common.resize_pixbuf(pixbuf, self.width, self.height)
cell.set_property('pixbuf', pixbuf)
self._set_visual(cell, record)
@catch_errors(None)
def _get_data(self, record):
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
value = field.get_client(record)
if isinstance(value, int):
if value > CONFIG['image.max_size']:
value = None
else:
value = field.get_data(record)
return value
@catch_errors()
def get_textual_value(self, record):
if not record:
return ''
return str(record[self.attrs['name']].get_size(record))
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
return str(field.get_size(record))
class M2O(GenericText):
@@ -974,9 +1020,11 @@ class O2O(M2O):
class O2M(GenericText):
align = 0.5
@catch_errors()
def get_textual_value(self, record):
return '( ' + str(len(record[self.attrs['name']]
.get_eval(record))) + ' )'
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
return '( ' + str(len(field.get_eval(record))) + ' )'
def value_from_text(self, record, text, callback=None):
if callback:
@@ -1056,13 +1104,16 @@ class Selection(GenericText, SelectionMixin, PopdownMixin):
def get_value(self, record, field):
return field.get(record)
@catch_errors()
def get_textual_value(self, record):
related = self.attrs['name'] + ':string'
record.load(related, process_exception=False)
if not self.view.editable and related in record.value:
return record.value[related]
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
self.update_selection(record, field)
self.update_selection(record, field, process_exception=False)
value = self.get_value(record, field)
text = dict(self.selection).get(value, '')
if value and not text:
@@ -1138,13 +1189,16 @@ class MultiSelection(GenericText, SelectionMixin):
if callback:
callback()
@catch_errors()
def get_textual_value(self, record):
related = self.attrs['name'] + ':string'
record.load(related, process_exception=False)
if not self.view.editable and related in record.value:
return ";".join(record.value[related])
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
self.update_selection(record, field)
self.update_selection(record, field, process_exception=False)
selection = dict(self.selection)
values = []
for value in field.get_eval(record):
@@ -1191,6 +1245,7 @@ class Reference(M2O):
_, value = value.split(',')
return int(value)
@catch_errors()
def get_textual_value(self, record):
value = super().get_textual_value(record)
if value:
@@ -1232,8 +1287,11 @@ class Dict(GenericText):
super().setter(column, cell, store, iter_, user_data=None)
cell.props.editable = False
@catch_errors()
def get_textual_value(self, record):
return '(%s)' % len(record[self.attrs['name']].get_client(record))
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
return '(%s)' % len(field.get_client(record))
class ProgressBar(Cell):
@@ -1275,8 +1333,11 @@ class ProgressBar(Cell):
callback=None):
raise NotImplementedError
@catch_errors()
def get_textual_value(self, record):
return record[self.attrs['name']].get_client(record, factor=100) or ''
record.load(self.attrs['name'], process_exception=False)
field = record[self.attrs['name']]
return field.get_client(record, factor=100) or ''
def value_from_text(self, record, text, callback=None):
field = record[self.attrs['name']]