From 17e198153ecfd9c5c32b3b22f43e2f1b8100a1c3 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 16 Jun 2017 15:44:43 +0200 Subject: [FIX] web: BasicModel: store changes in x2many as operations Datapoints of the BasicModel store the data fetched from the server and changes done (not saved yet) by the user. For x2many fields, datapoints are of type 'list' and have a 'res_ids' key containing the ids of all records in the relation. They also have a 'data' key containing datapoints of type 'record', representing the records in the relation that have been fetched (with a limit set to 40 or 80 depending on the subview used). So basically, the length of res_ids may be different than the length of data. Before this rev., the changes in x2manys were saved under the key '_changes' as a duplication of the content of 'data', i.e. same list of datapoints of type 'record', modulo the changes done (it contained the new records added and didn't contained the removed records, whereas the 'data' remained unchanged). This didn't worked at all when there were more records than the limit in the relation. Indeed, when this happened, the commands to send to the server couldn't be generated correctly. This rev. modifies the way changes are stored in those datapoints. Now, each change is stored as an operation. An operation could be of type 'ADD', 'REMOVE', 'REMOVE_ALL' or 'UPDATE'. As before, 'data' remains unchanged and always keeps the datapoints of records that have been fetched (the ones that are currently displayed in the list or kanban subview). This way, commands can now be properly computed. This had a nice side-effect of fixing two other bugs linked to x2manys. First, when used in a context (e.g. {some_key: some_x2many_field}), the value of some_x2many_field (i.e. a list of commands) was always [] if the field was invisible="1" in the view, because it's subrecords weren't fetched, and the generated commands were based on them. Now, they are generated from the list of res_ids in the relation, so it works fine. Second, the x2many records are supposed to be sorted client-side, after being read, and after each edition. This was working in the first case, but not in the second. Now it's working even after a record edition. This rev. also correctly sets the 'parentID' and 'static' keys in datapoints. Finally, we also fixed the pager in x2manys as it was badly displayed (small less tweaks). --- .../web/static/src/js/views/basic/basic_model.js | 447 ++++++++++++++------- 1 file changed, 293 insertions(+), 154 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 716624f368b..7e4c5927503 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -146,8 +146,11 @@ var BasicModel = AbstractModel.extend({ //-------------------------------------------------------------------------- /** - * Add a default record to a list object. This method actually make a new - * record with the _makeDefaultRecord method, then add it to the list object. + * Add a default record to a list object. This method actually makes a new + * record with the _makeDefaultRecord method, then adds it to the list object. + * The default record is added in the data directly. This is meant to be used + * by list or kanban controllers (i.e. not for x2manys in form views, as in + * this case, we store changes as commands). * * @param {string} listID a valid handle for a list object * @param {Object} [options] @@ -156,6 +159,7 @@ var BasicModel = AbstractModel.extend({ * @returns {Deferred} resolves to the id of the new created record */ addDefaultRecord: function (listID, options) { + var self = this; var list = this.localData[listID]; var context = this._getContext(list); @@ -169,12 +173,13 @@ var BasicModel = AbstractModel.extend({ }; return this._makeDefaultRecord(list.model, params).then(function (id) { list.count++; - list._changes = list._changes || list.data.slice(0); if (position === 'top') { - list._changes.unshift(id); + list.data.unshift(id); } else { - list._changes.push(id); + list.data.push(id); } + var record = self.localData[id]; + list._cache[record.res_id] = id; return id; }); }, @@ -331,11 +336,8 @@ var BasicModel = AbstractModel.extend({ if (field.type === 'one2many' || field.type === 'many2many') { if (options.raw) { relDataPoint = this.localData[data[fieldName]]; - var relData = relDataPoint._changes || relDataPoint.data; - var ids = _.map(relData, function (id) { - return self.localData[id].res_id; - }); - data[fieldName] = ids; + relDataPoint = this._applyX2ManyOperations(relDataPoint); + data[fieldName] = relDataPoint.res_ids; } else { data[fieldName] = this.get(data[fieldName]) || []; } @@ -376,25 +378,15 @@ var BasicModel = AbstractModel.extend({ } // here, type === 'list' - var listData, listCount, resIDs; - if (element._changes) { - listData = element._changes; - listCount = listData.length; - resIDs = _.map(listData, function (elemID) { - return self.localData[elemID].res_id; - }); - } else { - listData = element.data; - listCount = element.count; - resIDs = element.res_ids; - } - listData = _.map(listData, function (elemID) { + // apply potential changes (only for x2many lists) + element = this._applyX2ManyOperations(element); + var listData = _.map(element.data, function (elemID) { return self.get(elemID, options); }); var list = { aggregateValues: _.extend({}, element.aggregateValues), context: _.extend({}, element.context), - count: listCount, + count: element.count, data: listData, domain: element.domain.slice(0), fields: element.fields, @@ -409,7 +401,7 @@ var BasicModel = AbstractModel.extend({ offset: element.offset, orderedBy: element.orderedBy, res_id: element.res_id, - res_ids: resIDs, + res_ids: element.res_ids, type: 'list', value: element.value, viewType: element.viewType, @@ -417,6 +409,7 @@ var BasicModel = AbstractModel.extend({ if (element.fieldsInfo) { list.fieldsInfo = element.fieldsInfo; } + this._sortList(list); return list; }, /** @@ -538,6 +531,7 @@ var BasicModel = AbstractModel.extend({ id: id, display_name: display_name, }, + parentID: record.id, }); record.data[field.name] = dataPoint.id; if (display_name === undefined) { @@ -553,6 +547,7 @@ var BasicModel = AbstractModel.extend({ var dpParams = { fieldsInfo: relatedFieldsInfo, modelName: field.relation, + parentID: record.id, static: true, type: 'list', viewType: 'default', @@ -570,9 +565,11 @@ var BasicModel = AbstractModel.extend({ var recordDP = self._makeDataPoint({ data: data, modelName: field.relation, + parentID: dataPoint.id, type: 'record', }); dataPoint.data.push(recordDP.id); + dataPoint._cache[recordDP.res_id] = recordDP.id; }); } else { dpParams.res_ids = field.value; @@ -689,7 +686,16 @@ var BasicModel = AbstractModel.extend({ removeLine: function (elementID) { var record = this.localData[elementID]; var parent = this.localData[record.parentID]; - parent._changes = _.without(parent._changes, elementID); + if (parent.parentID) { + // x2Many case: the new record has been stored in _changes, as a + // command so we remove the command(s) related to that record + parent._changes = _.filter(parent._changes, function (change) { + return change.id !== elementID; + }); + } else { + // main list view case: the new record is in data + parent.data = _.without(parent.data, elementID); + } }, /** * Save a local resource, if needed. This is a complicated operation, @@ -809,13 +815,7 @@ var BasicModel = AbstractModel.extend({ record.fieldsInfo = _.defaults(record.fieldsInfo, viewInfo.fieldsInfo); }, /** - * For list resources, this changes the orderedBy key, then performs the - * sort directly, in javascript. This is used for sorting static datasets, - * such as a one2many in a form view. For dynamic datasets, such as a list - * view, this method will be called, but then the sort will be ignored since - * we will reload data. - * - * @todo don't sort in js when we reload data anyway + * For list resources, this changes the orderedBy key. * * @param {string} list_id id for the list resource * @param {string} fieldName valid field name @@ -837,7 +837,6 @@ var BasicModel = AbstractModel.extend({ }); list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy); } - this._sortList(list); return this; }, /** @@ -893,6 +892,36 @@ var BasicModel = AbstractModel.extend({ // Private //-------------------------------------------------------------------------- + /** + * Add a default record to a list object. This method actually makes a new + * record with the _makeDefaultRecord method, then adds it to the list object + * as a 'ADD' command in its _changes. This is meant to be used x2many lists, + * not by list or kanban controllers. + * + * @private + * @param {string} listID a valid handle for a list object + * @param {Object} [options] + * @param {string} [options.position=top] if the new record should be added + * on top or on bottom of the list + * @returns {Deferred -> string} resolves to the new record id + */ + _addX2ManyDefaultRecord: function (list, options) { + var self = this; + var params = { + context: this._getContext(list), + fields: list.fields, + fieldsInfo: list.fieldsInfo, + parentID: list.id, + viewType: list.viewType, + }; + return this._makeDefaultRecord(list.model, params).then(function (id) { + var position = options && options.position || 'top'; + list._changes.push({operation: 'ADD', id: id, position: position}); + var record = self.localData[id]; + list._cache[record.res_id] = id; + return id; + }); + }, /** * This method is the private version of notifyChanges. Unlike * notifyChanges, it is not protected by a mutex. Every changes from the @@ -1014,12 +1043,13 @@ var BasicModel = AbstractModel.extend({ fields: {}, fieldsInfo: {}, modelName: record.fields[fieldName].relation, + parentID: record.id, }); record._changes[fieldName] = rec.id; }); }, /** - * Applies the result on an onchange RPC on a record. + * Applies the result of an onchange RPC on a record. * * @private * @param {Object} values the result of the onchange RPC (a mapping of @@ -1053,6 +1083,7 @@ var BasicModel = AbstractModel.extend({ context: record.context, data: data, modelName: field.relation, + parentID: record.id, }); id = rec.id; } @@ -1070,12 +1101,15 @@ var BasicModel = AbstractModel.extend({ list = self._makeDataPoint({ fieldsInfo: fieldInfo.fieldsInfo, modelName: field.relation, + parentID: record.id, + static: true, type: 'list', viewtype: fieldInfo.viewType, }); } record._changes[name] = list.id; var shouldLoad = false; + list._changes = list._changes || []; _.each(val, function (command) { if (command[0] === 0 || command[0] === 1) { // CREATE or UPDATE @@ -1091,23 +1125,25 @@ var BasicModel = AbstractModel.extend({ params.res_id = command[1]; } rec = self._makeDataPoint(params); - list._changes.push(rec.id); + list._cache[rec.res_id] = rec.id; + list._changes.push({operation: 'ADD', id: rec.id}); + if (command[0] === 1) { + list._changes.push({operation: 'UPDATE', id: rec.id}); + } defs.push(self._applyOnChange(command[2], rec)); } else if (command[0] === 4) { // LINK TO - list.res_ids.push(command[1]); - list.count++; + // the dataPoint id will be set when the record will be fetched (for now, + // this dataPoint doesn't exist yet) + list._changes.push({operation: 'ADD', id: null, resID: command[1]}); shouldLoad = true; } else if (command[0] === 5) { // DELETE ALL - list._changes = []; + list._changes = [{operation: 'REMOVE_ALL'}]; } }); if (shouldLoad) { - var def = self._readUngroupedList(list).then(function () { - list._changes = list.data; - }); - defs.push(def); + defs.push(self._readUngroupedList(list)); } } else { record._changes[name] = self._parseServerValue(field, val); @@ -1143,9 +1179,9 @@ var BasicModel = AbstractModel.extend({ var field = record.fields[fieldName]; var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; - var rec; + var def, rec; var defs = []; - list._changes = list._changes || list.data.slice(0); + list._changes = list._changes || []; switch (command.operation) { case 'ADD': @@ -1163,8 +1199,8 @@ var BasicModel = AbstractModel.extend({ newRecord.fields = list.fields; newRecord.fieldsInfo = list.fieldsInfo; newRecord.viewType = list.viewType; - list._changes.push(newRecord.id); - this._sortList(list); + list._cache[newRecord.res_id] = newRecord.id; + list._changes.push(command); break; case 'ADD_M2M': // force to use link command instead of create command @@ -1184,8 +1220,8 @@ var BasicModel = AbstractModel.extend({ parentID: list.id, }); list_records[d.id] = rec; - list._changes.push(rec.id); - self._sortList(list); + list._cache[rec.res_id] = rec.id; + list._changes.push({operation: 'ADD', id: rec.id}); }); // read list's records as we only have their ids and optionally their display_name // (we can't use function readUngroupedList because those records are only in the @@ -1194,14 +1230,14 @@ var BasicModel = AbstractModel.extend({ // so that if a record is removed and then re-added, it won't be fetched twice var fieldNames = list.getFieldNames(); if (fieldNames.length) { - var def = this._rpc({ + def = this._rpc({ model: list.model, method: 'read', args: [_.pluck(data, 'id'), fieldNames] }).then(function (records) { _.each(records, function (record) { list_records[record.id].data = record; - self._parseServerData(fieldNames, list.fields, record); + self._parseServerData(fieldNames, list, record); }); return self._fetchX2ManysBatched(list); }); @@ -1209,29 +1245,44 @@ var BasicModel = AbstractModel.extend({ } break; case 'CREATE': - if (command.data) { - defs.push(this.addDefaultRecord(list.id).then(function (id) { + var options = {position: command.position}; + def = this._addX2ManyDefaultRecord(list, options).then(function (id) { + // FIXME: hack for lunch widget, which does useless default_get and onchange + if (command.data) { return self._applyChange(id, command.data); - })); - } else { - defs.push(this.addDefaultRecord(list.id, {position: command.position})); - } + } + }); + defs.push(def); break; case 'UPDATE': - defs.push(this._applyChange(command.id, command.data)); + list._changes.push({operation: 'UPDATE', id: command.id}); + if (command.data) { + defs.push(this._applyChange(command.id, command.data)); + } break; case 'REMOVE': - list._changes = _.difference(list._changes, command.ids); + // filter out existing operations involving the current + // dataPoint, and add a 'REMOVE' operation only if there is + // no 'ADD' operation for that dataPoint, as it would mean + // that the record wasn't in the relation yet + var idsToRemove = command.ids; + list._changes = _.reject(list._changes, function (change) { + var idInCommands = _.contains(command.ids, change.id); + if (idInCommands && change.operation === 'ADD') { + idsToRemove = _.without(idsToRemove, change.id); + } + return idInCommands; + }); + _.each(idsToRemove, function (id) { + list._changes.push({operation: 'REMOVE', id: id}); + }); break; case 'REPLACE_WITH': - // this is certainFly not optimal... and not sure that it is + // this is certainly not optimal... and not sure that it is // correct if some ids are added and some other are removed - var currentData = _.map(list._changes, function (localId) { - return self.localData[localId]; - }); - var currentIds = _.pluck(currentData, 'res_id'); - var newIds = _.difference(command.ids, currentIds); - var removedIds = _.difference(currentIds, command.ids); + list._changes = []; + var newIds = _.difference(command.ids, list.res_ids); + var removedIds = _.difference(list.res_ids, command.ids); var addDef, removedDef, values; if (newIds.length) { values = _.map(newIds, function (id) { @@ -1243,18 +1294,87 @@ var BasicModel = AbstractModel.extend({ }); } if (removedIds.length) { - values = _.filter(currentData, function (dataPoint) { - return _.contains(removedIds, dataPoint.res_id); + var listData = _.map(list.data, function (localId) { + return self.localData[localId]; }); removedDef = this._applyX2ManyChange(record, fieldName, { operation: 'REMOVE', - ids: _.pluck(values, 'id'), + ids: _.map(removedIds, function (resID) { + return _.findWhere(listData, {res_id: resID}).id; + }), }); } return $.when(addDef, removedDef); } - return $.when.apply($, defs); + return $.when.apply($, defs).then(function () { + // ensure to fetch up to 'limit' records (may be useful if records of + // the current page have been removed) + return self._readUngroupedList(list); + }); + }, + /** + * In dataPoints of type list for x2manys, the changes are stored as a list + * of operations (being of type 'ADD', 'REMOVE', 'UPDATE' or 'REMOVE_ALL'). + * This function applies the operation of such a dataPoint without altering + * the original dataPoint. It returns a copy of the dataPoint in which the + * 'count', 'data' and 'res_ids' keys have been updated. + * + * @private + * @param {Object} dataPoint of type list + * @returns {Object} element of type list in which the commands have been + * applied + */ + _applyX2ManyOperations: function (list) { + if (!list.static) { + // this function only applies on x2many lists + return list; + } + var self = this; + list = _.extend({}, list); + list.data = list.data.slice(0); + list.res_ids = list.res_ids.slice(0); + _.each(list._changes, function (change) { + var relRecord; + if (change.id) { + relRecord = self.localData[change.id]; + } + switch(change.operation) { + case 'ADD': + list.count++; + var resID = relRecord ? relRecord.res_id : change.resID; + if (change.position === 'top') { + list.res_ids.unshift(resID); + } else { + list.res_ids.push(resID); + } + break; + case 'REMOVE': + list.count--; + list.res_ids = _.without(list.res_ids, relRecord.res_id); + break; + case 'REMOVE_ALL': + list.count = 0; + list.res_ids = []; + break; + case 'UPDATE': + // nothing to do for UPDATE commands + break; + } + }); + var idsInRange; + if (list.limit) { + idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit); + } else { + idsInRange = list.res_ids; + } + list.data = []; + _.each(idsInRange, function (id) { + if (list._cache[id]) { + list.data.push(list._cache[id]); + } + }); + return list; }, /** * Helper method to build a 'spec', that is a description of all fields in @@ -1368,7 +1488,8 @@ var BasicModel = AbstractModel.extend({ var self = this; var model; var records = []; - var ids = _.map(list._changes || list.data, function (localId) { + list = this._applyX2ManyOperations(list); + var ids = _.map(list.data, function (localId) { var record = self.localData[localId]; var data = record._changes || record.data; var many2oneId = data[fieldName]; @@ -1420,7 +1541,7 @@ var BasicModel = AbstractModel.extend({ record.data = _.extend({}, record.data, result); }) .then(function () { - self._parseServerData(fieldNames, record.fields, record.data); + self._parseServerData(fieldNames, record, record.data); }) .then(function () { return self._fetchX2Manys(record, options).then(function () { @@ -1783,12 +1904,12 @@ var BasicModel = AbstractModel.extend({ var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; var fields = view ? view.fields : fieldInfo.relatedFields; var viewType = view ? view.type : fieldInfo.viewType; - var data = list._changes || list.data; + list = this._applyX2ManyOperations(list); var x2mRecords = []; // step 1: collect ids var ids = []; - _.each(data, function (dataPoint) { + _.each(list.data, function (dataPoint) { var record = self.localData[dataPoint]; if (typeof record.data[fieldName] === 'string') { // in this case, the value is a local ID, which means that the @@ -1803,6 +1924,7 @@ var BasicModel = AbstractModel.extend({ fieldsInfo: fieldsInfo, fields: fields, modelName: field.relation, + parentID: record.id, res_ids: record.data[fieldName], static: true, type: 'list', @@ -1834,25 +1956,20 @@ var BasicModel = AbstractModel.extend({ } return def.then(function (results) { // step 3: assign values to correct datapoints - var dataPoints = _.map(results, function (result) { - return self._makeDataPoint({ - modelName: field.relation, - data: result, - fields: fields, - fieldsInfo: fieldsInfo, - viewType: viewType, - }); - }); - _.each(x2mRecords, function (record) { var m2mList = self.localData[record.data[fieldName]]; - m2mList.data = []; _.each(m2mList.res_ids, function (res_id) { - var dataPoint = _.find(dataPoints, function (d) { - return d.res_id === res_id; + var dataPoint = self._makeDataPoint({ + modelName: field.relation, + data: _.findWhere(results, {id: res_id}), + fields: fields, + fieldsInfo: fieldsInfo, + parentID: m2mList.id, + viewType: viewType, }); m2mList.data.push(dataPoint.id); + m2mList._cache[res_id] = dataPoint.id; }); }); }); @@ -1961,6 +2078,8 @@ var BasicModel = AbstractModel.extend({ * * @param {Object} record * @param {Object} [options] + * @param {string} [options.fieldNames] if given, generates the commands for + * these fields only * @param {boolean} [changesOnly=false] if true, only generates commands for * fields that have changed * @param {boolean} [options.withReadonly=false] if false, doesn't generate @@ -1970,11 +2089,15 @@ var BasicModel = AbstractModel.extend({ _generateX2ManyCommands: function (record, options) { var self = this; options = options || {}; + var fields = record.fields; + if (options.fieldNames) { + fields = _.pick(fields, options.fieldNames); + } var commands = {}; var data = _.extend({}, record.data, record._changes); var type; - for (var fieldName in record.fields) { - type = record.fields[fieldName].type; + for (var fieldName in fields) { + type = fields[fieldName].type; if (type === 'many2many' || type === 'one2many') { commands[fieldName] = []; @@ -1983,24 +2106,31 @@ var BasicModel = AbstractModel.extend({ continue; } var list = this.localData[data[fieldName]]; - if (options.changesOnly && !list._changes) { + if (options.changesOnly && (!list._changes || !list._changes.length)) { // if only changes are requested, skip if there is no change continue; } - var relData = _.map(list._changes || list.data, function (localId) { - return self.localData[localId]; + var oldResIDs = list.res_ids.slice(0); + var relRecordAdded = []; + var relRecordUpdated = []; + _.each(list._changes, function (change) { + if (change.operation === 'ADD') { + relRecordAdded.push(self.localData[change.id]); + } else if (change.operation === 'UPDATE') { + relRecordUpdated.push(self.localData[change.id]); + } }); - var relIds = _.pluck(relData, 'res_id'); + list = this._applyX2ManyOperations(list); if (type === 'many2many' || list._forceM2MLink) { // deliberately generate a single 'replace' command instead // of a 'delete' and a 'link' commands with the exact diff // because 1) performance-wise it doesn't change anything // and 2) to guard against concurrent updates (policy: force // a complete override of the actual value of the m2m) - commands[fieldName].push(x2ManyCommands.replace_with(relIds)); + commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids)); // generate update commands for records that have been // updated (it may happen with editable lists) - _.each(relData, function (relRecord) { + _.each(relRecordUpdated, function (relRecord) { var changes = self._generateChanges(relRecord, options); if (!_.isEmpty(changes)) { var command = x2ManyCommands.update(relRecord.res_id, changes); @@ -2008,29 +2138,29 @@ var BasicModel = AbstractModel.extend({ } }); } else if (type === 'one2many') { - var removedIds = _.difference(list.res_ids, relIds); - var addedIds = _.difference(relIds, list.res_ids); - var keptIds = _.intersection(list.res_ids, relIds); + var removedIds = _.difference(oldResIDs, list.res_ids); + var addedIds = _.difference(list.res_ids, oldResIDs); + var keptIds = _.intersection(oldResIDs, list.res_ids); // the didChange variable keeps track of the fact that at // least one id was updated var didChange = false; var changes, command, relRecord; - for (var i = 0; i < relIds.length; i++) { - if (_.contains(keptIds, relIds[i])) { + for (var i = 0; i < list.res_ids.length; i++) { + if (_.contains(keptIds, list.res_ids[i])) { // this is an id that already existed - relRecord = _.findWhere(relData, {res_id: relIds[i]}); - changes = this._generateChanges(relRecord, options); + relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]}); + changes = relRecord ? this._generateChanges(relRecord, options) : {}; if (!_.isEmpty(changes)) { command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; } else { - command = x2ManyCommands.link_to(relIds[i]); + command = x2ManyCommands.link_to(list.res_ids[i]); } commands[fieldName].push(command); - } else if (_.contains(addedIds, relIds[i])) { + } else if (_.contains(addedIds, list.res_ids[i])) { // this is a new id - relRecord = _.findWhere(relData, {res_id: relIds[i]}); + relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]}); changes = this._generateChanges(relRecord, options); commands[fieldName].push(x2ManyCommands.create(changes)); } @@ -2190,6 +2320,13 @@ var BasicModel = AbstractModel.extend({ var self = this; var relDataPoint; var context = _.extend({}, record.data, record._changes); + + // calls _generateX2ManyCommands for a given field, and returns the array of commands + function _generateX2ManyCommands (fieldName) { + var commands = self._generateX2ManyCommands(record, {fieldNames: [fieldName]}); + return commands[fieldName]; + } + for (var fieldName in context) { var field = record.fields[fieldName]; if (context[fieldName] === null) { @@ -2210,23 +2347,9 @@ var BasicModel = AbstractModel.extend({ continue; } if (field.type === 'one2many' || field.type === 'many2many') { - relDataPoint = this.localData[context[fieldName]]; - var relData = relDataPoint._changes || relDataPoint.data; - var ids = _.map(relData, function (id) { - return self.localData[id].res_id; - }); - - ids.toJSON = function () { - return _.map(relData, function (id) { - var resID = self.localData[id].res_id; - if (typeof resID === 'string') { - var changes = self._generateChanges(self.localData[id]); - return x2ManyCommands.create(changes); - } else { - return x2ManyCommands.link_to(resID); - } - }); - }; + relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]); + var ids = relDataPoint.res_ids.slice(0); + ids.toJSON = _generateX2ManyCommands.bind(null, fieldName); context[fieldName] = ids; } @@ -2285,16 +2408,19 @@ var BasicModel = AbstractModel.extend({ * an onchange. * * @param {string} id id for a local resource of type 'list'. This is - * assumed to be a list element for a one2many + * assumed to be a list element for an x2many * @returns {boolean} */ _isX2ManyValid: function (id) { var self = this; var isValid = true; var element = this.localData[id]; - _.each(element._changes || element.data, function (recordID) { - var recordData = self.get(recordID, {raw: true}).data; - var record = self.localData[recordID]; + _.each(element._changes, function (command) { + if (command.operation === 'REMOVE' || command.operation === 'REMOVE_ALL') { + return; + } + var recordData = self.get(command.id, {raw: true}).data; + var record = self.localData[command.id]; _.each(element.getFieldNames(), function (fieldName) { var field = element.fields[fieldName]; var fieldInfo = element.fieldsInfo[element.viewType][fieldName]; @@ -2477,6 +2603,7 @@ var BasicModel = AbstractModel.extend({ context: record.context, data: {id: result[name]}, modelName: field.relation, + parentID: record.id, }); record._changes[name] = rec.id; } else if (field.type === 'one2many' || field.type === 'many2many') { @@ -2501,6 +2628,7 @@ var BasicModel = AbstractModel.extend({ viewType: viewType, }); record._changes[name] = x2manyList.id; + x2manyList._changes = []; var many2ones = {}; var r; var isCommandList = result[name].length && _.isArray(result[name][0]); @@ -2516,10 +2644,11 @@ var BasicModel = AbstractModel.extend({ context: x2manyList.context, fieldsInfo: fieldsInfo, fields: fields, + parentID: x2manyList.id, viewType: viewType, }); - x2manyList._changes = x2manyList._changes || []; - x2manyList._changes.push(r.id); + x2manyList._changes.push({operation: 'ADD', id: r.id}); + x2manyList._cache[r.res_id] = r.id; // this is necessary so the fields are initialized _.each(r.getFieldNames(), function (fieldName) { @@ -2533,7 +2662,8 @@ var BasicModel = AbstractModel.extend({ var rec = self._makeDataPoint({ context: r.context, modelName: r.fields[name].relation, - data: {id: r._changes[name]} + data: {id: r._changes[name]}, + parentID: r.id, }); r._changes[name] = rec.id; many2ones[name] = true; @@ -2586,14 +2716,15 @@ var BasicModel = AbstractModel.extend({ * parse the server values to javascript framwork * * @param {[string]} fieldNames - * @param {Object} fields - * @param {Object} record + * @param {Object} element the dataPoint used as parent for the created + * dataPoints + * @param {Object} data the server data to parse */ - _parseServerData: function (fieldNames, fields, record) { + _parseServerData: function (fieldNames, element, data) { var self = this; _.each(fieldNames, function (fieldName) { - var field = fields[fieldName]; - var val = record[fieldName]; + var field = element.fields[fieldName]; + var val = data[fieldName]; if (field.type === 'many2one') { // process many2one: split [id, nameget] and create corresponding record if (val !== false) { @@ -2608,14 +2739,15 @@ var BasicModel = AbstractModel.extend({ display_name: val[1], id: val[0], }, + parentID: element.id, }); - record[fieldName] = r.id; + data[fieldName] = r.id; } else { // no value for the many2one - record[fieldName] = false; + data[fieldName] = false; } } else { - record[fieldName] = self._parseServerValue(field, val); + data[fieldName] = self._parseServerValue(field, val); } }); }, @@ -2843,10 +2975,14 @@ var BasicModel = AbstractModel.extend({ var def; var ids = []; var missingIds = []; - var upper_bound = list.limit ? Math.min(list.offset + list.limit, list.count) : list.count; + // generate the current count and res_ids list by applying the changes + var listWithChanges = this._applyX2ManyOperations(list); + var currentCount = listWithChanges.count; + var currentResIDs = listWithChanges.res_ids; + var upper_bound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount; var fieldNames = list.getFieldNames(); for (var i = list.offset; i < upper_bound; i++) { - var id = list.res_ids[i]; + var id = currentResIDs[i]; ids.push(id); if (!list._cache[id]) { missingIds.push(id); @@ -2885,12 +3021,17 @@ var BasicModel = AbstractModel.extend({ }); // add many2one records - self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data); + self._parseServerData(fieldNames, dataPoint, dataPoint.data); list._cache[id] = dataPoint.id; } + // set the dataPoint id in potential 'ADD' operation adding the current record + _.each(list._changes, function (change) { + if (change.operation === 'ADD' && !change.id && change.resID === id) { + change.id = dataPoint.id; + } + }); list.data.push(dataPoint.id); }); - self._sortList(list); return list; }); }, @@ -2947,7 +3088,7 @@ var BasicModel = AbstractModel.extend({ }); // add many2one records - self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data); + self._parseServerData(fieldNames, dataPoint, dataPoint.data); return dataPoint.id; }); if (list.loadMoreOffset) { @@ -2978,22 +3119,20 @@ var BasicModel = AbstractModel.extend({ }, /** * Do a in-memory sort of a list resource data points. This method assumes - * that the list data has already been fetched. Its intended use is for - * static datasets, such as a one2many in a form view. + * that the list data has already been fetched, and that the changes have + * been applied. Its intended use is for static datasets, such as a one2many + * in a form view. * - * @param {Object} list + * @param {Object} list list dataPoint on which changes have been applied */ _sortList: function (list) { - var self = this; if (list.orderedBy.length) { // sort records according to ordered_by[0] var order = list.orderedBy[0]; - var data = list._changes || list.data; - data.sort(function (id1, id2) { - var r1 = self.localData[id1]; - var r2 = self.localData[id2]; - var data1 = r1._changes || r1.data; - var data2 = r2._changes || r2.data; + var data = list.data; + data.sort(function (r1, r2) { + var data1 = r1.data; + var data2 = r2.data; if (data1[order.name] < data2[order.name]) { return order.asc ? -1 : 1; } @@ -3040,8 +3179,8 @@ var BasicModel = AbstractModel.extend({ } } if (element.type === 'list') { - var listData = element._changes || element.data; - _.each(listData, function (elemId) { + element = this._applyX2ManyOperations(element); + _.each(element.data, function (elemId) { var elem = self.localData[elemId]; self._visitChildren(elem, fn); }); -- cgit v1.2.3 From 4881f942cb7cad51b8472425830c8444cfe09d1a Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 23 Jun 2017 14:06:03 +0200 Subject: [FIX] web: BasicModel: correctly eval x2manys in domains When x2many fields have to be evaluated in domains (e.g. ['id', 'in', some_x2many_field]), they must be evaluated as the list of ids in the relation (whereas in contexts, they are evaluated as a list of commands). Before this rev., they were always evaluated as a list of commands. Note that this didn't work neither before the new views. --- .../web/static/src/js/views/basic/basic_model.js | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 7e4c5927503..1001ac73857 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2248,14 +2248,14 @@ var BasicModel = AbstractModel.extend({ if (fieldInfo && fieldInfo.domain) { return Domain.prototype.stringToArray( fieldInfo.domain, - this._getEvalContext(element) + this._getEvalContext(element, true) ); } var fieldParams = element.fields[options.fieldName]; if (fieldParams.domain) { return Domain.prototype.stringToArray( fieldParams.domain, - this._getEvalContext(element) + this._getEvalContext(element, true) ); } return []; @@ -2263,7 +2263,7 @@ var BasicModel = AbstractModel.extend({ return Domain.prototype.stringToArray( element.domain, - this._getEvalContext(element) + this._getEvalContext(element, true) ); }, /** @@ -2275,10 +2275,12 @@ var BasicModel = AbstractModel.extend({ * sure. This allows some domains to use the uid key for example * * @param {Object} element - an element from the localData + * @param {boolean} [forDomain=false] if true, evaluates x2manys as a list of + * ids instead of a list of commands * @returns {Object} */ - _getEvalContext: function (element) { - var evalContext = element.type === 'record' ? this._getRecordEvalContext(element) : {}; + _getEvalContext: function (element, forDomain) { + var evalContext = element.type === 'record' ? this._getRecordEvalContext(element, forDomain) : {}; if (element.parentID) { var parent = this.localData[element.parentID]; @@ -2286,7 +2288,7 @@ var BasicModel = AbstractModel.extend({ parent = this.localData[parent.parentID]; } if (parent.type === 'record') { - evalContext.parent = this._getRecordEvalContext(parent); + evalContext.parent = this._getRecordEvalContext(parent, forDomain); } } return _.extend({ @@ -2314,9 +2316,11 @@ var BasicModel = AbstractModel.extend({ * current values for the record, with commands for x2manys fields. * * @param {Object} record an element of type 'record' + * @param {boolean} [forDomain=false] if true, x2many values are a list of + * ids instead of a list of commands * @returns Object */ - _getRecordEvalContext: function (record) { + _getRecordEvalContext: function (record, forDomain) { var self = this; var relDataPoint; var context = _.extend({}, record.data, record._changes); @@ -2349,7 +2353,11 @@ var BasicModel = AbstractModel.extend({ if (field.type === 'one2many' || field.type === 'many2many') { relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]); var ids = relDataPoint.res_ids.slice(0); - ids.toJSON = _generateX2ManyCommands.bind(null, fieldName); + if (!forDomain) { + // when sent to the server, the x2manys values must be a list + // of commands in a context, but the list of ids in a domain + ids.toJSON = _generateX2ManyCommands.bind(null, fieldName); + } context[fieldName] = ids; } -- cgit v1.2.3 From 9fbed63406702b43e41262da24d68f63feb582f0 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 26 Jun 2017 15:40:09 +0200 Subject: [FIX] web: FieldSelection on m2o: unset value When the value of a many2one field with widget selection was unset, it's value wasn't actually set to false in the BasicModel. It was set to a virtual id like 'virtual_294' instead. This caused a traceback when this value was sent to the server, obviously. This was for example the case of the Register Payment form view (accessible from a customer invoice), when the user selected a payment journal, and then unselected it (the virtual id was then sent to the onchange RPC). --- addons/web/static/src/js/views/basic/basic_model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 1001ac73857..811e3632975 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1001,7 +1001,7 @@ var BasicModel = AbstractModel.extend({ */ _applyMany2OneChange: function (record, fieldName, data) { var self = this; - if (!data) { + if (!data || data.id === false) { record._changes[fieldName] = false; return $.when(); } -- cgit v1.2.3 From ec3bc2eeaf88c957340615ed0da71ac7b91a596b Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Mon, 26 Jun 2017 18:11:56 +0200 Subject: [FIX] web: deal with reference fields The reference fields haven't been correctly implemented with the new views. This commit restores their behaviour. Note that an small improvement has been introduced in this commit. Before saas-16, a `name_get` was done for each record (so 80 if the field was displayed in a full list view), which is not optimal. The calls are now batched by model (so one `name_get` by model appearing in the field values). --- .../web/static/src/js/views/basic/basic_model.js | 195 +++++++++++++++++++-- 1 file changed, 176 insertions(+), 19 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 811e3632975..90fed121f15 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -332,8 +332,16 @@ var BasicModel = AbstractModel.extend({ } else { data[fieldName] = this.get(data[fieldName]) || false; } - } - if (field.type === 'one2many' || field.type === 'many2many') { + } else if (field.type === 'reference') { + if (options.raw) { + relDataPoint = this.localData[data[fieldName]]; + data[fieldName] = relDataPoint ? + relDataPoint.model + ',' + relDataPoint.res_id : + false; + } else { + data[fieldName] = this.get(data[fieldName]) || false; + } + } else if (field.type === 'one2many' || field.type === 'many2many') { if (options.raw) { relDataPoint = this.localData[data[fieldName]]; relDataPoint = this._applyX2ManyOperations(relDataPoint); @@ -946,8 +954,8 @@ var BasicModel = AbstractModel.extend({ field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType)); - } else if (field.type === 'many2one') { - defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName])); + } else if (field.type === 'many2one' || field.type === 'reference') { + defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName])); } else { record._changes[fieldName] = changes[fieldName]; } @@ -989,19 +997,19 @@ var BasicModel = AbstractModel.extend({ }); }, /** - * Apply a many2one onchange. There is a need for this function because the - * server only gives an id when a onchange modifies a many2one field. For - * this reason, we need (sometimes) to do a /name_get to fetch a - * display_name. + * Apply an x2one (either a many2one or a reference field) change. There is + * a need for this function because the server only gives an id when a + * onchange modifies a many2one field. For this reason, we need (sometimes) + * to do a /name_get to fetch a display_name. * * @param {Object} record * @param {string} fieldName * @param {Object} [data] * @returns {Deferred} */ - _applyMany2OneChange: function (record, fieldName, data) { + _applyX2OneChange: function (record, fieldName, data) { var self = this; - if (!data || data.id === false) { + if (!data || !data.id) { record._changes[fieldName] = false; return $.when(); } @@ -1022,12 +1030,16 @@ var BasicModel = AbstractModel.extend({ return $.when(); } var rel_data = _.pick(data, 'id', 'display_name'); + var field = record.fields[fieldName]; + + // the reference field doesn't store its co-model in its field metadata + // but directly in the data (as the co-model isn't fixed) + var coModel = field.type === 'reference' ? data.model : field.relation; var def; if (rel_data.display_name === undefined) { - var field = record.fields[fieldName]; // TODO: refactor this to use _fetchNameGet def = this._rpc({ - model: field.relation, + model: coModel, method: 'name_get', args: [data.id], context: record.context, @@ -1042,7 +1054,7 @@ var BasicModel = AbstractModel.extend({ data: rel_data, fields: {}, fieldsInfo: {}, - modelName: record.fields[fieldName].relation, + modelName: coModel, parentID: record.id, }); record._changes[fieldName] = rec.id; @@ -1239,7 +1251,10 @@ var BasicModel = AbstractModel.extend({ list_records[record.id].data = record; self._parseServerData(fieldNames, list, record); }); - return self._fetchX2ManysBatched(list); + return $.when( + self._fetchX2ManysBatched(list), + self._fetchReferencesBatched(list) + ); }); defs.push(def); } @@ -1544,11 +1559,139 @@ var BasicModel = AbstractModel.extend({ self._parseServerData(fieldNames, record, record.data); }) .then(function () { - return self._fetchX2Manys(record, options).then(function () { + return $.when( + self._fetchX2Manys(record, options), + self._fetchReferences(record) + ).then(function () { return self._postprocess(record, options); }); }); }, + /** + * Fetch the extra data (`name_get`) for the reference fields of the record + * model. + * + * @private + * @param {Object} record + * @returns {Deferred} + */ + _fetchReferences: function (record) { + var self = this; + var defs = []; + var fieldNames = record.getFieldNames(); + _.each(fieldNames, function (fieldName) { + var field = record.fields[fieldName]; + if (field.type === 'reference') { + var value = record.data[fieldName]; + if (value) { + var model = value.split(',')[0]; + var resID = parseInt(value.split(',')[1]); + var def = self._rpc({ + model: model, + method: 'name_get', + args: [resID], + context: record.getContext({fieldName: fieldName}), + }).then(function (result) { + var referenceDp = self._makeDataPoint({ + data: { + id: result[0][0], + display_name: result[0][1], + }, + modelName: model, + parentID: record.id, + }); + record.data[fieldName] = referenceDp.id; + }); + defs.push(def); + } + } + }); + return $.when.apply($, defs); + }, + /** + * Batch requests for one reference field in list (one request by different + * model in the field values). + * + * @see _fetchReferencesBatched + * @param {Object} list + * @param {string} fieldName + * @returns {Deferred} + */ + _fetchReferenceBatched: function (list, fieldName) { + var self = this; + list = this._applyX2ManyOperations(list); + + // collect ids by model + var toFetch = {}; + _.each(list.data, function (dataPoint) { + var record = self.localData[dataPoint]; + var value = record.data[fieldName]; + if (value) { + var model = value.split(',')[0]; + var resID = value.split(',')[1]; + if (!(model in toFetch)) { + toFetch[model] = {}; + } + // there could be multiple datapoints with the same model/resID + if (toFetch[model][resID]) { + toFetch[model][resID].push(dataPoint); + } else { + toFetch[model][resID] = [dataPoint]; + } + } + }); + + var defs = []; + var def; + // one name_get by model + _.each(toFetch, function (datapoints, model) { + var ids = _.map(Object.keys(datapoints), function (id) { return parseInt(id); }); + // we need one parent for the context (they all have the same) + var parent = datapoints[ids[0]][0]; + def = self._rpc({ + model: model, + method: 'name_get', + args: [ids], + context: self.localData[parent].getContext({fieldName: fieldName}), + }).then(function (result) { + _.each(result, function (el) { + var parentIDs = datapoints[el[0]]; + _.each(parentIDs, function (parentID) { + var parent = self.localData[parentID]; + var referenceDp = self._makeDataPoint({ + data: { + id: el[0], + display_name: el[1], + }, + modelName: model, + parentID: parent, + }); + parent.data[fieldName] = referenceDp.id; + }); + }); + }); + defs.push(def); + }); + + return $.when.apply($, defs); + }, + /** + * Batch requests for references for datapoint of type list. + * + * @param {Object} list + * @returns {Deferred} + */ + _fetchReferencesBatched: function (list) { + var defs = []; + var fieldNames = list.getFieldNames(); + for (var i = 0; i < fieldNames.length; i++) { + var field = list.fields[fieldNames[i]]; + if (field.type === 'reference') { + defs.push(this._fetchReferenceBatched(list, fieldNames[i])); + } + } + return $.when.apply($, defs); + }, /** * This method is incorrectly named. It should be named something like * _fetchMany2OneData. @@ -1826,7 +1969,9 @@ var BasicModel = AbstractModel.extend({ def = this._searchReadUngroupedList(list); } return def.then(function () { - return self._fetchX2ManysBatched(list); + return $.when( + self._fetchX2ManysBatched(list), + self._fetchReferencesBatched(list)); }).then(function () { return list; }); @@ -1880,7 +2025,10 @@ var BasicModel = AbstractModel.extend({ record.data[fieldName] = list.id; if (!fieldInfo.__no_fetch) { var def = self._readUngroupedList(list).then(function () { - return self._fetchX2ManysBatched(list); + return $.when( + self._fetchX2ManysBatched(list), + self._fetchReferencesBatched(list) + ); }); defs.push(def); } @@ -2029,6 +2177,7 @@ var BasicModel = AbstractModel.extend({ // process relational fields and handle the null case var type = record.fields[fieldName].type; + var value; if (type === 'one2many' || type === 'many2many') { if (commands[fieldName].length) { // replace localId by commands changes[fieldName] = commands[fieldName]; @@ -2036,8 +2185,13 @@ var BasicModel = AbstractModel.extend({ delete changes[fieldName]; } } else if (type === 'many2one' && fieldName in changes) { - var value = changes[fieldName]; + value = changes[fieldName]; changes[fieldName] = value ? this.localData[value].res_id : false; + } else if (type === 'reference' && fieldName in changes) { + value = changes[fieldName]; + changes[fieldName] = value ? + this.localData[value].model + ',' + this.localData[value].res_id : + false; } else if (changes[fieldName] === null) { changes[fieldName] = false; } @@ -2683,7 +2837,10 @@ var BasicModel = AbstractModel.extend({ x2manyList.res_ids = value[2]; x2manyList.count = x2manyList.res_ids.length; var def = self._readUngroupedList(x2manyList).then(function () { - return self._fetchX2ManysBatched(x2manyList); + return $.when( + self._fetchX2ManysBatched(x2manyList), + self._fetchReferencesBatched(x2manyList) + ); }); defs.push(def); } -- cgit v1.2.3 From b954cd64835eb2c251ba0890293079b7bcebd460 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 28 Jun 2017 13:20:10 +0200 Subject: [FIX] web: BasicModel: send correct context ... when reading and writing on a record (especially inside lists). Before this rev., the context sent when reading a record, and when writing records inside lists (editable list views) wasn't correct. This caused an issue in Sales > Products (variant activated) > open a product with variants > click on variant prices > edit the prices of a variant: the value wasn't saved correctly (because the inverse function requires 'active_id' to be in the context and it wasn't the case). --- addons/web/static/src/js/views/basic/basic_model.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 90fed121f15..69c6c13f9a7 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1546,7 +1546,7 @@ var BasicModel = AbstractModel.extend({ model: record.model, method: 'read', args: [[record.res_id], fieldNames], - context: _.extend({}, record.context, {bin_size: true}), + context: _.extend({}, record.getContext(), {bin_size: true}), }) .then(function (result) { if (result.length === 0) { @@ -2505,8 +2505,13 @@ var BasicModel = AbstractModel.extend({ continue; } if (field.type === 'one2many' || field.type === 'many2many') { - relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]); - var ids = relDataPoint.res_ids.slice(0); + var ids; + if (!context[fieldName] || _.isArray(context[fieldName])) { // no dataPoint created yet + ids = context[fieldName] || []; + } else { + relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]); + ids = relDataPoint.res_ids.slice(0); + } if (!forDomain) { // when sent to the server, the x2manys values must be a list // of commands in a context, but the list of ids in a domain @@ -3159,7 +3164,7 @@ var BasicModel = AbstractModel.extend({ model: list.model, method: 'read', args: [missingIds, fieldNames], - context: {} // FIXME + context: list.getContext(), }); } else { def = $.when(_.map(missingIds, function (id) { @@ -3177,6 +3182,7 @@ var BasicModel = AbstractModel.extend({ dataPoint = self.localData[list._cache[id]]; } else { dataPoint = self._makeDataPoint({ + context: list.context, data: _.findWhere(records, {id: id}), fieldsInfo: list.fieldsInfo, fields: list.fields, @@ -3233,7 +3239,7 @@ var BasicModel = AbstractModel.extend({ route: '/web/dataset/search_read', model: list.model, fields: fieldNames, - context: list.context, + context: list.getContext(), domain: list.domain || [], limit: list.limit, offset: list.loadMoreOffset + list.offset, @@ -3244,6 +3250,7 @@ var BasicModel = AbstractModel.extend({ var ids = _.pluck(result.records, 'id'); var data = _.map(result.records, function (record) { var dataPoint = self._makeDataPoint({ + context: list.context, data: record, fields: list.fields, fieldsInfo: list.fieldsInfo, -- cgit v1.2.3 From 8473bde80e6609d552af8b00396c72a20bd3c47f Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 29 Jun 2017 11:46:46 +0200 Subject: [FIX] web: handle 'domain' key returned by onchanges Onchange RPCs return an object that may contain a 'domain' key. When it does, its value is an object whose keys are field names and values are the new domain for the corresponding field. Before this rev., the 'domain' key was totally ignored by the BasicModel. This feature is used for example in the account.payment form view (go to an open invoice, click on 'Register payment'): the domain of field payment_method_id is updated by the onchanges (e.g. when changing the journal_id). As the domain was ignored, it always displayed all possible payment methods. --- addons/web/static/src/js/views/basic/basic_model.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 69c6c13f9a7..01b2a18091a 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1791,7 +1791,9 @@ var BasicModel = AbstractModel.extend({ var context = record.getContext({fieldName: fieldName}); var domain = record.getDomain({fieldName: fieldName}); if (domain.length) { - var localID = record._changes && record._changes[fieldName] || record.data[fieldName]; + var localID = (record._changes && fieldName in record._changes) ? + record._changes[fieldName] : + record.data[fieldName]; if (localID) { var element = this.localData[localID]; domain = ["|", ["id", "=", element.data.id]].concat(domain); @@ -2994,6 +2996,12 @@ var BasicModel = AbstractModel.extend({ }); record._warning = true; } + if (result.domain) { + var fieldsInfo = record.fieldsInfo[viewType || record.viewType]; + for (var fieldName in result.domain) { + fieldsInfo[fieldName].domain = result.domain[fieldName]; + } + } return self._applyOnChange(result.value, record).then(function () { return result; }); -- cgit v1.2.3 From a43b0464b28ce53074b027469747e379bbb389c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 5 Jul 2017 13:41:24 +0200 Subject: [FIX] web: better handling for readonly modifiers Before this commit, the web client read the readonly in the modifiers, and if no readonly was in the modifiers, it fell back to the field property. But the field property was already taken into account in the modifiers, so this was useless. And worse, in some case, it could cause an issue. For example, a field with readonly=true, and readonly="0" in the arch was considered readonly. --- addons/web/static/src/js/views/basic/basic_model.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 01b2a18091a..61c153e9032 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2539,15 +2539,12 @@ var BasicModel = AbstractModel.extend({ */ _isFieldReadonly: function (record, fieldName, viewType) { var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; - var modifiers; if (fieldInfo) { var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); - modifiers = this._evalModifiers(record, rawModifiers); - } - if (modifiers && 'readonly' in modifiers) { + var modifiers = this._evalModifiers(record, rawModifiers); return modifiers.readonly; } else { - return record.fields[fieldName].readonly; + return false; } }, /** -- cgit v1.2.3 From 1e30b30a8fc08d539cb68fa214226501a3fb96d6 Mon Sep 17 00:00:00 2001 From: Cedric Snauwaert Date: Mon, 3 Jul 2017 14:58:22 +0200 Subject: [FIX] web: onchange with domain should not crash if field is not in view In some cases (onchange in a purchase order), the onchange can return a domain for a field that is not in view. It should not be the case, but it is really simple to protect the web client anyway. --- addons/web/static/src/js/views/basic/basic_model.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src/js/views/basic/basic_model.js') diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 61c153e9032..2f06168fd23 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2996,7 +2996,9 @@ var BasicModel = AbstractModel.extend({ if (result.domain) { var fieldsInfo = record.fieldsInfo[viewType || record.viewType]; for (var fieldName in result.domain) { - fieldsInfo[fieldName].domain = result.domain[fieldName]; + if (fieldsInfo[fieldName]) { + fieldsInfo[fieldName].domain = result.domain[fieldName]; + } } } return self._applyOnChange(result.value, record).then(function () { -- cgit v1.2.3