[IMP] web: x2m list: conditionnaly hide a column

This commit introduces a new way to hide a column in a x2m list view.
The attribute 'tree_invisible' can be add in the field attrs in the x2m
view definition. This attribute can use the 'parent' key to make a reference
to the parent record (e.g. 'parent.id').
This commit is contained in:
fwi-odoo
2017-09-29 16:49:20 +02:00
parent a7d1740e47
commit da136d83bb
12 changed files with 191 additions and 31 deletions
@@ -32,7 +32,7 @@ class GoogleDrive(models.Model):
for node in doc.xpath("//field"): for node in doc.xpath("//field"):
if node.get('modifiers'): if node.get('modifiers'):
modifiers = json.loads(node.get('modifiers')) modifiers = json.loads(node.get('modifiers'))
if not modifiers.get('invisible') and not modifiers.get('tree_invisible'): if not modifiers.get('invisible') and not modifiers.get('column_invisible'):
display_fields.append(node.get('name')) display_fields.append(node.get('name'))
fields = " ".join(display_fields) fields = " ".join(display_fields)
domain = domain.replace("'", r"\'").replace('"', "'") domain = domain.replace("'", r"\'").replace('"', "'")
+19 -2
View File
@@ -51,13 +51,30 @@ var Domain = collections.Tree.extend({
} else if (_.isArray(this._data)) { } else if (_.isArray(this._data)) {
// The domain is a [name, operator, value] entity // The domain is a [name, operator, value] entity
// First check if we have the field value in the field values set // First check if we have the field value in the field values set
if (!(this._data[0] in values)) { // and if the first part of the domain contains 'parent.field'
// get the value from the parent record.
var isParentField = false;
var fieldName = this._data[0];
// We split the domain first part and check if it's a match
// for the syntax 'parent.field'.
var parentField = this._data[0].split('.');
if ('parent' in values && parentField.length === 2) {
fieldName = parentField[1];
isParentField = parentField[0] === 'parent' &&
fieldName in values.parent;
}
if (!(this._data[0] in values) && !(isParentField)) {
throw new Error(_.str.sprintf( throw new Error(_.str.sprintf(
"Unknown field %s in domain", "Unknown field %s in domain",
this._data[0] this._data[0]
)); ));
} }
var fieldValue = values[this._data[0]]; var fieldValue;
if (!isParentField) {
fieldValue = values[fieldName];
} else {
fieldValue = values.parent[fieldName];
}
switch (this._data[1]) { switch (this._data[1]) {
case "=": case "=":
@@ -604,6 +604,11 @@ var FieldX2Many = AbstractField.extend({
toggle_column_order: '_onToggleColumnOrder', toggle_column_order: '_onToggleColumnOrder',
}), }),
// We need to trigger the reset on every changes to be aware of the parent changes
// and then evaluate the 'column_invisible' modifier in case a evaluated value
// changed.
resetOnAnyFieldChange: true,
/** /**
* useSubview is used in form view to load view of the related model of the x2many field * useSubview is used in form view to load view of the related model of the x2many field
*/ */
@@ -629,6 +634,9 @@ var FieldX2Many = AbstractField.extend({
true; true;
this.editable = arch.attrs.editable; this.editable = arch.attrs.editable;
} }
if (this.attrs.columnInvisibleFields) {
this._processColumnInvisibleFields();
}
}, },
/** /**
* @override * @override
@@ -673,10 +681,20 @@ var FieldX2Many = AbstractField.extend({
* @override * @override
* @param {Object} record * @param {Object} record
* @param {OdooEvent} [ev] an event that triggered the reset action * @param {OdooEvent} [ev] an event that triggered the reset action
* @param {Boolean} [fieldChanged] if true, the widget field has changed
* @returns {Deferred} * @returns {Deferred}
*/ */
reset: function (record, ev) { reset: function (record, ev, fieldChanged) {
if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') { // If 'fieldChanged' is false, it means that the reset was triggered by
// the 'resetOnAnyFieldChange' mechanism. If it is the case the
// modifiers are evaluated and if there is no change in the modifiers
// values, the reset is skipped.
if (!fieldChanged) {
var newEval = this._evalColumnInvisibleFields();
if (_.isEqual(this.currentColInvisibleFields, newEval)) {
return $.when();
}
} else if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') {
var command = ev.data.changes[this.name]; var command = ev.data.changes[this.name];
// Here, we only consider 'UPDATE' commands with data, which occur // Here, we only consider 'UPDATE' commands with data, which occur
// with editable list view. In order to keep the current line in // with editable list view. In order to keep the current line in
@@ -700,6 +718,20 @@ var FieldX2Many = AbstractField.extend({
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/**
* Evaluates the 'column_invisible' modifier for the parent record.
*
* @return {Object} Object containing fieldName as key and the evaluated
* column_invisible modifier
*/
_evalColumnInvisibleFields: function () {
var self = this;
return _.mapObject(this.columnInvisibleFields, function (domains) {
return self.record.evalModifiers({
column_invisible: domains,
}).column_invisible;
});
},
/** /**
* Instanciates or updates the adequate renderer. * Instanciates or updates the adequate renderer.
* *
@@ -713,19 +745,25 @@ var FieldX2Many = AbstractField.extend({
} }
if (this.renderer) { if (this.renderer) {
this.renderer.updateState(this.value, {}); this.renderer.updateState(this.value, {});
this.pager.updateState({ size: this.value.count }); this.currentColInvisibleFields = this._evalColumnInvisibleFields();
this.pager.updateState({
size: this.value.count,
columnInvisibleFields: this.currentColInvisibleFields,
});
return $.when(); return $.when();
} }
var arch = this.view.arch; var arch = this.view.arch;
var viewType; var viewType;
if (arch.tag === 'tree') { if (arch.tag === 'tree') {
viewType = 'list'; viewType = 'list';
this.currentColInvisibleFields = this._evalColumnInvisibleFields();
this.renderer = new ListRenderer(this, this.value, { this.renderer = new ListRenderer(this, this.value, {
arch: arch, arch: arch,
mode: this.mode, mode: this.mode,
addCreateLine: !this.isReadonly && this.activeActions.create, addCreateLine: !this.isReadonly && this.activeActions.create,
addTrashIcon: !this.isReadonly && this.activeActions.delete, addTrashIcon: !this.isReadonly && this.activeActions.delete,
viewType: viewType, viewType: viewType,
columnInvisibleFields: this.currentColInvisibleFields,
}); });
} }
if (arch.tag === 'kanban') { if (arch.tag === 'kanban') {
@@ -826,6 +864,30 @@ var FieldX2Many = AbstractField.extend({
} }
return def; return def;
}, },
/**
* Parses the 'columnInvisibleFields' attribute to search for the domains
* containing the key 'parent'. If there are such domains, the string
* 'parent.field' is replaced with 'field' in order to be evaluated
* with the right field name in the parent context.
*
* @private
*/
_processColumnInvisibleFields: function () {
var columnInvisibleFields = {};
_.each(this.attrs.columnInvisibleFields, function (domains, fieldName) {
if (_.isArray(domains)) {
columnInvisibleFields[fieldName] = _.map(domains, function (domain) {
// We check if the domain is an array to avoid processing
// the '|' and '&' cases
if (_.isArray(domain)) {
return [domain[0].split('.')[1]].concat(domain.slice(1));
}
return domain;
});
}
});
this.columnInvisibleFields = columnInvisibleFields;
},
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Handlers // Handlers
@@ -283,15 +283,15 @@ return core.Class.extend({
// the invisible attribute of a field is supposed to be static ("1" in // the invisible attribute of a field is supposed to be static ("1" in
// general), but not totally as it may use keys of the context // general), but not totally as it may use keys of the context
// ("context.get('some_key')"). It is evaluated server-side, and the // ("context.get('some_key')"). It is evaluated server-side, and the
// result is put inside the modifiers as a value of the '(tree_)invisible' // result is put inside the modifiers as a value of the '(column_)invisible'
// key, and the raw value is left in the invisible attribute (it is used // key, and the raw value is left in the invisible attribute (it is used
// in debug mode for informational purposes). // in debug mode for informational purposes).
// this should change, for instance the server might set the evaluated // this should change, for instance the server might set the evaluated
// value in invisible, which could then be seen as static by the client, // value in invisible, which could then be seen as static by the client,
// and add another key in debug mode containing the raw value. // and add another key in debug mode containing the raw value.
// for now, we do an hack to detect if the value is static and retrieve // for now, we look inside the modifiers and consider the value only if
// it from the modifiers, // it is static (=== true),
if (attrs.invisible && attrs.modifiers.match('"(?:tree_)?invisible": ?true')) { if (attrs.modifiers.invisible === true || attrs.modifiers.column_invisible === true) {
attrs.__no_fetch = true; attrs.__no_fetch = true;
} }
@@ -357,6 +357,15 @@ return core.Class.extend({
attrs.orderedBy = [{name: handleField.attrs.name, asc: true}]; attrs.orderedBy = [{name: handleField.attrs.name, asc: true}];
} }
} }
attrs.columnInvisibleFields = {};
_.each(view.arch.children, function (child) {
if (child.attrs && child.attrs.modifiers) {
attrs.columnInvisibleFields[child.attrs.name] =
child.attrs.modifiers.column_invisible || false;
}
});
// detect editables list has they behave differently with respect // detect editables list has they behave differently with respect
// to the sorting (changes are not sorted directly) // to the sorting (changes are not sorted directly)
if (mode === 'list' && view.arch.attrs.editable) { if (mode === 'list' && view.arch.attrs.editable) {
@@ -396,6 +405,9 @@ return core.Class.extend({
if (typeof node === 'string') { if (typeof node === 'string') {
return false; return false;
} }
if (!_.isObject(node.attrs.modifiers)) {
node.attrs.modifiers = node.attrs.modifiers ? JSON.parse(node.attrs.modifiers) : {};
}
if (node.tag === 'field') { if (node.tag === 'field') {
fieldsInfo[node.attrs.name] = self._processField(viewType, fieldsInfo[node.attrs.name] = self._processField(viewType,
fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {}); fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {});
@@ -1689,6 +1689,9 @@ var BasicModel = AbstractModel.extend({
if ('invisible' in modifiers) { if ('invisible' in modifiers) {
result.invisible = evalModifier(modifiers.invisible); result.invisible = evalModifier(modifiers.invisible);
} }
if ('column_invisible' in modifiers) {
result.column_invisible = evalModifier(modifiers.column_invisible);
}
if ('readonly' in modifiers) { if ('readonly' in modifiers) {
result.readonly = evalModifier(modifiers.readonly); result.readonly = evalModifier(modifiers.readonly);
} }
@@ -2847,7 +2850,7 @@ var BasicModel = AbstractModel.extend({
_isFieldProtected: function (record, fieldName, viewType) { _isFieldProtected: function (record, fieldName, viewType) {
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
if (fieldInfo) { if (fieldInfo) {
var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); var rawModifiers = fieldInfo.modifiers || {};
var modifiers = this._evalModifiers(record, rawModifiers); var modifiers = this._evalModifiers(record, rawModifiers);
return modifiers.readonly && !fieldInfo.force_save; return modifiers.readonly && !fieldInfo.force_save;
} else { } else {
@@ -2899,7 +2902,7 @@ var BasicModel = AbstractModel.extend({
_.each(element.getFieldNames(), function (fieldName) { _.each(element.getFieldNames(), function (fieldName) {
var field = element.fields[fieldName]; var field = element.fields[fieldName];
var fieldInfo = element.fieldsInfo[element.viewType][fieldName]; var fieldInfo = element.fieldsInfo[element.viewType][fieldName];
var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); var rawModifiers = fieldInfo.modifiers || {};
var modifiers = self._evalModifiers(record, rawModifiers); var modifiers = self._evalModifiers(record, rawModifiers);
if (modifiers.required && !self._isFieldSet(recordData[fieldName], field.type)) { if (modifiers.required && !self._isFieldSet(recordData[fieldName], field.type)) {
isValid = false; isValid = false;
@@ -101,8 +101,9 @@ var BasicRenderer = AbstractRenderer.extend({
// which are configured to always be reset on any change // which are configured to always be reset on any change
var resetWidgets = []; var resetWidgets = [];
_.each(this.allFieldWidgets[id], function (widget) { _.each(this.allFieldWidgets[id], function (widget) {
if (_.contains(fields, widget.name) || widget.resetOnAnyFieldChange) { var fieldChanged = _.contains(fields, widget.name);
defs.push(widget.reset(record, ev)); if (fieldChanged || widget.resetOnAnyFieldChange) {
defs.push(widget.reset(record, ev, fieldChanged));
resetWidgets.push(widget); resetWidgets.push(widget);
} }
}); });
@@ -406,7 +407,7 @@ var BasicRenderer = AbstractRenderer.extend({
// If not, check the modifiers to see if it needs registration // If not, check the modifiers to see if it needs registration
var modifiersData = this._getModifiersData(node); var modifiersData = this._getModifiersData(node);
if (!modifiersData) { if (!modifiersData) {
var modifiers = JSON.parse(node.attrs.modifiers || "{}"); // FIXME parsed multiple times (record switching, no modifiers, ...) var modifiers = node.attrs.modifiers || {};
modifiersData = { modifiersData = {
node: node, node: node,
modifiers: modifiers, modifiers: modifiers,
@@ -38,7 +38,7 @@ function qwebAddIf(node, condition) {
function transformQwebTemplate(node, fields) { function transformQwebTemplate(node, fields) {
// Process modifiers // Process modifiers
if (node.tag && node.attrs.modifiers) { if (node.tag && node.attrs.modifiers) {
var modifiers = JSON.parse(node.attrs.modifiers || "{}"); var modifiers = node.attrs.modifiers || {};
if (modifiers.invisible) { if (modifiers.invisible) {
qwebAddIf(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible))); qwebAddIf(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
} }
@@ -47,16 +47,9 @@ var ListRenderer = BasicRenderer.extend({
*/ */
init: function (parent, state, params) { init: function (parent, state, params) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
var self = this;
this.hasHandle = false; this.hasHandle = false;
this.handleField = 'sequence'; this.handleField = 'sequence';
this.columns = _.reject(this.arch.children, function (c) { this._processColumns(params.columnInvisibleFields || {});
if (c.attrs.widget === 'handle') {
self.hasHandle = true;
self.handleField = c.attrs.name;
}
return !!JSON.parse(c.attrs.modifiers || "{}").tree_invisible;
});
this.rowDecorations = _.chain(this.arch.attrs) this.rowDecorations = _.chain(this.arch.attrs)
.pick(function (value, key) { .pick(function (value, key) {
return DECORATIONS.indexOf(key) >= 0; return DECORATIONS.indexOf(key) >= 0;
@@ -68,6 +61,18 @@ var ListRenderer = BasicRenderer.extend({
this.pagers = []; // instantiated pagers (only for grouped lists) this.pagers = []; // instantiated pagers (only for grouped lists)
}, },
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
updateState: function (state, params) {
this._processColumns(params.columnInvisibleFields || {});
return this._super.apply(this, arguments);
},
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -160,6 +165,29 @@ var ListRenderer = BasicRenderer.extend({
var n = this.columns.length; var n = this.columns.length;
return this.hasSelectors ? n+1 : n; return this.hasSelectors ? n+1 : n;
}, },
/**
* Removes the columns which should be invisible.
*
* @param {Object} columnInvisibleFields contains the column invisible modifier values
*/
_processColumns: function (columnInvisibleFields) {
var self = this;
self.hasHandle = false;
self.handleField = null;
this.columns = _.reject(this.arch.children, function (c) {
var reject = c.attrs.modifiers.column_invisible;
// If there is an evaluated domain for the field we override the node
// attribute to have the evaluated modifier value.
if (c.attrs.name in columnInvisibleFields) {
reject = columnInvisibleFields[c.attrs.name];
}
if (!reject && c.attrs.widget === 'handle') {
self.hasHandle = true;
self.handleField = c.attrs.name;
}
return reject;
});
},
/** /**
* Render a list of <td>, with aggregates if available. It can be displayed * Render a list of <td>, with aggregates if available. It can be displayed
* in the footer, or for each open groups. * in the footer, or for each open groups.
@@ -8351,6 +8351,40 @@ QUnit.module('relational_fields', {
form.destroy(); form.destroy();
}); });
QUnit.test('one2many invisible depends on parent field', function (assert) {
assert.expect(2);
this.data.partner.records[0].p = [2];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<notebook>' +
'<page string="Partner page">' +
'<field name="bar"/>' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
'</tree>' +
'</field>' +
'</page>' +
'</notebook>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('th').length, 2,
"should be 2 columns in the one2many");
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_boolean[name="bar"] input').click();
assert.strictEqual(form.$('th').length, 1,
"should be 1 column after the value change");
form.destroy();
});
}); });
}); });
}); });
@@ -234,7 +234,7 @@ var MockServer = Class.extend({
var pyevalContext = window.py.dict.fromJSON(context || {}); var pyevalContext = window.py.dict.fromJSON(context || {});
var v = pyeval.py_eval(node.attrs[a], {context: pyevalContext}) ? true: false; var v = pyeval.py_eval(node.attrs[a], {context: pyevalContext}) ? true: false;
if (inTreeView && a === 'invisible') { if (inTreeView && a === 'invisible') {
modifiers['tree_invisible'] = v; modifiers['column_invisible'] = v;
} else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) { } else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
modifiers[a] = v; modifiers[a] = v;
} }
@@ -1504,7 +1504,9 @@ QUnit.module('Views', {
default: { default: {
foo: {}, foo: {},
bar: { bar: {
modifiers:"{\"readonly\": true}", modifiers: {
readonly: true,
},
}, },
} }
}; };
@@ -1524,7 +1526,6 @@ QUnit.module('Views', {
return this._super(route, args); return this._super(route, args);
}, },
}); });
model.load(this.params).then(function (resultID) { model.load(this.params).then(function (resultID) {
var record = model.get(resultID); var record = model.get(resultID);
assert.strictEqual(record.data.bar, 2, assert.strictEqual(record.data.bar, 2,
@@ -1568,7 +1569,9 @@ QUnit.module('Views', {
default: { default: {
foo: {}, foo: {},
bar: { bar: {
modifiers:"{\"readonly\": true}", modifiers: {
readonly: true,
},
force_save: true, force_save: true,
}, },
} }
+3 -3
View File
@@ -78,7 +78,7 @@ def transfer_node_to_modifiers(node, modifiers, context=None, in_tree_view=False
if in_tree_view and a == 'invisible': if in_tree_view and a == 'invisible':
# Invisible in a tree view has a specific meaning, make it a # Invisible in a tree view has a specific meaning, make it a
# new key in the modifiers attribute. # new key in the modifiers attribute.
modifiers['tree_invisible'] = v modifiers['column_invisible'] = v
elif v or (a not in modifiers or not isinstance(modifiers[a], list)): elif v or (a not in modifiers or not isinstance(modifiers[a], list)):
# Don't set the attribute to False if a dynamic value was # Don't set the attribute to False if a dynamic value was
# provided (i.e. a domain from attrs or states). # provided (i.e. a domain from attrs or states).
@@ -106,14 +106,14 @@ def setup_modifiers(node, field=None, context=None, in_tree_view=False):
:type node: lxml.etree._Element :type node: lxml.etree._Element
:param dict field: field descriptor corresponding to the provided node :param dict field: field descriptor corresponding to the provided node
:param dict context: execution context used to evaluate node attributes :param dict context: execution context used to evaluate node attributes
:param bool in_tree_view: triggers the ``tree_invisible`` code :param bool in_tree_view: triggers the ``column_invisible`` code
path (separate from ``invisible``): in path (separate from ``invisible``): in
tree view there are two levels of tree view there are two levels of
invisibility, cell content (a column is invisibility, cell content (a column is
present but the cell itself is not present but the cell itself is not
displayed) with ``invisible`` and column displayed) with ``invisible`` and column
invisibility (the whole column is invisibility (the whole column is
hidden) with ``tree_invisible``. hidden) with ``column_invisible``.
:returns: nothing :returns: nothing
""" """
modifiers = {} modifiers = {}