[REF] web: use a mutex to serialize all operations in basic model
This commit is contained in:
committed by
Aaron Bohy
parent
ef52914ea2
commit
e3f84e592e
@@ -652,50 +652,7 @@ var BasicModel = AbstractModel.extend({
|
||||
* @returns {string[]} list of changed fields
|
||||
*/
|
||||
notifyChanges: function (record_id, changes) {
|
||||
var self = this;
|
||||
var record = this.localData[record_id];
|
||||
var onChangeFields = []; // the fields that have changed and that have an on_change
|
||||
var field;
|
||||
var defs = [];
|
||||
record._changes = record._changes || {};
|
||||
|
||||
// apply changes to local data
|
||||
for (var fieldName in changes) {
|
||||
field = record.fields[fieldName];
|
||||
if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName]));
|
||||
} else if (field.type === 'many2one') {
|
||||
defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName]));
|
||||
} else {
|
||||
record._changes[fieldName] = changes[fieldName];
|
||||
}
|
||||
if (field.onChange) {
|
||||
onChangeFields.push(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
var onchangeDef;
|
||||
if (onChangeFields.length) {
|
||||
onchangeDef = self._applyOnChange(record, onChangeFields).then(function (result) {
|
||||
return _.keys(changes).concat(Object.keys(result && result.value || {}));
|
||||
});
|
||||
} else {
|
||||
onchangeDef = $.Deferred().resolve(_.keys(changes));
|
||||
}
|
||||
return onchangeDef.then(function (fieldNames) {
|
||||
_.each(fieldNames, function (name) {
|
||||
if (record._changes && record._changes[name] === record.data[name]) {
|
||||
delete record._changes[name];
|
||||
}
|
||||
});
|
||||
return self._fetchSpecialData(record).then(function (fieldNames2) {
|
||||
// Return the names of the fields that changed (onchange or
|
||||
// associated special data change)
|
||||
return _.union(fieldNames, fieldNames2);
|
||||
});
|
||||
});
|
||||
});
|
||||
return this.mutex.exec(this._applyChange.bind(this, record_id, changes));
|
||||
},
|
||||
/**
|
||||
* Reload all data for a given resource
|
||||
@@ -769,18 +726,17 @@ var BasicModel = AbstractModel.extend({
|
||||
*/
|
||||
save: function (record_id, options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
if (options.localSave) {
|
||||
var record = self.localData[record_id];
|
||||
if (record._changes) {
|
||||
_.extend(record.data, record._changes);
|
||||
record._changes = null;
|
||||
}
|
||||
return $.when();
|
||||
}
|
||||
var shouldReload = 'reload' in options ? options.reload : true;
|
||||
return this.mutex.exec(function () {
|
||||
options = options || {};
|
||||
var record = self.localData[record_id];
|
||||
if (options.localSave) {
|
||||
if (record._changes) {
|
||||
_.extend(record.data, record._changes);
|
||||
record._changes = null;
|
||||
}
|
||||
return $.when();
|
||||
}
|
||||
var shouldReload = 'reload' in options ? options.reload : true;
|
||||
var method = self.isNew(record_id) ? 'create' : 'write';
|
||||
var changes = _.extend({}, record._changes);
|
||||
var commands = self._generateX2ManyCommands(record);
|
||||
@@ -957,6 +913,61 @@ var BasicModel = AbstractModel.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is the private version of notifyChanges. Unlike
|
||||
* notifyChanges, it is not protected by a mutex. Every changes from the
|
||||
* user to the model go through this method.
|
||||
*
|
||||
* @param {string} recordID
|
||||
* @param {Object} changes
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_applyChange: function (recordID, changes) {
|
||||
var self = this;
|
||||
var record = this.localData[recordID];
|
||||
var onChangeFields = []; // the fields that have changed and that have an on_change
|
||||
var field;
|
||||
var defs = [];
|
||||
record._changes = record._changes || {};
|
||||
|
||||
// apply changes to local data
|
||||
for (var fieldName in changes) {
|
||||
field = record.fields[fieldName];
|
||||
if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName]));
|
||||
} else if (field.type === 'many2one') {
|
||||
defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName]));
|
||||
} else {
|
||||
record._changes[fieldName] = changes[fieldName];
|
||||
}
|
||||
if (field.onChange) {
|
||||
onChangeFields.push(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
var onchangeDef;
|
||||
if (onChangeFields.length) {
|
||||
onchangeDef = self._applyOnChange(record, onChangeFields).then(function (result) {
|
||||
return _.keys(changes).concat(Object.keys(result && result.value || {}));
|
||||
});
|
||||
} else {
|
||||
onchangeDef = $.Deferred().resolve(_.keys(changes));
|
||||
}
|
||||
return onchangeDef.then(function (fieldNames) {
|
||||
_.each(fieldNames, function (name) {
|
||||
if (record._changes && record._changes[name] === record.data[name]) {
|
||||
delete record._changes[name];
|
||||
}
|
||||
});
|
||||
return self._fetchSpecialData(record).then(function (fieldNames2) {
|
||||
// Return the names of the fields that changed (onchange or
|
||||
// associated special data change)
|
||||
return _.union(fieldNames, fieldNames2);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
@@ -1032,102 +1043,100 @@ var BasicModel = AbstractModel.extend({
|
||||
if (fields.length === 1) {
|
||||
fields = fields[0];
|
||||
}
|
||||
return this.mutex.exec(function () {
|
||||
return self._rpc({
|
||||
model: record.model,
|
||||
method: 'onchange',
|
||||
args: [idList, currentData, fields, onchange_spec, context],
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.warning) {
|
||||
self.trigger_up('warning', {
|
||||
message: result.warning.message,
|
||||
title: result.warning.title,
|
||||
type: 'dialog',
|
||||
});
|
||||
}
|
||||
var defs = [];
|
||||
_.each(result.value, function (val, name) {
|
||||
var field = record.fields[name];
|
||||
if (!field) { return; } // ignore changes of unknown fields
|
||||
return self._rpc({
|
||||
model: record.model,
|
||||
method: 'onchange',
|
||||
args: [idList, currentData, fields, onchange_spec, context],
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.warning) {
|
||||
self.trigger_up('warning', {
|
||||
message: result.warning.message,
|
||||
title: result.warning.title,
|
||||
type: 'dialog',
|
||||
});
|
||||
}
|
||||
var defs = [];
|
||||
_.each(result.value, function (val, name) {
|
||||
var field = record.fields[name];
|
||||
if (!field) { return; } // ignore changes of unknown fields
|
||||
|
||||
var rec;
|
||||
if (field.type === 'many2one' ) {
|
||||
// in some case, the value returned by the onchange can
|
||||
// be false (no value), so we need to avoid creating a
|
||||
// local record for that.
|
||||
// FIXME: shouldn't we erase the value in that case?
|
||||
if (val) {
|
||||
// when the value isn't false, it can be either
|
||||
// an array [id, display_name] or just an id.
|
||||
var data = _.isArray(val) ?
|
||||
{id: val[0], display_name: val[1]} :
|
||||
{id: val};
|
||||
rec = self._makeDataPoint({
|
||||
context: context,
|
||||
data: data,
|
||||
modelName: field.relation,
|
||||
});
|
||||
record._changes[name] = rec.id;
|
||||
}
|
||||
} else if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
record._changes = record._changes || {};
|
||||
var listId = record._changes[name] || record.data[name];
|
||||
var list = self.localData[listId];
|
||||
_.each(val, function (command) {
|
||||
if (command[0] === 0 || command[0] === 1) {
|
||||
// CREATE or UPDATE
|
||||
var params = {
|
||||
context: context,
|
||||
fields: list.fields,
|
||||
fieldsInfo: list.fieldsInfo,
|
||||
modelName: list.model,
|
||||
fieldNames: _.keys(command[2]),
|
||||
};
|
||||
if (command[0] === 1) {
|
||||
params.res_id = command[1];
|
||||
}
|
||||
rec = self._makeDataPoint(params);
|
||||
var data = command[2];
|
||||
_.each(rec.fieldNames, function (name) {
|
||||
var field = rec.fields[name];
|
||||
if (field.type === 'many2one') {
|
||||
var r = self._makeDataPoint({
|
||||
modelName: field.relation,
|
||||
data: {
|
||||
id: data[name][0],
|
||||
display_name: data[name][1],
|
||||
},
|
||||
});
|
||||
data[name] = r.id;
|
||||
}
|
||||
});
|
||||
rec._changes = data;
|
||||
for (var f in rec._changes) {
|
||||
rec.data[f] = null;
|
||||
}
|
||||
list._changes.push(rec.id);
|
||||
}
|
||||
if (command[0] === 5) {
|
||||
// DELETE ALL
|
||||
list._changes = [];
|
||||
}
|
||||
var rec;
|
||||
if (field.type === 'many2one' ) {
|
||||
// in some case, the value returned by the onchange can
|
||||
// be false (no value), so we need to avoid creating a
|
||||
// local record for that.
|
||||
// FIXME: shouldn't we erase the value in that case?
|
||||
if (val) {
|
||||
// when the value isn't false, it can be either
|
||||
// an array [id, display_name] or just an id.
|
||||
var data = _.isArray(val) ?
|
||||
{id: val[0], display_name: val[1]} :
|
||||
{id: val};
|
||||
rec = self._makeDataPoint({
|
||||
context: context,
|
||||
data: data,
|
||||
modelName: field.relation,
|
||||
});
|
||||
} else if (field.type === 'date') {
|
||||
// process data: convert into a moment instance
|
||||
record._changes[name] = fieldUtils.parse.date(val);
|
||||
} else if (field.type === 'datetime') {
|
||||
// process datetime: convert into a moment instance
|
||||
record._changes[name] = fieldUtils.parse.datetime(val);
|
||||
} else {
|
||||
record._changes[name] = val;
|
||||
record._changes[name] = rec.id;
|
||||
}
|
||||
});
|
||||
return $.when.apply($, defs).then(function () {
|
||||
return result;
|
||||
});
|
||||
} else if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
record._changes = record._changes || {};
|
||||
var listId = record._changes[name] || record.data[name];
|
||||
var list = self.localData[listId];
|
||||
_.each(val, function (command) {
|
||||
if (command[0] === 0 || command[0] === 1) {
|
||||
// CREATE or UPDATE
|
||||
var params = {
|
||||
context: context,
|
||||
fields: list.fields,
|
||||
fieldsInfo: list.fieldsInfo,
|
||||
modelName: list.model,
|
||||
fieldNames: _.keys(command[2]),
|
||||
};
|
||||
if (command[0] === 1) {
|
||||
params.res_id = command[1];
|
||||
}
|
||||
rec = self._makeDataPoint(params);
|
||||
var data = command[2];
|
||||
_.each(rec.fieldNames, function (name) {
|
||||
var field = rec.fields[name];
|
||||
if (field.type === 'many2one') {
|
||||
var r = self._makeDataPoint({
|
||||
modelName: field.relation,
|
||||
data: {
|
||||
id: data[name][0],
|
||||
display_name: data[name][1],
|
||||
},
|
||||
});
|
||||
data[name] = r.id;
|
||||
}
|
||||
});
|
||||
rec._changes = data;
|
||||
for (var f in rec._changes) {
|
||||
rec.data[f] = null;
|
||||
}
|
||||
list._changes.push(rec.id);
|
||||
}
|
||||
if (command[0] === 5) {
|
||||
// DELETE ALL
|
||||
list._changes = [];
|
||||
}
|
||||
});
|
||||
} else if (field.type === 'date') {
|
||||
// process data: convert into a moment instance
|
||||
record._changes[name] = fieldUtils.parse.date(val);
|
||||
} else if (field.type === 'datetime') {
|
||||
// process datetime: convert into a moment instance
|
||||
record._changes[name] = fieldUtils.parse.datetime(val);
|
||||
} else {
|
||||
record._changes[name] = val;
|
||||
}
|
||||
});
|
||||
});
|
||||
return $.when.apply($, defs).then(function () {
|
||||
return result;
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When an operation is applied to a x2many field, the field widgets
|
||||
@@ -1206,7 +1215,7 @@ var BasicModel = AbstractModel.extend({
|
||||
defs.push(this.addDefaultRecord(list.id));
|
||||
break;
|
||||
case 'UPDATE':
|
||||
defs.push(this.notifyChanges(command.id, command.data));
|
||||
defs.push(this._applyChange(command.id, command.data));
|
||||
break;
|
||||
case 'REMOVE':
|
||||
list._changes = _.without(list._changes, command.id);
|
||||
|
||||
@@ -831,7 +831,7 @@ QUnit.module('relational_fields', {
|
||||
// discard changes
|
||||
form.$buttons.find('.o_form_button_cancel').click();
|
||||
assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
|
||||
"changes shouldn't have been discarding yet, waiting for user confirmation");
|
||||
"changes shouldn't have been discarded yet, waiting for user confirmation");
|
||||
$('.modal .modal-footer .btn-primary').click();
|
||||
assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1',
|
||||
"display name of first record in o2m list should be 'relational record 1'");
|
||||
|
||||
Reference in New Issue
Block a user