@@ -1,3 +1,4 @@
|
||||
* Read xxx2Many fields using the dotted notation
|
||||
* Display XML ID on Log window
|
||||
* Use operators when converting PYSON to string
|
||||
* Add DnD on binary and image widgets
|
||||
|
||||
+78
-14
@@ -3,6 +3,39 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function get_x2m_sub_fields(f_attrs, prefix) {
|
||||
if (f_attrs.loading == 'eager' && f_attrs.views) {
|
||||
// There's only one key but we don't know its value
|
||||
const [[, view],] = Object.entries(f_attrs.views);
|
||||
|
||||
const sub_fields = view.fields || {};
|
||||
const x2m_sub_fields = [];
|
||||
|
||||
for (const [s_field, f_def] of Object.entries(sub_fields)) {
|
||||
x2m_sub_fields.push(`${prefix}.${s_field}`);
|
||||
|
||||
var type_ = f_def.type;
|
||||
if (['many2one', 'one2one', 'reference'].includes(type_)) {
|
||||
x2m_sub_fields.push(`${prefix}.${s_field}.rec_name`);
|
||||
} else if (['selection', 'multiselection'].includes(type_)) {
|
||||
x2m_sub_fields.push(`${prefix}.${s_field}:string`);
|
||||
} else if (['one2many', 'many2many'].includes(type_)) {
|
||||
x2m_sub_fields.push(
|
||||
...get_x2m_sub_fields(f_def, `${prefix}.${s_field}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
x2m_sub_fields.push(
|
||||
`${prefix}._timestamp`,
|
||||
`${prefix}._write`,
|
||||
`${prefix}._delete`);
|
||||
return x2m_sub_fields;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Sao.Model = Sao.class_(Object, {
|
||||
init: function(name, attributes) {
|
||||
attributes = attributes || {};
|
||||
@@ -78,7 +111,7 @@
|
||||
this.__readonly = value;
|
||||
}
|
||||
});
|
||||
array.load = function(ids, modified=false, position=-1) {
|
||||
array.load = function(ids, modified=false, position=-1, preloaded=null) {
|
||||
if (position == -1) {
|
||||
position = this.length;
|
||||
}
|
||||
@@ -91,6 +124,9 @@
|
||||
this.splice(position, 0, new_record);
|
||||
position += 1;
|
||||
}
|
||||
if (preloaded && (id in preloaded)) {
|
||||
new_record.set(preloaded[id], false, false);
|
||||
}
|
||||
new_records.push(new_record);
|
||||
}
|
||||
// Remove previously removed or deleted records
|
||||
@@ -666,6 +702,7 @@
|
||||
fnames.push(fname);
|
||||
}
|
||||
}
|
||||
var related_read_limit = null;
|
||||
var fnames_to_fetch = fnames.slice();
|
||||
var rec_named_fields = ['many2one', 'one2one', 'reference'];
|
||||
const selection_fields = ['selection', 'multiselection'];
|
||||
@@ -676,6 +713,13 @@
|
||||
else if (~selection_fields.indexOf(fdescription.type) &&
|
||||
((fdescription.loading || 'lazy') == 'eager')) {
|
||||
fnames_to_fetch.push(fname + ':string');
|
||||
} else if (
|
||||
['many2many', 'one2many'].includes(fdescription.type)) {
|
||||
var sub_fields = get_x2m_sub_fields(fdescription, fname);
|
||||
fnames_to_fetch = [ ...fnames_to_fetch, ...sub_fields];
|
||||
if (sub_fields.length > 0) {
|
||||
related_read_limit = Sao.config.display_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!~fnames.indexOf('rec_name')) {
|
||||
@@ -686,6 +730,9 @@
|
||||
fnames_to_fetch.push('_delete');
|
||||
|
||||
var context = jQuery.extend({}, this.get_context());
|
||||
if (related_read_limit) {
|
||||
context.related_read_limit = related_read_limit;
|
||||
}
|
||||
if (loading == 'eager') {
|
||||
var limit = Math.trunc(Sao.config.limit /
|
||||
Math.min(fnames_to_fetch.length, 10));
|
||||
@@ -845,7 +892,7 @@
|
||||
}
|
||||
for (name in later) {
|
||||
value = later[name];
|
||||
this.model.fields[name].set(this, value);
|
||||
this.model.fields[name].set(this, value, false, values[`${name}.`]);
|
||||
this._loaded[name] = true;
|
||||
fieldnames.push(name);
|
||||
}
|
||||
@@ -2183,7 +2230,7 @@
|
||||
},
|
||||
_default: null,
|
||||
_single_value: false,
|
||||
_set_value: function(record, value, default_, modified) {
|
||||
_set_value: function(record, value, default_, modified, data) {
|
||||
this._set_default_value(record);
|
||||
var group = record._values[this.name];
|
||||
if (jQuery.isEmptyObject(value)) {
|
||||
@@ -2196,17 +2243,30 @@
|
||||
} else {
|
||||
mode = 'list values';
|
||||
}
|
||||
if (mode == 'list values') {
|
||||
if ((mode == 'list values') || data) {
|
||||
var context = this.get_context(record);
|
||||
let field_names = new Set();
|
||||
for (const val of value) {
|
||||
for (const fieldname in val) {
|
||||
if (!(fieldname in group.model.fields) &&
|
||||
(!~fieldname.indexOf('.')) &&
|
||||
(!fieldname.startsWith('_'))) {
|
||||
field_names.add(fieldname);
|
||||
var value_fields = new Set();
|
||||
if (mode == 'list values') {
|
||||
for (const v of value) {
|
||||
for (const f of Object.keys(v)) {
|
||||
value_fields.add(f);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const d of data) {
|
||||
for (const f in d) {
|
||||
value_fields.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
let field_names = new Set();
|
||||
for (const fieldname of value_fields) {
|
||||
if (!(fieldname in group.model.fields) &&
|
||||
(!~fieldname.indexOf('.')) &&
|
||||
(!~fieldname.indexOf(':')) &&
|
||||
(!fieldname.startsWith('_'))) {
|
||||
field_names.add(fieldname);
|
||||
}
|
||||
}
|
||||
var attr_fields = Object.values(this.description.views || {})
|
||||
.map(v => v.fields)
|
||||
@@ -2254,7 +2314,11 @@
|
||||
for (const record_to_remove of records_to_remove) {
|
||||
group.remove(record_to_remove, true, false, false);
|
||||
}
|
||||
group.load(value, modified || default_);
|
||||
var preloaded = {};
|
||||
for (const d of (data || [])) {
|
||||
preloaded[d.id] = d;
|
||||
}
|
||||
group.load(value, modified || default_, -1, preloaded);
|
||||
} else {
|
||||
for (const vals of value) {
|
||||
var new_record;
|
||||
@@ -2279,7 +2343,7 @@
|
||||
group.record_modified();
|
||||
}
|
||||
},
|
||||
set: function(record, value, _default=false) {
|
||||
set: function(record, value, _default=false, data=null) {
|
||||
var group = record._values[this.name];
|
||||
var model;
|
||||
if (group !== undefined) {
|
||||
@@ -2292,7 +2356,7 @@
|
||||
}
|
||||
record._values[this.name] = undefined;
|
||||
this._set_default_value(record, model);
|
||||
this._set_value(record, value, _default);
|
||||
this._set_value(record, value, _default, undefined, data);
|
||||
},
|
||||
get: function(record) {
|
||||
var group = record._values[this.name];
|
||||
|
||||
+2
-2
@@ -1319,7 +1319,7 @@
|
||||
}
|
||||
},
|
||||
load: function(ids, set_cursor=true, modified=false, position=-1) {
|
||||
this.group.load(ids, modified, position);
|
||||
this.group.load(ids, modified, position, null);
|
||||
this.current_view.reset();
|
||||
this.current_record = null;
|
||||
return this.display(set_cursor);
|
||||
@@ -1707,7 +1707,7 @@
|
||||
var records = this.current_view.selected_records;
|
||||
this.model.copy(records, this.context)
|
||||
.then(new_ids => {
|
||||
this.group.load(new_ids, false, this.new_position);
|
||||
this.group.load(new_ids, false, this.new_position, null);
|
||||
if (!jQuery.isEmptyObject(new_ids)) {
|
||||
this.current_record = this.group.get(new_ids[0]);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ function eval_pyson(value){
|
||||
this.container.add(null, attributes);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attributes.loading == 'eager') {
|
||||
this.field_attrs[name].loading = 'eager';
|
||||
}
|
||||
|
||||
var WidgetFactory = Sao.View.FormXMLViewParser.WIDGETS[
|
||||
attributes.widget];
|
||||
var widget = new WidgetFactory(this.view, attributes);
|
||||
@@ -3532,7 +3537,7 @@ function eval_pyson(value){
|
||||
for (i = 0, len = result.length; i < len; i++) {
|
||||
ids.push(result[i][0]);
|
||||
}
|
||||
this.screen.group.load(ids, true);
|
||||
this.screen.group.load(ids, true, -1, null);
|
||||
prm = this.screen.display();
|
||||
if (sequence) {
|
||||
this.screen.group.set_sequence(
|
||||
|
||||
+1
-1
@@ -445,7 +445,7 @@
|
||||
for (const record of result) {
|
||||
ids.push(record[0]);
|
||||
}
|
||||
this.screen.group.load(ids, true);
|
||||
this.screen.group.load(ids, true, -1, null);
|
||||
prm = this.screen.display();
|
||||
}
|
||||
prm.done(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
* Read xxx2Many fields using the dotted notation
|
||||
* Display XML ID on Log window
|
||||
* Use operators when converting PYSON to string
|
||||
* Add contextual menu to copy cell and column
|
||||
|
||||
@@ -701,7 +701,8 @@ class O2MField(Field):
|
||||
skip={self.attrs.get('relation_field', '')}))
|
||||
return result
|
||||
|
||||
def _set_value(self, record, value, default=False, modified=False):
|
||||
def _set_value(
|
||||
self, record, value, default=False, modified=False, data=None):
|
||||
self._set_default_value(record)
|
||||
group = record.value[self.name]
|
||||
if value is None:
|
||||
@@ -711,13 +712,18 @@ class O2MField(Field):
|
||||
else:
|
||||
mode = 'list values'
|
||||
|
||||
if mode == 'list values':
|
||||
if mode == 'list values' or data:
|
||||
context = self.get_context(record)
|
||||
field_names = set(f for v in value for f in v
|
||||
if mode == 'list values':
|
||||
fields = set(f for v in value for f in v)
|
||||
else:
|
||||
fields = functools.reduce(
|
||||
operator.or_, (d.keys() for d in data), set())
|
||||
field_names = {f for f in fields
|
||||
if (f not in group.fields
|
||||
and '.' not in f
|
||||
and ':' not in f
|
||||
and not f.startswith('_')))
|
||||
and not f.startswith('_'))}
|
||||
attr_fields = functools.reduce(
|
||||
operator.or_,
|
||||
(v['fields'] for v in self.attrs.get('views', {}).values()),
|
||||
@@ -739,7 +745,9 @@ class O2MField(Field):
|
||||
records_to_remove = [r for r in group if r.id not in value]
|
||||
for record_to_remove in records_to_remove:
|
||||
group.remove(record_to_remove, remove=True, modified=False)
|
||||
group.load(value, modified=modified or default)
|
||||
group.load(
|
||||
value, modified=modified or default,
|
||||
preloaded={v['id']: v for v in (data or [])})
|
||||
else:
|
||||
for vals in value:
|
||||
if 'id' in vals:
|
||||
@@ -760,7 +768,7 @@ class O2MField(Field):
|
||||
# Trigger modified only once
|
||||
group.record_modified()
|
||||
|
||||
def set(self, record, value, _default=False):
|
||||
def set(self, record, value, _default=False, preloaded=None):
|
||||
group = record.value.get(self.name)
|
||||
fields = {}
|
||||
if group is not None:
|
||||
@@ -780,7 +788,7 @@ class O2MField(Field):
|
||||
|
||||
# Prevent to trigger group-cleared
|
||||
group.parent = None
|
||||
self._set_value(record, value, default=_default)
|
||||
self._set_value(record, value, default=_default, data=preloaded)
|
||||
group.parent = record
|
||||
|
||||
def set_client(self, record, value, force_change=False):
|
||||
|
||||
@@ -239,7 +239,7 @@ class Group(list):
|
||||
return []
|
||||
return list({}.fromkeys(res))
|
||||
|
||||
def load(self, ids, modified=False, position=-1):
|
||||
def load(self, ids, modified=False, position=-1, preloaded=None):
|
||||
if not ids:
|
||||
return True
|
||||
|
||||
@@ -256,6 +256,8 @@ class Group(list):
|
||||
else:
|
||||
self.insert(position, new_record)
|
||||
position += 1
|
||||
if preloaded and (already_loaded := preloaded.get(id)):
|
||||
new_record.set(already_loaded, modified=False, validate=False)
|
||||
new_records.append(new_record)
|
||||
|
||||
# Remove previously removed or deleted records
|
||||
|
||||
@@ -1,6 +1,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.
|
||||
import functools
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import tryton.common as common
|
||||
from tryton.common import RPCException, RPCExecute
|
||||
@@ -13,6 +15,30 @@ from . import field as fields
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_x2m_sub_fields(f_attrs, prefix):
|
||||
if f_attrs['loading'] == 'eager' and f_attrs.get('views'):
|
||||
sub_fields = functools.reduce(
|
||||
operator.or_,
|
||||
(v.get('fields', {}) for v in f_attrs['views'].values()),
|
||||
{})
|
||||
x2m_sub_fields = []
|
||||
for s_field, f_def in sub_fields.items():
|
||||
x2m_sub_fields.append(f"{prefix}.{s_field}")
|
||||
if f_def['type'] in {'many2one', 'one2one', 'reference'}:
|
||||
x2m_sub_fields.append(f"{prefix}.{s_field}.rec_name")
|
||||
elif f_def['type'] in {'selection', 'multiselection'}:
|
||||
x2m_sub_fields.append(f"{prefix}.{s_field}:string")
|
||||
elif f_def['type'] in {'one2many', 'many2many'}:
|
||||
x2m_sub_fields.extend(
|
||||
get_x2m_sub_fields(f_def, f"{prefix}.{s_field}"))
|
||||
x2m_sub_fields.extend(
|
||||
f"{prefix}.{f}"
|
||||
for f in ['_timestamp', '_write', '_delete'])
|
||||
return x2m_sub_fields
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class Record:
|
||||
|
||||
id = -1
|
||||
@@ -80,6 +106,7 @@ class Record:
|
||||
fnames = [fname for fname, field in fields
|
||||
if fname not in self._loaded
|
||||
and (not views or (views & field.views))]
|
||||
related_read_limit = 0
|
||||
for fname in list(fnames):
|
||||
f_attrs = self.group.fields[fname].attrs
|
||||
if f_attrs['type'] in {'many2one', 'one2one', 'reference'}:
|
||||
@@ -87,6 +114,11 @@ class Record:
|
||||
elif (f_attrs['type'] in {'selection', 'multiselection'}
|
||||
and f_attrs.get('loading', 'lazy') == 'eager'):
|
||||
fnames.append('%s:string' % fname)
|
||||
elif f_attrs['type'] in {'many2many', 'one2many'}:
|
||||
sub_fields = get_x2m_sub_fields(f_attrs, fname)
|
||||
fnames.extend(sub_fields)
|
||||
if sub_fields:
|
||||
related_read_limit += len(sub_fields)
|
||||
if 'rec_name' not in fnames:
|
||||
fnames.append('rec_name')
|
||||
fnames.extend(['_timestamp', '_write', '_delete'])
|
||||
@@ -133,6 +165,9 @@ class Record:
|
||||
ctx.update(dict(('%s.%s' % (self.model_name, fname), 'size')
|
||||
for fname, field in self.group.fields.items()
|
||||
if field.attrs['type'] == 'binary' and fname in fnames))
|
||||
if related_read_limit:
|
||||
ctx['related_read_limit'] = (
|
||||
CONFIG['client.limit'] // min(related_read_limit, 10))
|
||||
exception = False
|
||||
try:
|
||||
values = RPCExecute('model', self.model_name, 'read',
|
||||
@@ -497,7 +532,9 @@ class Record:
|
||||
self._loaded.add(fieldname)
|
||||
fieldnames.append(fieldname)
|
||||
for fieldname, value in later.items():
|
||||
self.group.fields[fieldname].set(self, value)
|
||||
if isinstance(
|
||||
field := self.group.fields[fieldname], fields.O2MField):
|
||||
field.set(self, value, preloaded=val.get(f"{fieldname}."))
|
||||
self._loaded.add(fieldname)
|
||||
fieldnames.append(fieldname)
|
||||
if validate:
|
||||
|
||||
@@ -230,6 +230,9 @@ class FormXMLViewParser(XMLViewParser):
|
||||
self.container.add(None, attributes)
|
||||
return
|
||||
|
||||
if attributes.get('loading') == 'eager':
|
||||
self.field_attrs[name]['loading'] = 'eager'
|
||||
|
||||
widget = self.WIDGETS[attributes['widget']](self.view, attributes)
|
||||
self.view.widgets[name].append(widget)
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
* Set loading eager to visible xxx2many on form view
|
||||
* Add loading attribute to form view
|
||||
* Add a contextual read limit on Model.read
|
||||
* Add a contextual read limit for related fields
|
||||
* Add xml_id field on ModelStorage
|
||||
* Add send_message methods
|
||||
|
||||
@@ -365,6 +365,14 @@ attributes:
|
||||
``rounded`` or ``circle``.
|
||||
The default value is ``square``.
|
||||
|
||||
``loading``
|
||||
Override the :attr:`~trytond.model.fields.Field.loading` of the field.
|
||||
When ``eager`` for :class:`~trytond.model.fields.One2Many` and
|
||||
:class:`~trytond.model.fields.Many2Many` the client may try to read the
|
||||
fields of the related model using dotted notation.
|
||||
This result in less round trip between the client and the server at the
|
||||
expense of a bigger payload and a bigger query execution time.
|
||||
|
||||
:ref:`yexpand <common-attributes-yexpand>`,
|
||||
:ref:`yfill <common-attributes-yfill>`,
|
||||
:ref:`xexpand <common-attributes-xexpand>`,
|
||||
|
||||
@@ -121,6 +121,7 @@ attlist.field &=
|
||||
attlist.field &= attribute symbol { text }?
|
||||
attlist.field &= [a:defaultValue = "1"] attribute grouping { "0" | "1" }?
|
||||
attlist.field &= [a:defaultValue = "square"] attribute border { "square" | "circle" | "rounded" }?
|
||||
attlist.field &= attribute loading { "lazy" | "eager" }?
|
||||
image = element image { attlist.image, empty }
|
||||
attlist.image &= attribute name { text }
|
||||
attlist.image &= [a:defaultValue = "icon"] attribute type { "icon" | "url" }?
|
||||
|
||||
@@ -577,6 +577,17 @@
|
||||
</attribute>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="attlist.field" combine="interleave">
|
||||
<optional>
|
||||
<attribute>
|
||||
<name ns="">loading</name>
|
||||
<choice>
|
||||
<value>lazy</value>
|
||||
<value>eager</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="image">
|
||||
<element>
|
||||
<name ns="">image</name>
|
||||
|
||||
@@ -45,6 +45,23 @@ class ButtonActionException(RPCReturnException):
|
||||
return self.value
|
||||
|
||||
|
||||
def set_visible(node, fields, loading='eager'):
|
||||
"Set loading to visible fields"
|
||||
if node.tag == 'group':
|
||||
if node.attrib.get('expandable', '1') == '1':
|
||||
for child in node:
|
||||
set_visible(child, fields, loading)
|
||||
elif node.tag == 'notebook':
|
||||
if len(node) > 0:
|
||||
set_visible(node[0], fields, loading)
|
||||
elif node.tag == 'field':
|
||||
if node.attrib['name'] in fields:
|
||||
node.attrib['loading'] = 'eager'
|
||||
else:
|
||||
for child in node:
|
||||
set_visible(child, fields, loading)
|
||||
|
||||
|
||||
def on_change(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
@@ -443,11 +460,14 @@ class ModelView(Model):
|
||||
if colspan is not None:
|
||||
element.attrib['colspan'] = colspan
|
||||
|
||||
# Remove empty pages
|
||||
if type == 'form':
|
||||
for page in tree.xpath('//page[not(descendant::*)]'):
|
||||
page.getparent().remove(page)
|
||||
|
||||
set_visible(tree,
|
||||
{fname for fname, field in cls._fields.items()
|
||||
if field._type in {'one2many', 'many2many'}})
|
||||
|
||||
if type == 'tree':
|
||||
user = Transaction().user
|
||||
if Transaction().context.get('view_tree_width'):
|
||||
|
||||
@@ -8,6 +8,7 @@ from lxml import etree
|
||||
|
||||
from trytond.model.exceptions import (
|
||||
AccessButtonError, AccessError, ButtonActionException)
|
||||
from trytond.model.modelview import set_visible
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, PYSONDecoder, PYSONEncoder
|
||||
from trytond.tests.test_tryton import activate_module, with_transaction
|
||||
@@ -20,6 +21,162 @@ class ModelView(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
activate_module('tests')
|
||||
|
||||
def test_set_visible(self):
|
||||
"Test loading of visible x2m fields in group"
|
||||
|
||||
GROUP_XML = """
|
||||
<form>
|
||||
<group>
|
||||
<field name="OK"/>
|
||||
</group>
|
||||
<group expandable="1">
|
||||
<field name="OK"/>
|
||||
<group expandable="0">
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
</group>
|
||||
<group expandable="0">
|
||||
<field name="KO"/>
|
||||
<group>
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="OK"/>
|
||||
</group>
|
||||
<group expandable="1">
|
||||
<field name="OK"/>
|
||||
</group>
|
||||
<group expandable="0">
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
"""
|
||||
group_tree = etree.fromstring(GROUP_XML)
|
||||
set_visible(group_tree, {'OK', 'KO'})
|
||||
self.assertEqual(
|
||||
['OK', 'OK', 'OK', 'OK'],
|
||||
[n.attrib['name']
|
||||
for n in group_tree.xpath("//field[@loading='eager']")])
|
||||
|
||||
def test_mark_visible_x2m_notebook(self):
|
||||
"Test marking visible x2m fields in notebook"
|
||||
|
||||
NOTEBOOK_XML = """
|
||||
<form>
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="OK"/>
|
||||
<field name="OK"/>
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="OK"/>
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="OK"/>
|
||||
<field name="OK"/>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
"""
|
||||
nb_tree = etree.fromstring(NOTEBOOK_XML)
|
||||
set_visible(nb_tree, {'OK', 'KO'})
|
||||
self.assertEqual(
|
||||
['OK', 'OK', 'OK', 'OK', 'OK'],
|
||||
[n.attrib['name']
|
||||
for n in nb_tree.xpath("//field[@loading='eager']")])
|
||||
|
||||
def test_mark_visible_x2m_notebook_group(self):
|
||||
"Test marking visible x2m fields in notebook and groups"
|
||||
|
||||
NBGROUP_XML = """
|
||||
<form>
|
||||
<notebook>
|
||||
<page>
|
||||
<group>
|
||||
<field name="OK"/>
|
||||
</group>
|
||||
<group expandable="0">
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
<group expandable="1">
|
||||
<field name="OK"/>
|
||||
</group>
|
||||
</page>
|
||||
<page>
|
||||
<group>
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
<group expandable="0">
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
<group expandable="1">
|
||||
<field name="KO"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
<group expandable="0">
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
<group expandable="1">
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="OK"/>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
<group>
|
||||
<notebook>
|
||||
<page>
|
||||
<field name="OK"/>
|
||||
</page>
|
||||
<page>
|
||||
<field name="KO"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
</form>
|
||||
"""
|
||||
ng_tree = etree.fromstring(NBGROUP_XML)
|
||||
set_visible(ng_tree, {'OK', 'KO'})
|
||||
self.assertEqual(
|
||||
['OK', 'OK', 'OK', 'OK'],
|
||||
[n.attrib['name']
|
||||
for n in ng_tree.xpath("//field[@loading='eager']")])
|
||||
|
||||
@with_transaction()
|
||||
def test_changed_values(self):
|
||||
"Test ModelView._changed_values"
|
||||
|
||||
Reference in New Issue
Block a user