[FIX] web: make the web client process the new output format of onchange

The dataset implementation has been improved to handle the result of a `read`
on ids that are already modified in the dataset: the lists `to_create` and
`to_write` are updated properly.
This commit is contained in:
Raphael Collet
2015-08-14 15:09:04 +02:00
parent 9f81c6d651
commit e0cd26376c
3 changed files with 181 additions and 70 deletions
+143 -58
View File
@@ -5,6 +5,7 @@ var core = require('web.core');
var Model = require('web.Model');
var session = require('web.session');
var pyeval = require('web.pyeval');
var utils = require('web.utils');
var Class = core.Class;
var mixins = core.mixins;
@@ -730,6 +731,7 @@ var BufferedDataSet = DataSetStatic.extend({
this.reset_ids([]);
this.last_default_get = {};
this.running_reads = [];
this.mutex = new utils.Mutex();
},
default_get: function(fields, options) {
var self = this;
@@ -737,43 +739,126 @@ var BufferedDataSet = DataSetStatic.extend({
self.last_default_get = res;
});
},
_update_cache: function (id, options) {
// One should call this method after modifying this.from_read,
// this.to_create or this.to_write. It updates this.cache and
// this.from_readonly.
var cached = _.detect(this.cache, function(x) {return x.id === id;});
if (!cached) {
this.cache.push(cached = {id: id});
}
var record = _.detect(this.from_read, function(x) {return x.id === id;});
if (record) {
cached.values = _.clone(record.values);
} else if (record = _.detect(this.to_create, function(x) {return x.id === id;})) {
cached.values = _.clone(record.values);
cached.values = _.extend(cached.values, record.defaults);
} else {
record = {};
}
var record_to_write = _.detect(this.to_write, function(x) {return x.id === id;});
if (record_to_write) {
_.extend(cached.values, record_to_write.values);
}
var record_from_readonly = _.detect(this.from_readonly, function(x) {return x.id === id;});
if (options && options.readonly_fields) {
if (!record_from_readonly) {
this.from_readonly.push(record_from_readonly = {id: id, values: {}});
}
_.extend(record_from_readonly.values, options.readonly_fields);
}
if (record_from_readonly) {
_.extend(cached.values, record_from_readonly.values);
}
return cached;
},
create: function(data, options) {
var cached = {
id:_.uniqueId(this.virtual_id_prefix),
values: _.extend({}, data, (options || {}).readonly_fields || {}),
defaults: this.last_default_get
};
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
this.cache.push(cached);
this.to_create.push(_.clone(cached));
this._update_cache(cached.id, options);
return $.Deferred().resolve(cached.id).promise();
},
write: function (id, data, options) {
var self = this;
var record = _.detect(this.to_create, function(x) {return x.id === id;});
record = record || _.detect(this.to_write, function(x) {return x.id === id;});
var dirty = false;
if (record) {
for (var k in data) {
if (record.values[k] === undefined || record.values[k] !== data[k]) {
dirty = true;
break;
}
var cached = this._update_cache(id, options);
// read missing field to create the to_write diff
var def_read = $.Deferred();
this.mutex.def.then(function () {
if (typeof id !== "number" && id.indexOf(self.virtual_id_prefix) === 0) {
return def_read.resolve();
}
$.extend(record.values, data);
} else {
dirty = true;
record = {id: id, values: data};
self.to_write.push(record);
}
var cached = _.detect(this.cache, function(x) {return x.id === id;});
if (!cached) {
cached = {id: id, values: {}};
this.cache.push(cached);
}
$.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
if (dirty)
this.trigger("dataset_changed", id, data, options);
return $.Deferred().resolve(data).promise();
var data_fields = _.keys(data);
var record_fields = _.keys((_.detect(self.from_read, function(x) {return x.id === id;}) || {}).values);
var fields = _.union(data_fields, record_fields);
if (fields.length !== data_fields.length || fields.length !== record_fields.length) {
self.read_ids(self.ids, fields).always(function () {
def_read.resolve();
});
} else {
def_read.resolve();
}
});
// apply change
var def = $.Deferred();
def_read.then(function () {
self.mutex.exec(function () {
var dirty = false;
var record_to_create = _.detect(self.to_create, function(x) {return x.id === id;});
if (record_to_create) {
// update record in self.to_create
_.each(data, function (v, k) {
if (!_.isEqual(v, cached.values[k])) {
record_to_create.values[k] = v;
dirty = true;
} else {
delete data[k];
}
});
} else {
// check if the record has changed, and update self.to_write accordingly
var record_to_write = _.detect(self.to_write, function(x) {return x.id === id;});
if (!record_to_write) {
self.to_write.push(record_to_write = {id: id, values: {}});
}
var record_from_read = _.detect(self.from_read, function(x) {return x.id === id;});
_.each(data, function (v, k) {
if (!_.isEqual(v, cached.values[k])) {
dirty = true;
if (_.isEqual(v, record_from_read.values[k])) { // clean changes
delete record_to_write.values[k];
} else {
record_to_write.values[k] = v;
}
} else {
delete data[k];
}
});
if (_.isEmpty(record_to_write.values)) {
self.to_write.splice(self.to_write.indexOf(record_to_write), 1);
}
}
self._update_cache(id, options);
if (dirty) {
self.trigger("dataset_changed", id, data, options);
}
return def.resolve(data).promise();
});
});
return def;
},
unlink: function(ids, callback, error_callback) {
var self = this;
@@ -782,6 +867,7 @@ var BufferedDataSet = DataSetStatic.extend({
self.to_delete.push({id: id});
}
});
this.from_read = _.reject(this.from_read, function(x) { return _.include(ids, x.id);});
this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);});
this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);});
this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);});
@@ -789,11 +875,15 @@ var BufferedDataSet = DataSetStatic.extend({
this.trigger("dataset_changed", ids, callback, error_callback);
return $.async_when({result: true}).done(callback);
},
reset_ids: function(ids) {
reset_ids: function(ids, options) {
this.set_ids(ids);
this.to_delete = [];
this.to_create = [];
this.to_write = [];
if (!options || !options.keep_read_data) {
this.from_read = [];
this.from_readonly = [];
}
this.cache = [];
this.delete_all = false;
_.each(_.clone(this.running_reads), function(el) {
@@ -801,23 +891,24 @@ var BufferedDataSet = DataSetStatic.extend({
});
},
read_ids: function (ids, fields, options) {
// read what is necessary from the server to have ids and the given
// fields in this.from_read
var self = this;
var to_get = [];
options = options || {};
_.each(ids, function(id) {
var cached = _.detect(self.cache, function(x) {return x.id === id;});
var created = _.detect(self.to_create, function(x) {return x.id === id;});
if (created) {
_.each(fields, function(x) {if (cached.values[x] === undefined)
cached.values[x] = created.defaults[x] || false;});
} else {
if (!cached || !_.all(fields, function(x) {return cached.values[x] !== undefined;}))
if (typeof id === 'number' || id.indexOf(self.virtual_id_prefix) === -1) {
var record = _.detect(self.from_read, function(x) {return x.id === id;});
if (!record || !_.all(fields, function(x) {return record.values[x] !== undefined;})) {
to_get.push(id);
}
}
});
var return_records = function() {
var records = _.map(ids, function(id) {
var c = _.find(self.cache, function(c) {return c.id === id;});
return _.isUndefined(c) ? c : _.extend({}, c.values, {"id": id});
var cache = _.find(self.cache, function(cache) {return cache.id === id;});
return _.extend({}, cache.values, {"id": id}, options.readonly_fields || {});
});
if (self.debug_mode) {
if (_.include(records, undefined)) {
@@ -863,21 +954,26 @@ var BufferedDataSet = DataSetStatic.extend({
def.always(function() {
self.running_reads = _.without(self.running_reads, def);
});
this._super(to_get, fields, options).then(function() {
def.resolve.apply(def, arguments);
}, function() {
def.reject.apply(def, arguments);
var _super = this._super;
this.mutex.exec(function () {
_super.call(self, to_get, fields, options).then(function() {
def.resolve.apply(def, arguments);
}, function() {
def.reject.apply(def, arguments);
});
return def;
});
return def.then(function(records) {
_.each(records, function(record, index) {
// add information into from_read
var id = to_get[index];
var cached = _.detect(self.cache, function(x) {return x.id === id;});
if (!cached) {
self.cache.push({id: id, values: record});
var record_from_read = _.detect(self.from_read, function(x) {return x.id === id;});
if (record_from_read) {
_.extend(record_from_read.values, record);
} else {
// I assume cache value is prioritary
cached.values = _.defaults(_.clone(cached.values), record);
self.from_read.push({id: id, values: record});
}
self._update_cache(id, options);
});
return return_records();
});
@@ -900,19 +996,8 @@ var BufferedDataSet = DataSetStatic.extend({
// and this breaks the assumptions of other methods (that the data
// for new and altered records is both in the cache and in the to_write
// or to_create collection)
if (_(this.to_create.concat(this.to_write)).find(function (record) {
return record.id === id; })) {
return;
}
for(var i=0, len=this.cache.length; i<len; ++i) {
var record = this.cache[i];
// if record we call the button upon is in the cache
if (record.id === id) {
// evict it so it gets reloaded from server
this.cache.splice(i, 1);
break;
}
}
this.from_read = _.reject(this.from_read, function(x) { return x.id === id;});
this._update_cache(id);
},
call_button: function (method, args) {
this.evict_record(args[0][0]);
@@ -452,9 +452,11 @@ var AbstractManyField = common.AbstractField.extend({
self.trigger("change:commands");
}
});
this.on("change:commands", this, function () {
this.on("change:commands", this, function (options) {
self.has_not_committed_changes = false;
self._inhibit_on_change_flag = !!(options||{})._inhibit_on_change_flag;
self.set({'value': self.dataset.ids.slice()});
self._inhibit_on_change_flag = false;
});
},
@@ -472,11 +474,23 @@ var AbstractManyField = common.AbstractField.extend({
throw new Error("set_value of '"+this.name+"' must receive an list of ids without virtual ids.", ids);
}
if (_.find(ids, function(id) { return typeof(id) !== "number"; } )) {
this.dataset.reset_ids([]);
return this.send_commands(ids);
this.dataset.reset_ids([], {keep_read_data: true});
ids = _.map(ids, function(command) {
if (command instanceof Array) {
return command;
} else {
// command is a dictionary, convert it to a command
if (command.id) {
return COMMANDS.update(command.id, command);
} else {
return COMMANDS.create(command);
}
}
});
return this.send_commands(ids, {'_inhibit_on_change_flag': this._inhibit_on_change_flag});
}
this.dataset.reset_ids(ids);
this._super(ids);
return $.when(this._super(ids));
},
internal_set_value: function(ids) {
@@ -614,7 +628,7 @@ var AbstractManyField = common.AbstractField.extend({
});
this.mutex.def.then(function () {
self.trigger("change:commands");
self.trigger("change:commands", options);
def.resolve(res);
});
return def;
@@ -632,17 +646,27 @@ var AbstractManyField = common.AbstractField.extend({
command_list = [],
id, index, alter_order;
function format_many2one (values) {
values = _.clone(values);
for (var k in values) {
if ((values[k] instanceof Array) && values[k].length === 2 && typeof values[k][0] === "number" && typeof values[k][1] === "string") {
values[k] = values[k][0];
}
}
return values;
}
_.each(this.get('value'), function (id) {
index = starting_ids.indexOf(id);
if (index !== -1) {
starting_ids.splice(index, 1);
}
if (alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;})) {
command_list.push(COMMANDS.create(alter_order.values));
command_list.push(COMMANDS.create(format_many2one(alter_order.values)));
return;
}
if (alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;})) {
command_list.push(COMMANDS.update(alter_order.id, alter_order.values));
command_list.push(COMMANDS.update(alter_order.id, format_many2one(alter_order.values)));
return;
}
if (!is_one2many || self.dataset.delete_all) {
@@ -830,10 +854,12 @@ var FieldX2Many = AbstractManyField.extend({
return (this.viewmanager && this.viewmanager.active_view);
},
set_value: function(value_) {
this._super(value_);
if (this.is_started && !this.no_rerender) {
return this.reload_current_view();
}
var self = this;
this._super(value_).then(function () {
if (self.is_started && !self.no_rerender) {
return self.reload_current_view();
}
});
},
commit_value: function() {
var view = this.get_active_view();
+1 -1
View File
@@ -1144,7 +1144,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{
},
render: function () {
var self = this;
this.$current.empty().append(
this.$current.html(
QWeb.render('ListView.rows', _.extend({
render_cell: function () {
return self.render_cell.apply(self, arguments); }