flectra/addons/web/static/tests/helpers/mock_server.js

1066 lines
38 KiB
JavaScript
Raw Normal View History

2018-01-16 11:34:37 +01:00
flectra.define('web.MockServer', function (require) {
"use strict";
var Class = require('web.Class');
var data_manager = require('web.data_manager');
var Domain = require('web.Domain');
var pyeval = require('web.pyeval');
var utils = require('web.utils');
var MockServer = Class.extend({
/**
* @constructor
* @param {Object} data
* @param {Object} options
* @param {integer} [options.logLevel=0]
* @param {string} [options.currentDate] formatted string, default to
* current day
*/
init: function (data, options) {
this.data = data;
for (var modelName in this.data) {
var model = this.data[modelName];
if (!('id' in model.fields)) {
model.fields.id = {string: "ID", type: "integer"};
}
if (!('display_name' in model.fields)) {
model.fields.display_name = {string: "Display Name", type: "char"};
}
if (!('__last_update' in model.fields)) {
model.fields.__last_update = {string: "Last Modified on", type: "datetime"};
}
if (!('name' in model.fields)) {
model.fields.name = {string: "Name", type: "char", default: "name"};
}
model.records = model.records || [];
for (var i = 0; i < model.records.length; i++) {
var record = model.records[i];
this._applyDefaults(model, record);
}
}
// 0 is for no log
// 1 is for short
// 2 is for detailed
this.logLevel = (options && options.logLevel) || 0;
this.currentDate = options.currentDate || moment().format("YYYY-MM-DD");
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* helper: read a string describing an arch, and returns a simulated
* 'field_view_get' call to the server. Calls processViews() of data_manager
* to mimick the real behavior of a call to loadViews().
*
* @param {Object} params
* @param {string|Object} params.arch a string OR a parsed xml document
* @param {string} params.model a model name (that should be in this.data)
* @param {Object} params.toolbar the actions possible in the toolbar
* @param {Object} [params.viewOptions] the view options set in the test (optional)
* @returns {Object} an object with 2 keys: arch and fields
*/
fieldsViewGet: function (params) {
var model = params.model;
var toolbar = params.toolbar;
var viewOptions = params.viewOptions || {};
if (!(model in this.data)) {
throw new Error('Model ' + model + ' was not defined in mock server data');
}
var fields = $.extend(true, {}, this.data[model].fields);
var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context);
var fields_views = {};
fields_views[fvg.type] = fvg;
data_manager.processViews(fields_views, fields);
if (toolbar) {
fvg.toolbar = toolbar;
}
return fields_views[fvg.type];
},
/**
* Simulate a complete RPC call. This is the main method for this class.
*
* This method also log incoming and outgoing data, and stringify/parse data
* to simulate a barrier between the server and the client. It also simulate
* server errors.
*
* @param {string} route
* @param {Object} args
* @returns {Deferred<any>}
* Resolved with the result of the RPC, stringified then parsed.
* If the RPC should fail, the deferred will be rejected with the
* error object, stringified then parsed.
*/
performRpc: function (route, args) {
var logLevel = this.logLevel;
args = JSON.parse(JSON.stringify(args));
if (logLevel === 2) {
console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args);
args = JSON.parse(JSON.stringify(args));
}
return this._performRpc(route, args).then(function (result) {
var resultString = JSON.stringify(result || false);
if (logLevel === 1) {
console.log('Mock: ' + route, JSON.parse(resultString));
} else if (logLevel === 2) {
console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString));
}
return JSON.parse(resultString);
}).fail(function (result) {
var errorString = JSON.stringify(result || false);
if (logLevel === 1) {
console.log('Mock: (ERROR)' + route, JSON.parse(errorString));
} else if (logLevel === 2) {
console.log('%c[rpc] response (error) ' + route, 'color: orange; font-weight: bold;', JSON.parse(errorString));
}
return JSON.parse(errorString);
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Apply the default values when creating an object in the local database.
*
* @private
* @param {Object} model a model object from the local database
* @param {Object} record
*/
_applyDefaults: function (model, record) {
record.display_name = record.display_name || record.name;
for (var fieldName in model.fields) {
if (fieldName === 'id') {
continue;
}
if (!(fieldName in record)) {
if ('default' in model.fields[fieldName]) {
record[fieldName] = model.fields[fieldName].default;
} else if (_.contains(['one2many', 'many2many'], model.fields[fieldName].type)) {
record[fieldName] = [];
} else {
record[fieldName] = false;
}
}
}
},
/**
* helper to evaluate a domain for given field values.
* Currently, this is only a wrapper of the Domain.compute function in
* "web.Domain".
*
* @param {Array} domain
* @param {Object} fieldValues
* @returns {boolean}
*/
_evaluateDomain: function (domain, fieldValues) {
return new Domain(domain).compute(fieldValues);
},
/**
* helper: read a string describing an arch, and returns a simulated
* 'fields_view_get' call to the server.
*
* @private
* @param {string|Object} arch a string OR a parsed xml document
* @param {string} model a model name (that should be in this.data)
* @param {Object} fields
* @param {Object} context
* @returns {Object} an object with 2 keys: arch and fields (the fields
* appearing in the views)
*/
_fieldsViewGet: function (arch, model, fields, context) {
var self = this;
var modifiersNames = ['invisible', 'readonly', 'required'];
var onchanges = this.data[model].onchanges || {};
var fieldNodes = {};
if (typeof arch === 'string') {
var doc = $.parseXML(arch).documentElement;
arch = utils.xml_to_json(doc, true);
}
var inTreeView = (arch.tag === 'tree');
this._traverse(arch, function (node) {
if (typeof node === "string") {
return false;
}
var modifiers = {};
var isField = (node.tag === 'field');
if (isField) {
fieldNodes[node.attrs.name] = node;
// 'transfer_field_to_modifiers' simulation
var field = fields[node.attrs.name];
if (!field) {
throw new Error("Field " + node.attrs.name + " does not exist");
}
var defaultValues = {};
var stateExceptions = {};
_.each(modifiersNames, function (attr) {
stateExceptions[attr] = [];
defaultValues[attr] = !!field[attr];
});
_.each(field['states'] || {}, function (modifs, state) {
_.each(modifs, function (modif) {
if (defaultValues[modif[0]] !== modif[1]) {
stateExceptions[modif[0]].append(state);
}
});
});
_.each(defaultValues, function (defaultValue, attr) {
if (stateExceptions[attr].length) {
modifiers[attr] = [("state", defaultValue ? "not in" : "in", stateExceptions[attr])];
} else {
modifiers[attr] = defaultValue;
}
});
}
// 'transfer_node_to_modifiers' simulation
if (node.attrs.attrs) {
var attrs = pyeval.py_eval(node.attrs.attrs);
_.extend(modifiers, attrs);
delete node.attrs.attrs;
}
if (node.attrs.states) {
if (!modifiers.invisible) {
modifiers.invisible = [];
}
modifiers.invisible.push(["state", "not in", node.attrs.states.split(",")]);
}
_.each(modifiersNames, function (a) {
if (node.attrs[a]) {
var pyevalContext = window.py.dict.fromJSON(context || {});
var v = pyeval.py_eval(node.attrs[a], {context: pyevalContext}) ? true: false;
if (inTreeView && a === 'invisible') {
modifiers['column_invisible'] = v;
} else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) {
modifiers[a] = v;
}
}
});
// 'transfer_modifiers_to_node' simulation
_.each(modifiersNames, function (a) {
if (a in modifiers && (!!modifiers[a] === false || (_.isArray(modifiers[a]) && !modifiers[a].length))) {
delete modifiers[a];
}
});
node.attrs.modifiers = JSON.stringify(modifiers);
return !isField;
});
var relModel, relFields;
_.each(fieldNodes, function (node, name) {
var field = fields[name];
if (field.type === "many2one" || field.type === "many2many") {
node.attrs.can_create = node.attrs.can_create || "true";
node.attrs.can_write = node.attrs.can_write || "true";
}
if (field.type === "one2many" || field.type === "many2many") {
field.views = {};
_.each(node.children, function (children) {
relModel = field.relation;
relFields = $.extend(true, {}, self.data[relModel].fields);
field.views[children.tag] = self._fieldsViewGet(children, relModel,
relFields, context);
});
}
// add onchanges
if (name in onchanges) {
node.attrs.on_change="1";
}
});
return {
arch: arch,
fields: _.pick(fields, _.keys(fieldNodes)),
model: model,
type: arch.tag === 'tree' ? 'list' : arch.tag,
};
},
/**
* Get all records from a model matching a domain. The only difficulty is
* that if we have an 'active' field, we implicitely add active = true in
* the domain.
*
* @private
* @param {string} model a model name
* @param {any[]} domain
* @returns {Object[]} a list of records
*/
_getRecords: function (model, domain) {
if (!_.isArray(domain)) {
throw new Error("MockServer._getRecords: given domain has to be an array.");
}
var self = this;
var records = this.data[model].records;
if ('active' in this.data[model].fields) {
// add ['active', '=', true] to the domain if 'active' is not yet present in domain
var activeInDomain = false;
_.each(domain, function (subdomain) {
activeInDomain = activeInDomain || subdomain[0] === 'active';
});
if (!activeInDomain) {
domain.unshift(['active', '=', true]);
}
}
if (domain.length) {
records = _.filter(records, function (record) {
var fieldValues = _.mapObject(record, function (value) {
return value instanceof Array ? value[0] : value;
});
return self._evaluateDomain(domain, fieldValues);
});
}
return records;
},
/**
* Helper function, to find an available ID. The current algorithm is to add
* all other IDS.
*
* @private
* @param {string} modelName
* @returns {integer} a valid ID (> 0)
*/
_getUnusedID: function (modelName) {
var model = this.data[modelName];
return _.reduce(model.records, function (acc, record){
return acc + record.id;
}, 1);
},
/**
* Simulate a 'copy' operation, so we simply try to duplicate a record in
* memory
*
* @private
* @param {string} modelName
* @param {integer} id the ID of a valid record
* @returns {integer} the ID of the duplicated record
*/
_mockCopy: function (modelName, id) {
var model = this.data[modelName];
var newID = this._getUnusedID(modelName);
var originalRecord = _.findWhere(model.records, {id: id});
var duplicateRecord = _.extend({}, originalRecord, {id: newID});
duplicateRecord.display_name = originalRecord.display_name + ' (copy)';
model.records.push(duplicateRecord);
return newID;
},
/**
* Simulate a 'create' operation. This is basically a 'write' with the
* added work of getting a valid ID and applying default values.
*
* @private
* @param {string} modelName
* @param {Object} values
* @returns {integer}
*/
_mockCreate: function (modelName, values) {
if ('id' in values) {
throw "Cannot create a record with a predefinite id";
}
var model = this.data[modelName];
var id = this._getUnusedID(modelName);
var record = {id: id};
model.records.push(record);
this._applyDefaults(model, values);
this._mockWrite(modelName, [[id], values]);
return id;
},
/**
* Simulate a 'default_get' operation
*
* @private
* @param {string} modelName
* @param {array[]} args a list with a list of fields in the first position
* @param {Object} [kwargs]
* @param {Object} [kwargs.context] the context to eventually read default
* values
* @returns {Object}
*/
_mockDefaultGet: function (modelName, args, kwargs) {
var result = {};
var fields = args[0];
var model = this.data[modelName];
_.each(fields, function (name) {
var field = model.fields[name];
if ('default' in field) {
result[name] = field.default;
}
});
if (kwargs && kwargs.context)
_.each(kwargs.context, function (value, key) {
if ('default_' === key.slice(0, 8)) {
result[key.slice(8)] = value;
}
});
return result;
},
/**
* Simulate a 'field_get' operation
*
* @private
* @param {string} modelName
* @param {any} args
* @returns {Object}
*/
_mockFieldsGet: function (modelName, args) {
var modelFields = this.data[modelName].fields;
// Get only the asked fields (args[0] could be the field names)
if (args[0] && args[0].length) {
modelFields = _.pick.apply(_, [modelFields].concat(args[0]));
}
// Get only the asked attributes (args[1] could be the attribute names)
if (args[1] && args[1].length) {
modelFields = _.mapObject(modelFields, function (field) {
return _.pick.apply(_, [field].concat(args[1]));
});
}
return modelFields;
},
/**
* Simulate a 'name_get' operation
*
* @private
* @param {string} model
* @param {Array} args
* @returns {Array[]} a list of [id, display_name]
*/
_mockNameGet: function (model, args) {
var ids = args[0];
if (!_.isArray(ids)) {
ids = [ids];
}
var records = this.data[model].records;
var names = _.map(ids, function (id) {
return [id, _.findWhere(records, {id: id}).display_name];
});
return names;
},
/**
* Simulate a 'name_create' operation
*
* @private
* @param {string} model
* @param {Array} args
* @returns {Array} a couple [id, name]
*/
_mockNameCreate: function (model, args) {
var name = args[0];
var values = {
name: name,
display_name: name,
};
var id = this._mockCreate(model, values);
return [id, name];
},
/**
* Simulate a 'name_search' operation.
*
* not yet fully implemented (missing: limit, and evaluate operators)
* domain works but only to filter on ids
*
* @private
* @param {string} model
* @param {Array} args
* @param {string} args[0]
* @param {Array} args[1], search domain
* @param {Object} _kwargs
* @returns {Array[]} a list of [id, display_name]
*/
_mockNameSearch: function (model, args, _kwargs) {
var str = args && typeof args[0] === 'string' ? args[0] : _kwargs.name;
var domain = (args && args[1]) || _kwargs.args || [];
var records = this._getRecords(model, domain);
if (str.length) {
records = _.filter(records, function (record) {
return record.display_name.indexOf(str) !== -1;
});
}
var result = _.map(records, function (record) {
return [record.id, record.display_name];
});
if (args.limit) {
return result.slice(0, args.limit);
}
return result;
},
/**
* Simulate an 'onchange' rpc
*
* @private
* @param {string} model
* @param {string|string[]} args a list of field names, or just a field name
* @returns {Object}
*/
_mockOnchange: function (model, args) {
var onchanges = this.data[model].onchanges || {};
var record = args[1];
var fields = args[2];
if (!(fields instanceof Array)) {
fields = [fields];
}
var result = {};
_.each(fields, function (field) {
if (field in onchanges) {
var changes = _.clone(record);
onchanges[field](changes);
_.each(changes, function (value, key) {
if (record[key] !== value) {
result[key] = value;
}
});
}
});
return {value: result};
},
/**
* Simulate a 'read' operation.
*
* @private
* @param {string} model
* @param {Array} args
* @param {Object} _kwargs ignored... is that correct?
* @returns {Object}
*/
_mockRead: function (model, args, _kwargs) {
var self = this;
var ids = args[0];
if (!_.isArray(ids)) {
ids = [ids];
}
var fields = args[1] && args[1].length ? _.uniq(args[1].concat(['id'])) : Object.keys(this.data[model].fields);
var records = _.reduce(ids, function (records, id) {
if (!id) {
throw "mock read: falsy value given as id, would result in an access error in actual server !";
}
var record = _.findWhere(self.data[model].records, {id: id});
return record ? records.concat(record) : records;
}, []);
var results = _.map(records, function (record) {
var result = {};
for (var i = 0; i < fields.length; i++) {
var field = self.data[model].fields[fields[i]];
if (!field) {
// the field doens't exist on the model, so skip it
continue;
}
if (field.type === 'float' ||
field.type === 'integer' ||
field.type === 'monetary') {
// read should return 0 for unset numeric fields
result[fields[i]] = record[fields[i]] || 0;
} else if (field.type === 'many2one') {
var relatedRecord = _.findWhere(self.data[field.relation].records, {
id: record[fields[i]]
});
if (relatedRecord) {
result[fields[i]] =
[record[fields[i]], relatedRecord.display_name];
} else {
result[fields[i]] = false;
}
} else if (field.type === 'one2many' || field.type === 'many2many') {
result[fields[i]] = record[fields[i]] || [];
} else {
result[fields[i]] = record[fields[i]] || false;
}
}
return result;
});
return results;
},
/**
* Simulate a 'read_group' call to the server.
*
* Note: most of the keys in kwargs are still ignored
*
* @private
* @param {string} model a string describing an existing model
* @param {Object} kwargs various options supported by read_group
* @param {string[]} kwargs.groupby fields that we are grouping
* @param {string[]} kwargs.fields fields that we are aggregating
* @param {Array} kwargs.domain the domain used for the read_group
* @param {boolean} kwargs.lazy still mostly ignored
* @param {integer} kwargs.limit ignored as well
* @returns {Object[]}
*/
_mockReadGroup: function (model, kwargs) {
if (!('lazy' in kwargs)) {
kwargs.lazy = true;
}
var self = this;
var fields = this.data[model].fields;
var aggregatedFields = _.clone(kwargs.fields);
var groupBy = [];
if (kwargs.groupby.length) {
groupBy = kwargs.lazy ? [kwargs.groupby[0]] : kwargs.groupby;
}
var records = this._getRecords(model, kwargs.domain);
// if no fields have been given, the server picks all stored fields
if (aggregatedFields.length === 0) {
aggregatedFields = _.keys(this.data[model].fields);
}
// filter out non existing fields
aggregatedFields = _.filter(aggregatedFields, function (name) {
return name in self.data[model].fields;
});
function aggregateFields(group, records) {
var type;
for (var i = 0; i < aggregatedFields.length; i++) {
type = fields[aggregatedFields[i]].type;
if (type === 'float' || type === 'integer') {
group[aggregatedFields[i]] = 0;
for (var j = 0; j < records.length; j++) {
group[aggregatedFields[i]] += records[j][aggregatedFields[i]];
}
}
}
}
function formatValue(groupByField, val) {
var fieldName = groupByField.split(':')[0];
var aggregateFunction = groupByField.split(':')[1] || 'month';
if (fields[fieldName].type === 'date') {
if (!val) {
return false;
} else if (aggregateFunction === 'day') {
return moment(val).format('YYYY-MM-DD');
} else {
return moment(val).format('MMMM YYYY');
}
} else {
return val instanceof Array ? val[0] : (val || false);
}
}
function groupByFunction(record) {
var value = '';
_.each(groupBy, function (groupByField) {
value = (value ? value + ',' : value) + groupByField + '#';
var fieldName = groupByField.split(':')[0];
if (fields[fieldName].type === 'date') {
var aggregateFunction = groupByField.split(':')[1] || 'month';
if (aggregateFunction === 'day') {
value += moment(record[fieldName]).format('YYYY-MM-DD');
} else {
value += moment(record[fieldName]).format('MMMM YYYY');
}
} else {
value += record[groupByField];
}
});
return value;
}
if (!groupBy.length) {
var group = { __count: records.length };
aggregateFields(group, records);
return [group];
}
var groups = _.groupBy(records, groupByFunction);
var result = _.map(groups, function (group) {
var res = {
__domain: kwargs.domain || [],
};
_.each(groupBy, function (groupByField) {
var fieldName = groupByField.split(':')[0];
var val = formatValue(groupByField, group[0][fieldName]);
var field = self.data[model].fields[fieldName];
if (field.type === 'many2one' && !_.isArray(val)) {
var related_record = _.findWhere(self.data[field.relation].records, {
id: val
});
if (related_record) {
res[groupByField] = [val, related_record.display_name];
} else {
res[groupByField] = false;
}
} else {
res[groupByField] = val;
}
res.__domain = [[fieldName, "=", val]].concat(res.__domain);
});
// compute count key to match dumb server logic...
var countKey;
if (kwargs.lazy) {
countKey = groupBy[0].split(':')[0] + "_count";
} else {
countKey = "__count";
}
res[countKey] = group.length;
aggregateFields(res, group);
return res;
});
if (kwargs.orderby) {
// only consider first sorting level
kwargs.orderby = kwargs.orderby.split(',')[0];
var fieldName = kwargs.orderby.split(' ')[0];
var order = kwargs.orderby.split(' ')[1];
result.sort(function (g1, g2) {
if (g1[fieldName] < g2[fieldName]) {
return order === 'ASC' ? -1 : 1;
}
if (g1[fieldName] > g2[fieldName]) {
return order === 'ASC' ? 1 : -1;
}
return 0;
});
}
return result;
},
/**
* Simulates a 'read_progress_bar' operation
*
* @private
* @param {string} model
* @param {Object} kwargs
* @returns {Object[][]}
*/
_mockReadProgressBar: function (model, kwargs) {
var domain = kwargs.domain;
var groupBy = kwargs.groupBy;
var progress_bar = kwargs.progress_bar;
var records = this._getRecords(model, domain || []);
var data = {};
_.each(records, function (record) {
var groupByValue = record[groupBy]; // always technical value here
if (!(groupByValue in data)) {
data[groupByValue] = {};
_.each(progress_bar.colors, function (val, key) {
data[groupByValue][key] = 0;
});
}
var fieldValue = record[progress_bar.field];
if (fieldValue in data[groupByValue]) {
data[groupByValue][fieldValue]++;
}
});
return data;
},
/**
* Simulate a 'search_count' operation
*
* @private
* @param {string} model
* @param {Array} args
* @returns {integer}
*/
_mockSearchCount: function (model, args) {
return this._getRecords(model, args[0]).length;
},
/**
* Simulate a 'search_read' operation on a model
*
* @private
* @param {Object} args
* @param {Array} args.domain
* @param {string} args.model
* @param {Array} [args.fields] defaults to the list of all fields
* @param {integer} [args.limit]
* @param {integer} [args.offset=0]
* @param {string[]} [args.sort]
* @returns {Object}
*/
_mockSearchRead: function (model, args, kwargs) {
var result = this._mockSearchReadController({
model: model,
domain: kwargs.domain || args[0],
fields: kwargs.fields || args[1],
offset: kwargs.offset || args[2],
limit: kwargs.limit || args[3],
order: kwargs.order || args[4],
context: kwargs.context,
});
return result.records;
},
/**
* Simulate a 'search_read' operation, from the controller point of view
*
* @private
* @private
* @param {Object} args
* @param {Array} args.domain
* @param {string} args.model
* @param {Array} [args.fields] defaults to the list of all fields
* @param {integer} [args.limit]
* @param {integer} [args.offset=0]
* @param {string[]} [args.sort]
* @returns {Object}
*/
_mockSearchReadController: function (args) {
var self = this;
var records = this._getRecords(args.model, args.domain || []);
var fields = args.fields || _.keys(this.data[args.model].fields);
var nbRecords = records.length;
var offset = args.offset || 0;
records = records.slice(offset, args.limit ? (offset + args.limit) : nbRecords);
var processedRecords = _.map(records, function (r) {
var result = {};
_.each(_.uniq(fields.concat(['id'])), function (fieldName) {
var field = self.data[args.model].fields[fieldName];
if (field.type === 'many2one') {
var related_record = _.findWhere(self.data[field.relation].records, {
id: r[fieldName]
});
result[fieldName] =
related_record ? [r[fieldName], related_record.display_name] : false;
} else {
result[fieldName] = r[fieldName];
}
});
return result;
});
if (args.sort) {
// deal with sort on multiple fields (i.e. only consider the first)
args.sort = args.sort.split(',')[0];
var fieldName = args.sort.split(' ')[0];
var order = args.sort.split(' ')[1];
processedRecords.sort(function (r1, r2) {
if (r1[fieldName] < r2[fieldName]) {
return order === 'ASC' ? -1 : 1;
}
if (r1[fieldName] > r2[fieldName]) {
return order === 'ASC' ? 1 : -1;
}
return 0;
});
}
var result = {
length: nbRecords,
records: processedRecords,
};
return $.extend(true, {}, result);
},
/**
* Simulate a 'unlink' operation
*
* @private
* @param {string} model
* @param {Array} args
* @returns {boolean} currently, always returns true
*/
_mockUnlink: function (model, args) {
var ids = args[0];
if (!_.isArray(ids)) {
ids = [ids];
}
this.data[model].records = _.reject(this.data[model].records, function (record) {
return _.contains(ids, record.id);
});
// update value of one2many fields pointing to the deleted records
_.each(this.data, function (d) {
var relatedFields = _.pick(d.fields, function (field) {
return field.type === 'one2many' && field.relation === model;
});
_.each(Object.keys(relatedFields), function (relatedField) {
_.each(d.records, function (record) {
record[relatedField] = _.difference(record[relatedField], ids);
});
});
});
return true;
},
/**
* Simulate a 'write' operation
*
* @private
* @param {string} model
* @param {Array} args
* @returns {boolean} currently, always return 'true'
*/
_mockWrite: function (model, args) {
_.each(args[0], this._writeRecord.bind(this, model, args[1]));
return true;
},
/**
* Dispatch a RPC call to the correct helper function
*
* @see performRpc
*
* @private
* @param {string} route
* @param {Object} args
* @returns {Deferred<any>}
* Resolved with the result of the RPC. If the RPC should fail, the
* deferred should either be rejected or the call should throw an
* exception (@see performRpc for error handling).
*/
_performRpc: function (route, args) {
switch (route) {
case '/web/action/load':
return $.when(this._mockLoadAction(args));
case '/web/dataset/search_read':
return $.when(this._mockSearchReadController(args));
}
if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) {
return $.when();
}
switch (args.method) {
case 'copy':
return $.when(this._mockCopy(args.model, args.args[0]));
case 'create':
return $.when(this._mockCreate(args.model, args.args[0]));
case 'default_get':
return $.when(this._mockDefaultGet(args.model, args.args, args.kwargs));
case 'fields_get':
return $.when(this._mockFieldsGet(args.model, args.args));
case 'name_get':
return $.when(this._mockNameGet(args.model, args.args));
case 'name_create':
return $.when(this._mockNameCreate(args.model, args.args));
case 'name_search':
return $.when(this._mockNameSearch(args.model, args.args, args.kwargs));
case 'onchange':
return $.when(this._mockOnchange(args.model, args.args));
case 'read':
return $.when(this._mockRead(args.model, args.args, args.kwargs));
case 'read_group':
return $.when(this._mockReadGroup(args.model, args.kwargs));
case 'read_progress_bar':
return $.when(this._mockReadProgressBar(args.model, args.kwargs));
case 'search_count':
return $.when(this._mockSearchCount(args.model, args.args));
case 'search_read':
return $.when(this._mockSearchRead(args.model, args.args, args.kwargs));
case 'unlink':
return $.when(this._mockUnlink(args.model, args.args));
case 'write':
return $.when(this._mockWrite(args.model, args.args));
}
var model = this.data[args.model];
if (model && typeof model[args.method] === 'function') {
return $.when(this.data[args.model][args.method](args.args, args.kwargs));
}
throw new Error("Unimplemented route: " + route);
},
/**
* helper function: traverse a tree and apply the function f to each of its
* nodes.
*
* Note: this should be abstracted somewhere in web.utils, or in
* web.tree_utils
*
* @param {Object} tree object with a 'children' key, which contains an
* array of trees.
* @param {function} f
*/
_traverse: function (tree, f) {
var self = this;
if (f(tree)) {
_.each(tree.children, function (c) { self._traverse(c, f); });
}
},
/**
* Write a record. The main difficulty is that we have to apply x2many
* commands
*
* @private
* @param {string} model
* @param {Object} values
* @param {integer} id
*/
_writeRecord: function (model, values, id) {
var self = this;
var record = _.findWhere(this.data[model].records, {id: id});
for (var field_changed in values) {
var field = this.data[model].fields[field_changed];
var value = values[field_changed];
if (!field) {
console.warn("Mock: Can't write on field '" + field_changed + "' on model '" + model + "' (field is undefined)");
continue;
}
if (_.contains(['one2many', 'many2many'], field.type)) {
var ids = _.clone(record[field_changed]) || [];
// convert commands
_.each(value, function (command) {
if (command[0] === 0) { // CREATE
var id = self._mockCreate(field.relation, command[2]);
ids.push(id);
} else if (command[0] === 1) { // UPDATE
self._mockWrite(field.relation, [[command[1]], command[2]]);
} else if (command[0] === 2) { // DELETE
ids = _.without(ids, command[1]);
} else if (command[0] === 3) { // FORGET
ids = _.without(ids, command[1]);
} else if (command[0] === 4) { // LINK_TO
if (!_.contains(ids, command[1])) {
ids.push(command[1]);
}
} else if (command[0] === 5) { // DELETE ALL
ids = [];
} else if (command[0] === 6) { // REPLACE WITH
ids = command[2];
} else {
console.error('Command ' + JSON.stringify(command) + ' not supported by the MockServer');
}
});
record[field_changed] = ids;
} else if (field.type === 'many2one') {
if (value) {
var relatedRecord = _.findWhere(this.data[field.relation].records, {
id: value
});
if (!relatedRecord) {
throw "Wrong id for a many2one";
} else {
record[field_changed] = value;
}
} else {
record[field_changed] = false;
}
} else {
record[field_changed] = value;
}
}
},
});
return MockServer;
});