From 905e01921f3c3ef43ace6fc537d1d8c0d280002c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 5 Mar 2017 10:05:56 +0100 Subject: [REF] web, *: redesign all JS views This commit introduce a full redesign of all JS views. We started basically from scratch. The goal was to unify all the various views under a common framework, to make them testable, to make then usable in different conditions (in studio, or in the frontend), and to make our lives easier. Some important points are: - we introduced new coding guidelines (camelCase, 80 chars width, ...) - we have a brand new testing framework (still QUnit based) - kanban view moved to the web addon - calendar view (formerly web_calender) moved to web as well - the tree view was removed - all new code should be documented We hope that this code is the start of a new era for the Odoo web client, we want to have a high quality codebase, well documented, well tested, well designed. Work done by the framework team: mostly aab, ged, chm, dmo, qsm --- .../web/static/src/js/views/basic/basic_model.js | 1996 ++++++++++++++++++++ 1 file changed, 1996 insertions(+) create mode 100644 addons/web/static/src/js/views/basic/basic_model.js (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 new file mode 100644 index 00000000000..5baf65f1c30 --- /dev/null +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -0,0 +1,1996 @@ +odoo.define('web.BasicModel', function (require) { +"use strict"; + +/** + * Basic Model + * + * This class contains all the logic necessary to communicate between the + * python models and the web client. More specifically, its job is to give a + * simple unified API to the rest of the web client (in particular, the views and + * the field widgets) to query and modify actual records in db. + * + * From a high level perspective, BasicModel is essentially a hashmap with + * integer keys and some data and metadata object as value. Each object in this + * hashmap represents a piece of data, and can be reloaded and modified by using + * its id as key in many methods. + * + * Here is a description of what those data point look like: + * { + * _changes: { + * data: { fieldName: field_values | [commands] }, + * relational_data: { field_names: datapoint } + * }, + * aggregate_values: { fieldName: number} | undefined + * count: number, default=0, + * context: {string:value}, + * data: {string:value} | [{string:value}], + * field_names: [string], + * fields: {string:field description}, + * grouped_by: [strings] | undefined, + * id: number, + * is_group: boolean, + * is_list: boolean, + * is_open: boolean, + * is_record: boolean, + * limit: number | undefined (default=80 for lists) + * model: string, + * offset: number, default=0, + * openGroupByDefault: boolean, + * ordered_by: [order] | undefined, + * res_id: [number]|undefined, + * res_ids: [number] | undefined, + * relational_data: { + * field_names: datapoints + * }, + * static: boolean, * like previous DatasetStatic + * } + * + * Notes: + * - id: is totally unrelated to res_id. id is a web client local concept + * - res_id: if set to a number, it is an actual id for a record in the server + * database. If set to 'virtual_' + number, it is a record not yet saved (so, + * in create mode). + * - res_ids: if set, it represent the context in which the data point is actually + * used. For example, a given record in a form view (opened from a list view) + * might have a res_id = 2 and res_ids = [1,2,3] + * - offset: this is mainly used for pagination. Useful when we need to load + * another page, then we can simply change the offset and reload. + * - count is basically the number of records being manipulated. We can't use + * res_ids, because we might have a very large number of records, or a + * domain, and the res_ids would be the current page, not the full set. + * - model is the actual name of a (odoo) model, such as 'res.partner' + * - fields contains the description of all the fields from the model. Note that + * these properties might have been modified by a view (for example, with + * required=true. So, the fields kind of depends of the context of the + * data point. + * - field_names: list of some relevant field names (string). Usually, it + * denotes the fields present in the view. Only those fields should be + * loaded. + * - _cache and _changes are private, they should not leak out of the basicModel + * and be used by anyone else. + * + * Commands: + * commands are the base commands for x2many (0 -> 6), but with a + * slight twist: each [0, _, values] command is augmented with a virtual id: + * it means that when the command is added in basicmodel, it generates an id + * looking like this: 'virtual_' + number, and uses this id to identify the + * element, so it can be edited later. + */ + +var AbstractModel = require('web.AbstractModel'); +var concurrency = require('web.concurrency'); +var data = require('web.data'); // TODO: remove dependency to data.js +var pyeval = require('web.pyeval'); +var session = require('web.session'); + +var x2ManyCommands = { + // (0, _, {values}) + CREATE: 0, + create: function (values) { + return [x2ManyCommands.CREATE, false, values]; + }, + // (1, id, {values}) + UPDATE: 1, + update: function (id, values) { + return [x2ManyCommands.UPDATE, id, values]; + }, + // (2, id[, _]) + DELETE: 2, + delete: function (id) { + return [x2ManyCommands.DELETE, id, false]; + }, + // (3, id[, _]) removes relation, but not linked record itself + FORGET: 3, + forget: function (id) { + return [x2ManyCommands.FORGET, id, false]; + }, + // (4, id[, _]) + LINK_TO: 4, + link_to: function (id) { + return [x2ManyCommands.LINK_TO, id, false]; + }, + // (5[, _[, _]]) + DELETE_ALL: 5, + delete_all: function () { + return [5, false, false]; + }, + // (6, _, ids) replaces all linked records with provided ids + REPLACE_WITH: 6, + replace_with: function (ids) { + return [6, false, ids]; + } +}; + +var Model = AbstractModel.extend({ + /** + * @override + */ + init: function () { + // this mutex is necessary to make sure some operations are done + // sequentially, for example, an onchange needs to be completed before a + // save is performed. + this.mutex = new concurrency.Mutex(); + + // this dictionary is used to hold all many2one informations for widget + // selection. + this.many2ones = {}; + + // this dictionary is used to hold all many2many informations for widget + // many2many_checkboxes. + this.many2manys = {}; + + this.localData = Object.create(null); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + + /** + * 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. + * + * @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} resolves to the id of the new created record + */ + addDefaultRecord: function (listID, options) { + var list = this.localData[listID]; + var context = this._getContext(list); + + var position = (options && options.position) || 'top'; + var params = { + context: context, + fieldAttrs: list.fieldAttrs, + fieldNames: list.fieldNames, + fields: list.fields, + }; + return this.makeDefaultRecord(list.model, params).then(function(id) { + list.count++; + if (position === 'top') { + list.data.unshift(id); + } else { + list.data.push(id); + } + return id; + }); + }, + /** + * Delete a list of records, then, if a parent_id is given, reload the + * parent. + * + * @todo we should remove the deleted records from the localData + * @todo is it really necessary to reload the data? it seems artificial, and + * the caller should be able to do that himself + * @todo why can't we infer modelName? + * + * @param {string[]} recordIds list of local resources ids. They should all + * be of type 'record', and of the same model + * @param {string} modelName + * @param {string} parent_id + * @returns {Deferred} + */ + deleteRecords: function (recordIds, modelName, parent_id) { + var self = this; + var records = _.map(recordIds, function (id) { return self.localData[id]; }); + return this.performModelRPC(modelName, 'unlink', [_.pluck(records, 'res_id')], { + context: session.user_context, // todo: combine with view context + }).then(function () { + _.each(records, function (record) { + record.res_ids.splice(record.offset, 1)[0]; + record.res_id = record.res_ids[record.offset]; + record.count--; + }); + if (parent_id) { + return self.reload(parent_id); + } + }); + }, + /** + * Discard all changes in a local resource. Basically, it removes + * everything that was stored in a _changes key. + * + * @param {string} id local resource id + */ + discardChanges: function (id) { + var element = this.localData[id]; + this._visitChildren(element, function (elem) { + elem._changes = null; + }); + }, + /** + * Duplicate a record (by calling the 'copy' route) + * + * @param {string} recordID id for a local resource + * @returns {Deferred -> string} resolves to the id of duplicate record + */ + duplicateRecord: function (recordID) { + var self = this; + var record = this.localData[recordID]; + return this.performModelRPC(record.model, 'copy', [record.data.id], { + context: this._getContext(record), + }).then(function (res_id) { + var index = record.res_ids.indexOf(record.res_id); + record.res_ids.splice(index + 1, 0, res_id); + return self.load({ + fieldAttrs: record.fieldAttrs, + fieldNames: record.fieldNames, + fields: record.fields, + modelName: record.model, + res_id: res_id, + res_ids: record.res_ids.slice(0), + }); + }); + }, + /** + * The get method first argument is the handle returned by the load method. + * It is optional (the handle can be undefined). In some case, it makes + * sense to use the handle as a key, for example the BasicModel holds the + * data for various records, each with its local ID. + * + * synchronous method, it assumes that the resource has already been loaded. + * + * @param {string} id local id for the resource + * @param {any} options + * @param {boolean} [options.env=false] if true, will only return res_id + * (if record) or res_ids (if list) + * @param {boolean} [options.raw=false] if true, will not follow relations + * @returns {Object} + */ + get: function (id, options) { + var self = this; + options = options || {}; + + if (!(id in this.localData)) { + return null; + } + + var record = this.localData[id]; + + if (options.env) { + var env = { + ids: record.res_ids ? record.res_ids.slice(0) : [], + }; + if (record.type === 'record') { + env.currentId = this.isNew(record.id) ? undefined : record.res_id; + } + return env; + } + + // do not copy fields: this has a really big performance cost, for views + // with many records and lots of fields (for ex, kanban view contacts) + var fields = record.fields; + delete record.fields; + var element = $.extend(true, {}, record); + record.fields = fields; + element.fields = fields; + + var field, relDataPoint; + if (element.type === 'record') { + // apply changes + _.extend(element.data, element._changes); + + for (var fieldName in element.data) { + field = element.fields[fieldName]; + + // get relational datapoint + if (field.type === 'many2one') { + if (options.raw) { + relDataPoint = this.localData[element.data[fieldName]]; + element.data[fieldName] = relDataPoint ? relDataPoint.res_id : false; + } else { + element.data[fieldName] = this.get(element.data[fieldName]) || false; + } + } + if (field.type === 'one2many' || field.type === 'many2many') { + if (options.raw) { + relDataPoint = this.localData[element.data[fieldName]]; + var ids = _.map(relDataPoint.data, function (id) { + return self.localData[id].res_id; + }); + element.data[fieldName] = ids; + } else { + element.data[fieldName] = this.get(element.data[fieldName]) || []; + } + } + + // weird way to get out many2ones for widget=selection + if (field.__fetch_selection) { + field.__selection_information = self.many2ones[field.relation]; + } + + // weird way to get out many2manys for widget=many2many_checkboxes + if (field.__fetch_many2manys) { + field.__many2many_information = self.many2manys[field.relation]; + } + } + + // this is not strictly necessary, but it hides some implementation + // details, and can easily be removed if needed. + delete element.orderedBy; + delete element.aggregateValues; + delete element.groupedBy; + } + if (element.type === 'list') { + // apply changes if any + if (element._changes) { + element.data = element._changes; + element.count = element._changes.length; + element.res_ids = _.map(element._changes, function (elemID) { + return self.localData[elemID].res_id; + }); + } + // get relational datapoint + element.data = _.map(element.data, function (elemID) { + return self.get(elemID); + }); + } + + delete element._cache; + delete element._changes; + delete element.static; + delete element.parentID; + delete element.rawContext; + return element; + }, + /** + * return true if a record is dirty. A record is considered dirty if it has + * some unsaved changes. A list is considered dirty if its _changes key is + * set to an array of its new datapoints (possibly empty) + * + * @param {string} id id for a local resource + * @returns {boolean} + */ + isDirty: function (id) { + var isDirty = false; + var record = this.localData[id]; + this._visitChildren(record, function (r) { + if (r.type === "record" ? !_.isEmpty(r._changes) : r._changes) { + isDirty = true; + } + }); + return isDirty; + }, + /** + * Check if a record is new, meaning if it is in the process of being + * created and no actual record exists in db. + * + * @param {string} id id for a local resource + * @returns {boolean} + */ + isNew: function (id) { + return typeof this.localData[id].res_id !== 'number'; + }, + /** + * Main entry point, the goal of this method is to fetch and process all + * data (following relations if necessary) for a given record/list. + * + * @todo document all params + * + * @param {any} params + * @param {Object} [params.fieldAttrs={}] contains the attrs of each field + * @param {Array} [params.fieldNames] the name of fields to load, the list + * of all fields by default + * @param {Object} params.fields contains the description of each field + * @param {string} [params.type] 'record' or 'list' + * @returns {Deferred -> string} resolves to a local id, or handle + */ + load: function (params) { + params.type = params.type || (params.res_id !== undefined ? 'record' : 'list'); + + if (params.type === 'record' && params.res_id === undefined) { + return this.makeDefaultRecord(params.modelName, params); + } + var dataPoint = this._makeDataPoint(params); + return this._load(dataPoint).then(function () { + return dataPoint.id; + }); + }, + /** + * When one needs to create a record from scratch, a not so simple process + * needs to be done: + * - call the /default_get route to get default values + * - fetch all relational data + * - apply all onchanges if necessary + * - fetch all relational data + * + * This method tries to optimize the process as much as possible. Also, + * it is quite horrible and should be refactored at some point. + * + * @param {any} params + * @param {string} modelName model name + * @param {Object} params.context the context for the new record + * @param {Object} [params.fieldAttrs={}] contains the attrs of each field + * @param {Array} params.fieldNames the name of fields to load, the list + * of all fields by default + * @param {Object} params.fields contains the description of each field + * @param {Object} params.context the context for the new record + * @returns {Deferred -> string} resolves to the id for the created resource + */ + makeDefaultRecord: function (modelName, params) { + var self = this; + var fields_key = _.without(params.fieldNames, '__last_update'); + + return this.performModelRPC(modelName, 'default_get', [fields_key], { + context: params.context, + }).then(function (result) { + // fill default values for missing fields + for (var i = 0; i < params.fieldNames.length; i++) { + var fieldName = params.fieldNames[i]; + if (!(fieldName in result)) { + var field = params.fields[fieldName]; + if (field.type === 'one2many' || field.type === 'many2many') { + result[fieldName] = []; + } else { + result[fieldName] = null; + } + } + } + + var data = {}; + var record = self._makeDataPoint({ + modelName: modelName, + data: data, + fields: params.fields, + fieldNames: params.fieldNames, + fieldAttrs: params.fieldAttrs, + context: params.context, + res_ids: params.res_ids, + }); + + var defs = []; // FIXME: remove defs? + _.each(params.fieldNames, function (name) { + var field = params.fields[name]; + data[name] = null; + record._changes = record._changes || {}; + if (field.type === 'many2one' && result[name]) { + var rec = self._makeDataPoint({ + context: record.context, + data: {id: result[name]}, + modelName: field.relation, + }); + record._changes[name] = rec.id; + } else if (field.type === 'one2many' || field.type === 'many2many') { + var x2manyList = self._makeDataPoint({ + context: record.context, + fieldAttrs: field.fieldAttrs, + fields: field.relatedFields, + limit: field.limit, + modelName: field.relation, + res_ids: [], + static: true, + type: 'list', + }); + record._changes[name] = x2manyList.id; + var many2ones = {}; + _.each(result[name], function (command) { + if (command[0] === 0) { + // CREATE + var r = self._makeDataPoint({ + modelName: x2manyList.model, + context: x2manyList.context, + fields: field.relatedFields, + fieldAttrs: field.fieldAttrs, + }); + x2manyList._changes.push(r.id); + r._changes = command[2]; + + // this is necessary so the fields are initialized + for (var fieldName in command[2]) { + r.data[fieldName] = null; + } + + for (var name in r._changes) { + if (r.fields[name].type === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: r.fields[name].relation, + data: {id: r._changes[name]} + }); + r._changes[name] = rec.id; + many2ones[name] = true; + } + } + } + if (command[0] === 6) { + // REPLACE_WITH + x2manyList._changes = []; + } + }); + + // fetch many2ones display_name + _.each(_.keys(many2ones), function (name) { + defs.push(self._fetchNameGets(x2manyList, name)); + }); + } else { + record._changes[name] = result[name]; + } + }); + return $.when.apply($, defs) + .then(function () { + var shouldApplyOnchange = false; + var field; + for (var field_name in record.data) { + field = record.fields[field_name]; + if (field.onChange) { + shouldApplyOnchange = true; + } + } + if (shouldApplyOnchange) { + return self._applyOnChange(record, fields_key); + } else { + return $.when(); + } + }) + .then(function () { + return self._fetchRelationalData(record); + }) + .then(function () { + return self._postprocess(record); + }) + .then(function () { + return record.id; + }); + }); + }, + /** + * This helper method is designed to help developpers that want to use a + * field widget outside of a view. In that case, we want a way to create + * data without actually performing a fetch. + * + * @param {string} model name of the model + * @param {Object[]} fields a description of field properties + * @param {Object} [attrs] various field attrs that we want to set + * @returns {string} the local id for the created resource + */ + makeRecord: function (model, fields, attrs) { + var self = this; + var record_fields = {}; + _.each(fields, function (field) { + record_fields[field.name] = _.pick(field, 'type', 'relation', 'domain'); + }); + var record = this._makeDataPoint({ + modelName: model, + fields: record_fields, + fieldAttrs: attrs, + }); + _.each(fields, function (field) { + if ('value' in field) { + if (field.type === 'many2one') { + var id = field.value[0]; + var display_name = field.value[1]; + var relatedRecord = self._makeDataPoint({ + modelName: field.relation, + data: { + id: id, + display_name: display_name, + } + }); + record.data[field.name] = relatedRecord.id; + } else { + record.data[field.name] = field.value; + } + } + }); + return record.id; + }, + /** + * This method just does a name_create call to the server. + * + * @todo Maybe this method should also create a local record... + * + * @param {string} model the name of the model + * @param {string} name the desired name of the created record + * @param {Object} context the context to be passed to the rpc + * @returns {Deferred} + */ + nameCreate: function (model, name, context) { + return this.performModelRPC(model, 'name_create', [name], { + context: _.extend({}, session.user_context, context), + }); + }, + /** + * Do a name_search. I think that it should be done directly by the widget + * @todo remove this + * @deprecated + * + * @param {any} model + * @param {any} name + * @param {any} domain + * @param {any} operator + * @param {any} limit + * @returns + */ + nameSearch: function (model, name, domain, operator, limit) { + // fixme: correctly handle context + return this.performModelRPC(model, 'name_search', [], { + name: name || '', + args: domain || [], + operator: operator || 'ilike', + limit: limit || 0, + }); + }, + /** + * This is an extremely important method. All changes in any field go + * through this method. It will then apply them in the local state, check + * if onchanges needs to be applied, actually do them if necessary, then + * resolves with the list of changed fields. + * + * @param {string} record_id + * @param {Object} changes a map field => new value + * @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 fieldNames; + }); + }); + }, + /** + * Reload all data for a given resource + * + * @param {string} id local id for a resource + * @param {Object} [options] + * @returns {Deferred -> string} resolves to the id of the resource + */ + reload: function (id, options) { + options = options || {}; + var element = this.localData[id]; + + if (element.type === 'record') { + if ('currentId' in options && !options.currentId) { + var params = { + context: element.context, + fieldAttrs: element.fieldAttrs, + fieldNames: element.fieldNames, + fields: element.fields, + }; + return this.makeDefaultRecord(element.model, params); + } + this.discardChanges(id); + } + + if (options.context !== undefined) { + element.context = options.context; + } + if (options.domain !== undefined) { + element.domain = options.domain; + } + if (options.groupBy !== undefined) { + element.groupedBy = options.groupBy; + } + if (options.limit !== undefined) { + element.limit = options.limit; + } + if (options.offset !== undefined) { + this._setOffset(element.id, options.offset); + } + if (options.currentId !== undefined) { + element.res_id = options.currentId; + } + if (options.ids !== undefined) { + element.res_ids = options.ids; + element.count = element.res_ids.length; + } + if (element.type === 'record') { + element.offset = _.indexOf(element.res_ids, element.res_id); + } + var loadOptions = _.pick(options, 'fieldNames'); + return this._load(element, loadOptions).then(function (result) { + return result.id; + }); + }, + /** + * Save a local resource, if needed. This is a complicated operation, + * - it needs to check all changes, + * - generate commands for x2many fields, + * - call the /create or /write method according to the record status + * - After that, it has to reload all data, in case something changed, + * server side. + * + * @param {string} record_id local resource + * @param {Object} [options] + * @param {boolean} [options.reload=true] if true, data will be reloaded + * @returns {Deferred} + */ + save: function (record_id, options) { + var self = this; + options = options || {}; + var shouldReload = 'reload' in options ? options.reload : true; + return this.mutex.exec(function () { + var record = self.localData[record_id]; + var method = self.isNew(record_id) ? 'create' : 'write'; + var changes = _.extend({}, record._changes); + var commands = self._generateX2ManyCommands(record); + for (var fieldName in commands) { + if (commands[fieldName] === null) { + delete changes[fieldName]; + } else { + changes[fieldName] = commands[fieldName]; + } + } + + // replace local ids by actual ids in relational changes + for (fieldName in changes) { + var field = record.fields[fieldName]; + if (field.type === 'many2one') { + if (changes[fieldName]) { + var res_id = self.localData[changes[fieldName]].res_id; + changes[fieldName] = res_id; + } else { + changes[fieldName] = false; + } + } + } + + if (method === 'create') { + _.each(record.fieldNames, function (name) { + changes[name] = changes[name] || record.data[name]; + if (changes[name] === null) { + delete changes[name]; + } + }); + } + // make sure we don't write an undefined id + delete changes.id; + + // in the case of a write, only perform the RPC if there are changes to save + if (method === 'create' || Object.keys(changes).length) { + var args = method === 'write' ? [[record.data.id], changes] : [changes]; + return self.performModelRPC(record.model, method, args, { + context: session.user_context, // todo: combine with view context + }).then(function (id) { + if (method === 'create') { + record.res_id = id; // create returns an id, write returns a boolean + record.data.id = id; + record.offset = record.res_ids.length; + record.res_ids.push(id); + record.count++; + } + // erase changes as they have been applied + record._changes = {}; + + return shouldReload ? self._fetchRecord(record) : false; + }); + } else { + return $.when(record_id); + } + }); + }, + /** + * 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 + * + * @param {string} list_id id for the list resource + * @param {string} fieldName valid field name + * @returns {this} so we can chain call this method + */ + setSort: function (list_id, fieldName) { + var list = this.localData[list_id]; + if (list.type === 'record') { + return; + } + list.offset = 0; + if (list.orderedBy.length === 0) { + list.orderedBy.push({name: fieldName, asc: true}); + } else if (list.orderedBy[0].name === fieldName){ + list.orderedBy[0].asc = !list.orderedBy[0].asc; + } else { + var orderedBy = _.reject(list.orderedBy, function (o) { + return o.name === fieldName; + }); + list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy); + } + this._sortList(list); + return this; + }, + /** + * Toggle the active value of given records (to archive/unarchive them) + * + * @param {Array} recordIDs local ids of the records to (un)archive + * @param {boolean} value false to archive, true to unarchive (value of the active field) + * @param {string} parentID id of the parent resource to reload + * @returns {Deferred -> string} resolves to the parent id + */ + toggleActive: function(recordIDs, value, parentID) { + var self = this; + var parent = this.localData[parentID]; + var resIDs = _.map(recordIDs, function(recordID) { + return self.localData[recordID].res_id; + }); + return this + .performModelRPC(parent.model, 'write', [resIDs, { active: value }]) + .then(this.reload.bind(this, parentID)); + + }, + /** + * Toggle (open/close) a group in a grouped list, then fetches relevant + * data + * + * @param {string} groupId + * @returns {Deferred -> string} resolves to the group id + */ + toggleGroup: function (groupId) { + var group = this.localData[groupId]; + if (group.isOpen) { + group.isOpen = false; + group.data = []; + group.offset = 0; + return $.when(groupId); + } + if (!group.isOpen) { + group.isOpen = true; + var def; + if (group.count > 0) { + def = this._fetchUngroupedList(group); + } + return $.when(def).then(function () { + return groupId; + }); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * 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. + * + * @param {Object} record + * @param {string} fieldName + * @param {Object} [data] + * @returns {Deferred} + */ + _applyMany2OneChange: function (record, fieldName, data) { + var self = this; + if (!data) { + record._changes[fieldName] = false; + return $.when(); + } + var rel_data = _.pick(data, 'id', 'display_name'); + var def; + if (!('display_name' in rel_data)) { + var field = record.fields[fieldName]; + def = this.performModelRPC(field.relation, 'name_get', [data.id], { + context: record.context, + }).then(function (result) { + rel_data.display_name = result[0][1]; + }); + } + return $.when(def).then(function () { + var rec = self._makeDataPoint({ + context: record.context, + data: rel_data, + fields: {}, + modelName: record.fields[fieldName].relation, + }); + record._changes[fieldName] = rec.id; + }); + }, + /** + * This method is quite important: it is supposed to perform the /onchange + * rpc and apply the result. + * + * @param {Object} record + * @param {string[]} fields changed fields + * @returns + */ + _applyOnChange: function (record, fields) { + var self = this; + var onchange_spec = this._buildOnchangeSpecs(record); + var idList = record.data.id ? [record.data.id] : []; + var context = data.build_context(record, session.user_context).eval(); + + var rawData = this.get(record.id, {raw: true}).data; + var commands = this._generateX2ManyCommands(record); + var currentData = _.pick(_.extend(rawData, commands), record.fieldNames); + if (fields.length === 1) { + fields = fields[0]; + } + return this.mutex.exec(function () { + var args = [idList, currentData, fields, onchange_spec, context]; + return self.performModelRPC(record.model, 'onchange', args, {}).then(function (result) { + var defs = []; + _.each(result.value, function (val, name) { + var field = record.fields[name]; + if (!field) { return; } // ignore changes of unknown fields + + 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}; + var rec = self._makeDataPoint({ + context: context, + data: data, + modelName: field.relation, + }); + record._changes[name] = rec.id; + } + } else if (field.type === 'one2many') { + record._changes = record._changes || {}; + var listId = record._changes[name] || record.data[name]; + var list = self.localData[listId]; + var rec; + _.each(val, function (command) { + if (command[0] === 0 || command[0] === 1) { + // CREATE or UPDATE + var params = { + context: context, + fields: list.fields, + fieldAttrs: list.fieldAttrs, + 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 { + record._changes[name] = val; + } + }); + return $.when.apply($, defs).then(function () { + return result; + }); + }); + }); + }, + /** + * When an operation is applied to a x2many field, the field widgets + * generate one (or more) command, which describes the exact operation. + * This method tries to interpret these commands and apply them to the + * localData. + * + * @param {Object} record + * @param {string} fieldName + * @param {Object} command A command object. It should have a 'operation' + * key. For example, it looks like {operation: ADD, id: 'partner_1'} + * @returns {Deferred} + */ + _applyX2ManyChange: function (record, fieldName, command) { + var self = this; + var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; + var field = record.fields[fieldName]; + var rec; + var defs = []; + list._changes = list._changes || list.data; + + switch (command.operation) { + case 'ADD': + // for now, we are in the context of a one2many field + // the command should look like this: + // { operation: 'ADD', id: localID } + // The corresponding record may contain value for fields that + // are unknown in the list (e.g. fields that are in the + // subrecord form view but not in the kanban or list view), so + // to ensure that onchanges are correctly handled, we extend the + // list's fields with those in the created record + var newRecord = this.localData[command.id]; + _.defaults(list.fields, newRecord.fields); + newRecord.fields = list.fields; + list._changes.push(newRecord.id); + break; + case 'ADD_M2M': + // handle multiple add: command[2] may be a dict of values (1 + // record added) or an array of dict of values + var data = _.isArray(command.ids) ? command.ids : [command.ids]; + var list_records = {}; + _.each(data, function (d) { + rec = self._makeDataPoint({ + context: record.context, + modelName: field.relation, + fields: field.relatedFields, + fieldAttrs: field.fieldAttrs, + res_id: d.id, + }); + list_records[d.id] = rec; + list._changes.push(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 + // _changes so this is a very specific case) + // this could be optimized by registering the fetched records in the list's _cache + // so that if a record is removed and then re-added, it won't be fetched twice + var def = this + .performModelRPC(list.model, 'read', [_.pluck(data, 'id'), list.fieldNames], {}) + .then(function (records) { + _.each(records, function (record) { + list_records[record.id].data = record; + }); + }); + defs.push(def); + break; + case 'CREATE': + defs.push(this.addDefaultRecord(list.id)); + break; + case 'UPDATE': + var changedRecord = this.localData[command.id]; + changedRecord._changes = changedRecord._changes || {}; + _.extend(changedRecord._changes, _.pick(command.data, list.fieldNames)); + break; + case 'REMOVE': + list._changes = _.without(list._changes, command.id); + break; + case 'REPLACE_WITH': + // this is certainly not optimal... and not sure that it is + // correct if some ids are added and some other are removed + var currentIds = _.map(list._changes, function (localId) { + return self.localData[localId].res_id; + }); + var newIds = _.difference(command.ids, currentIds); + var removedIds = _.difference(currentIds, command.ids); + var addDef, removedDef; + if (newIds.length) { + var values = _.map(newIds, function (id) { + return {id: id}; + }); + + addDef = this._applyX2ManyChange(record, fieldName, { + operation: 'ADD_M2M', + ids: values + }); + } + if (removedIds.length) { + removedDef = this._applyX2ManyChange(record, fieldName, { + operation: 'REMOVE', + ids: removedIds + }); + } + return $.when(addDef, removedDef); + } + + return $.when.apply($, defs); + }, + /** + * Helper method to build a 'spec', that is a description of all fields in + * the view that have a onchange defined on them. + * + * An onchange spec is necessary as an argument to the /onchange route. It + * looks like this: { field: "1", anotherField: "", relation.subField: "1"} + * + * @see _applyOnChange + * + * @param {Object} record resource object of type 'record' + * @returns {Object} an onchange spec + */ + _buildOnchangeSpecs: function (record) { + // TODO: replace this function by some generic tree function in utils + var specs = {}; + _.each(record.fieldNames, function (name) { + var field = record.fields[name]; + specs[name] = (field.onChange) || ""; + _.each(field.views, function (view) { + _.each(view.fields, function (field, subname) { + specs[name + '.' + subname] = (field.onChange) || ""; + }); + }); + }); + return specs; + }, + /** + * Fetch all name_gets for the many2ones in a group + * + * @param {Object} group a valid resource object + * @returns {Deferred} + */ + _fetchMany2OneGroup: function (group) { + var ids = _.uniq(_.pluck(group, 'res_id')); + + return this.performModelRPC(group[0].model, 'name_get', [ids], { + context: group[0].context, + }).then(function (name_gets) { + _.each(group, function (record) { + var nameGet = _.find(name_gets, function (n) { return n[0] === record.res_id;}); + record.data.display_name = nameGet[1]; + }); + }); + }, + _fetchNameGets: function (list, fieldName) { + var self = this; + var model; + var records = []; + var ids = _.map(list._changes || list.data, function (localId) { + var record = self.localData[localId]; + var data = record._changes || record.data; + var many2oneId = data[fieldName]; + var many2oneRecord = self.localData[many2oneId]; + records.push(many2oneRecord); + model = many2oneRecord.model; + return many2oneRecord.res_id; + }); + return this.performModelRPC(model, 'name_get', [ids], { + context: list.context, + }).then(function (name_gets) { + for (var i = 0; i < name_gets.length; i++) { + records[i].data.display_name = name_gets[i][1]; + } + }); + }, + /** + * For a given resource of type 'record', fetch all data. + * + * @param {Object} record local resource + * @param {string[]} [fieldNames] the list of fields to fetch. If not given, + * fetch all the fields in record.fieldNames (+ display_name) + * @returns {Deferred -> Object} resolves to the record + */ + _fetchRecord: function (record, fieldNames) { + var self = this; + fieldNames = fieldNames || _.uniq(record.fieldNames.concat(['display_name'])); + var oldData = record.data; + return this.performModelRPC(record.model, 'read', [record.res_id, fieldNames], { + // context: { 'bin_size': true }, // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field + }).then(function (result) { + result = result[0]; + record.data = _.extend({}, record.data, result); + return self._postprocess(record); + }).then(function (record) { + // process many2one: split [id, nameget] and create corresponding record + _.each(fieldNames, function (name) { + var field = record.fields[name]; + var val = record.data[name]; + if (field.type === 'many2one') { + if (val !== false) { + // the many2one value is of the form [id, display_name] + var r = self._makeDataPoint({ + modelName: field.relation, + data: { + display_name: val[1], + id: val[0], + }, + }); + record.data[name] = r.id; + } else { + // no value for the many2one + record.data[name] = false; + } + } + }); + }).then(function () { + return self._fetchX2Manys(record, oldData, fieldNames).then(function () { + return record; + }); + }); + }, + /** + * This method is incorrectly named. It should be named something like + * _fetchMany2OneData. + * + * For a given record, this method fetches all many2ones information, + * batching the requests if possible (for example, if 3 many2ones are in + * relation on the same model, then we can probably fetch them in one rpc) + * + * This method is currently only called by makeDefaultRecord, it should be + * called by the onchange methods at some point. + * + * @todo fix bug: returns a list of deferred, not a deferred + * + * @param {Object} record a valid resource object + * @returns {Deferred} + */ + _fetchRelationalData: function (record) { + var self = this; + var toBeFetched = []; + + // find all many2one related records to be fetched + _.each(record.fieldNames, function (name) { + var field = record.fields[name]; + if (field.type === 'many2one') { + var localId = (record._changes && record._changes[name]) || record.data[name]; + var relatedRecord = self.localData[localId]; + if (!relatedRecord) { + return; + } + toBeFetched.push(relatedRecord); + } + }); + + // group them by model and context. Using the context as key is + // necessary to make sure the correct context is used for the rpc; + var groups = _.groupBy(toBeFetched, function (rec) { + return [rec.model, JSON.stringify(rec.context)].join(); + }); + + return $.when.apply($, _.map(groups, this._fetchMany2OneGroup.bind(this))); + }, + /** + * Fetch all data in a ungrouped list + * + * @param {Object} list a valid resource object + * @returns {Deferred -> Object} resolves to the fecthed list + */ + _fetchUngroupedList: function (list) { + var self = this; + var def; + if (list.static) { + def = this._readUngroupedList(list); + } else { + def = this._searchReadUngroupedList(list); + } + return def.then(function () { + return self._fetchX2ManysBatched(list); + }).then(function () { + return list; + }); + }, + /** + * X2Manys have to be fetched by separate rpcs (their data are stored on + * different models). This method takes a record, look at its x2many fields, + * then, if necessary, create a local resource and fetch the corresponding + * data. + * + * It also tries to reuse data, if it can find an existing list, to prevent + * useless rpcs. + * + * @param {Object} record local resource + * @param {Object} oldData the data fetched previously + * @param {string[]} [fieldNames] list of fields to fetch + * @returns {Deferred} + */ + _fetchX2Manys: function (record, oldData, fieldNames) { + var self = this; + var defs = []; + fieldNames = fieldNames || record.fieldNames; + _.each(fieldNames, function (fieldName) { + var field = record.fields[fieldName]; + if (field.type === 'one2many' || field.type === 'many2many') { + var list; + + // a previously loaded list exists + if (oldData[fieldName]) { + list = self.localData[oldData[fieldName]]; + list.res_ids = record.data[fieldName]; + list.count = list.res_ids.length; + list.offset = list.offset < list.count ? list.offset : 0; + } else { + // need to create a list from scratch + var attrs = record.fieldAttrs[fieldName]; + var rawContext = attrs && attrs.context; + var ids = record.data[fieldName] || []; + list = self._makeDataPoint({ + count: ids.length, + fieldAttrs: field.fieldAttrs, + fields: field.relatedFields, + limit: field.limit, + modelName: field.relation, + res_ids: ids, + static: true, + type: 'list', + parentID: record.id, + rawContext: rawContext, + }); + } + record.data[fieldName] = list.id; + if (!field.__no_fetch) { + defs.push(self._readUngroupedList(list)); + } + } + }); + return $.when.apply($, defs); + }, + /** + * batch requests for 1 x2m in list + * + * @see _fetchX2ManysBatched + * @param {Object} list + * @param {string} fieldName + * @returns {Deferred} + */ + _fetchX2ManyBatched: function (list, fieldName) { + var self = this; + var field = list.fields[fieldName]; + + // step 1: collect ids + var ids = []; + _.each(list.data, function (dataPoint) { + var record = self.localData[dataPoint]; + ids = _.unique(ids.concat(record.data[fieldName] || [])); + var m2mList = self._makeDataPoint({ + fieldAttrs: field.fieldAttrs, + fields: field.relatedFields, + modelName: field.relation, + res_ids: record.data[fieldName], + static: true, + type: 'list', + }); + record.data[fieldName] = m2mList.id; + }); + + if (!ids.length || field.__no_fetch) { + return $.when(); + } + + // step 2: fetch data from server + return this.performModelRPC(field.relation, 'read', [ids, _.keys(field.relatedFields)], { + context: {}, // FIXME + }).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: field.relatedFields, + fieldAttrs: field.fieldAttrs, + }); + }); + + _.each(list.data, function (dataPoint) { + var record = self.localData[dataPoint]; + 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; + }); + m2mList.data.push(dataPoint.id); + m2mList.count++; + }); + }); + }); + }, + /** + * batch request for x2ms for datapoint of type list + * + * @param {Object} list + * @returns {Deferred} + */ + _fetchX2ManysBatched: function (list) { + var defs = []; + for (var i = 0; i < list.fieldNames.length; i++) { + var field = list.fields[list.fieldNames[i]]; + if (field.type === 'many2many' || field.type === 'one2many') { + defs.push(this._fetchX2ManyBatched(list, list.fieldNames[i])); + } + } + return $.when.apply($, defs); + }, + /** + * Read all x2many fields and generate the commands for the server to create + * or write them... + * + * @param {Object} record + * @returns {Object} a map from some field names to commands + */ + _generateX2ManyCommands: function (record) { + var self = this; + var commands = {}; + var type, list, relData, relIds, removedIds, addedIds, keptIds, relRecord, i; + + var data = _.extend({}, record.data, record._changes); + + for (var fieldName in record.fields) { + type = record.fields[fieldName].type; + + if (type === 'many2many' || type === 'one2many') { + // skip if this field is empty + if (!data[fieldName]) { + commands[fieldName] = null; + continue; + } + list = this.localData[data[fieldName]]; + if (!list._changes) { + commands[fieldName] = null; + // skip if this field hasn't changed + continue; + } + relData = _.map(list._changes, function (localId) { + return self.localData[localId]; + }); + relIds = _.pluck(relData, 'res_id'); + commands[fieldName] = []; + + if (type === 'many2many') { + // 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 + // an complete override of the actual value of the m2m) + commands[fieldName].push(x2ManyCommands.replace_with(relIds)); + } else if (type === 'one2many') { + removedIds = _.difference(list.res_ids, relIds); + addedIds = _.difference(relIds, list.res_ids); + keptIds = _.intersection(list.res_ids, relIds); + for (i = 0; i < keptIds.length; i++) { + commands[fieldName].push(x2ManyCommands.link_to(keptIds[i])); + } + for (i = 0; i < removedIds.length; i++) { + commands[fieldName].push(x2ManyCommands.delete(removedIds[i])); + } + for (i = 0; i < addedIds.length; i++) { + relRecord = _.findWhere(relData, {res_id: addedIds[i]}); + relRecord = this.get(relRecord.id, {raw: true}); + commands[fieldName].push(x2ManyCommands.create(_.omit(relRecord.data, 'id'))); + } + // FIXME: update of records temporarily disabled as it doesn't work if we updated + // the record in a form view, as the _changes contains all the fields of the + // form view instead of the one that have changed + // _.each(relData, function (relRecord) { + // var new_values = relRecord._changes; + // if (!_.isEmpty(new_values)) { + // commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, new_values)); + // } + // }); + } + } + } + + return commands; + }, + /** + * Every RPC done by the model need to add some context, which is a + * combination of the context of the session, of the record/list, and/or of + * the concerned field. This method combines all these contexts and evaluate + * them with the proper evalcontext. + * + * @param {Object} element an element from the localData + * @param {Object} [options] + * @param {string} [options.field] if given, the context of the field will + * also be added (and evaluated) to the result + * @returns {Object} the evaluated context + */ + _getContext: function (element, options) { + var context = new data.CompoundContext(session.user_context, element.context); + if (options && options.field) { + var attrs = element.fieldAttrs[options.field]; + if (attrs && attrs.context) { + context.add(attrs.context); + } + } + if (element.rawContext) { + context.add(element.rawContext); + var evalContext = this.get(element.parentID, {raw: true}).data; + context.set_eval_context(evalContext); + } + return context.eval(); + }, + /** + * Helper method for the load entry point. + * + * @see load + * + * @param {Object} dataPoint some local resource + * @param {Object} [options] + * @param {string[]} [options.fieldNames] the fields to fetch for a record + * @returns {Deferred} + */ + _load: function (dataPoint, options) { + if (dataPoint.type === 'record') { + return this._fetchRecord(dataPoint, options && options.fieldNames); + } + if (dataPoint.type === 'list' && dataPoint.groupedBy.length) { + return this._readGroup(dataPoint); + } + if (dataPoint.type === 'list' && !dataPoint.groupedBy.length) { + return this._fetchUngroupedList(dataPoint); + } + }, + /** + * Turns a bag of properties into a valid local resource. Also, register + * the resource in the localData object. + * + * @param {Object} params + * @param {Object} [params.fieldAttrs={}] contains the attrs of each field + * @param {Array} [params.fieldNames] the name of fields to load, the list + * of all fields by default + * @param {Object} params.fields contains the description of each field + * @returns {Object} the resource created + */ + _makeDataPoint: function (params) { + var type = params.type || ('domain' in params && 'list') || 'record'; + + var res_id; + var value; + if (type === 'record') { + res_id = params.res_id || (params.data && params.data.id) || _.uniqueId('virtual_'); + } else { + var value_is_array = params.value instanceof Array; + res_id = value_is_array ? params.value[0] : undefined; + value = value_is_array ? params.value[1] : params.value; + } + params.res_id = res_id; + + var dataPoint = { + context: params.context || {}, + domain: params.domain || [], + groupedBy: params.groupedBy || [], + id: _.uniqueId(params.modelName + '_'), + model: params.modelName, + res_id: res_id, + res_ids: params.res_ids || [], + }; + + this.localData[dataPoint.id] = dataPoint; + + var fields = _.extend({ + display_name: {type: 'char'}, + id: {type: 'integer'}, + }, params.fields); + + _.extend(dataPoint, { + _cache: type === 'list' ? {} : undefined, + _changes: null, + aggregateValues: params.aggregateValues || {}, + count: params.count || dataPoint.res_ids.length, + data: params.data || (type === 'record' ? {} : []), + fieldNames: params.fieldNames || Object.keys(fields), + fields: fields, + fieldAttrs: params.fieldAttrs || {}, + isOpen: params.isOpen, + limit: type === 'record' ? 1 : params.limit, + offset: params.offset || (type === 'record' ? _.indexOf(dataPoint.res_ids, res_id) : 0), + openGroupByDefault: params.openGroupByDefault, + orderedBy: params.orderedBy || [], + parentID: params.parentID, + rawContext: params.rawContext, + static: params.static || false, + type: type, // 'record' | 'list' + value: value, + }); + + return dataPoint; + }, + /** + * Once a record is created and some data has been fetched, we need to do + * quite a lot of computations to determine what needs to be fetched. This + * method is doing that. + * + * @see _fetchRecord @see makeDefaultRecord + * + * @param {any} record + * @returns {Deferred -> Object} resolves to the finished resource + */ + _postprocess: function (record) { + var self = this; + var defs = []; + _.each(record.fieldNames, function (name) { + var field = record.fields[name]; + var attrs = record.fieldAttrs[name]; + if (field.__fetch_status && !field.__status_information) { + var field_values = _.mapObject(record.data, function (val, key) { + var fieldType = record.fields[key].type; + if (fieldType === 'many2one') { + return val instanceof Array && val[0]; + } + return val; + }); + var _domain = new data.CompoundDomain(field.domain || {}).set_eval_context(field_values); + var domain = pyeval.eval('domain', _domain); + var fold_field = pyeval.py_eval(attrs.options || '{}').fold_field; + var fetch_status_information = self.performRPC('/web/dataset/search_read', { + model: field.relation, + fields: ['id'].concat(fold_field ? [fold_field] : []), + domain: domain, + }).then(function (result) { + var ids = _.pluck(result.records, 'id'); + return self.performModelRPC(field.relation, 'name_get', [ids], { + context: self._getContext(record, {field: name}), + }).then(function (name_gets) { + _.each(result.records, function (record) { + var name_get = _.find(name_gets, function (n) { + return n[0] === record.id; + }); + record.display_name = name_get[1]; + }); + field.__status_information = result.records; + }); + }); + defs.push(fetch_status_information); + } + if (field.__always_reload) { + if (record.data[name] instanceof Array) { + defs.push(self.performModelRPC(field.relation, 'name_get', [record.data[name][0]], { + context: self._getContext(record, {field: name}), + }).then(function (result) { + record.data[name] = result[0]; + })); + } + } + if (field.__fetch_selection && !self.many2ones[field.relation]) { + var fetchSelection = self + .performModelRPC(field.relation, 'name_search', [], {}) + .then(function (result) { + self.many2ones[field.relation] = result; + }); + defs.push(fetchSelection); + } + if (field.__fetch_many2manys) { + var fetchMany2Manys = self + .performModelRPC(field.relation, 'name_search', [], {}) + .then(function (result) { + self.many2manys[field.relation] = result; + }); + defs.push(fetchMany2Manys); + } + }); + + return $.when.apply($, defs).then(function () { + return record; + }); + }, + /** + * For a grouped list resource, this method fetches all group data by + * performing a /read_group. It also tries to read open subgroups if they + * were open before. + * + * @param {Object} list valid resource object + * @returns {Deferred} resolves to the fetched group object + */ + _readGroup: function (list) { + var self = this; + var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); + return this.performModelRPC(list.model, 'read_group', [], { + fields: fields, + context: list.context, + groupby: list.groupedBy, + domain: list.domain, + orderby: false, + lazy: true, + }).then(function (groups) { + var rawGroupBy = list.groupedBy[0].split(':')[0]; + var previousGroups = _.map(list.data, function (groupID) { + return self.localData[groupID]; + }); + list.data = []; + list.count = groups.length; + var defs = []; + + _.each(groups, function (group) { + var aggregateValues = {}; + _.each(group, function (value, key) { + if (_.contains(fields, key) && key !== list.groupedBy[0]) { + aggregateValues[key] = value; + } + }); + var newGroup = self._makeDataPoint({ + modelName: list.model, + count: group[rawGroupBy + '_count'], + domain: group.__domain, + context: list.context, + fields: list.fields, + fieldNames: list.fieldNames, + fieldAttrs: list.fieldAttrs, + value: group[rawGroupBy], + aggregateValues: aggregateValues, + groupedBy: list.groupedBy.slice(1), + orderedBy: list.orderedBy, + limit: list.limit, + openGroupByDefault: list.openGroupByDefault, + type: 'list', // nested groupedBys not handled yet + }); + list.data.push(newGroup.id); + var old_group = _.find(previousGroups, function (g) { + return g.res_id === newGroup.res_id && g.value === newGroup.value; + }); + if (old_group) { + newGroup.isOpen = old_group.isOpen; + } else if (!newGroup.openGroupByDefault) { + newGroup.isOpen = false; + } else { + newGroup.isOpen = '__fold' in group ? !group.__fold : true; + } + if (newGroup.isOpen && newGroup.count > 0) { + defs.push(self._fetchUngroupedList(newGroup)); + } + }); + return $.when.apply($, defs).then(function () { + // generate the res_ids of the main list, being the concatenation + // of the fetched res_ids in each group + list.res_ids = _.flatten(_.map(arguments, function (group) { + return group ? group.res_ids : []; + })); + return list; + }); + }); + }, + /** + * For 'static' list, such as one2manys in a form view, we can do a /read + * instead of a /search_read. + * + * @param {Object} list a valid resource object + * @returns {Deferred -> Object} resolves to the fetched list object + */ + _readUngroupedList: function (list) { + var self = this; + var def; + var ids = []; + var missingIds = []; + var upper_bound = list.limit ? Math.min(list.offset + list.limit, list.count) : list.count; + for (var i = list.offset; i < upper_bound; i++) { + var id = list.res_ids[i]; + ids.push(id); + if (!list._cache[id]) { + missingIds.push(id); + } + } + if (missingIds.length) { + def = this.performModelRPC(list.model, 'read', [missingIds, list.fieldNames], { + context: {}, // FIXME + }); + } else { + def = $.when(); + } + return def.then(function (records) { + list.data = []; + _.each(ids, function (id) { + var dataPoint; + if (id in list._cache) { + dataPoint = self.localData[list._cache[id]]; + } else { + dataPoint = self._makeDataPoint({ + modelName: list.model, + data: _.findWhere(records, {id: id}), + fields: list.fields, + fieldNames: list.fieldNames, + fieldAttrs: list.fieldAttrs, + }); + + // add many2one records + _.each(dataPoint.fieldNames, function (name) { + var field = dataPoint.fields[name]; + if (field.type === 'many2one') { + var r = self._makeDataPoint({ + modelName: field.relation, + data: { + id: dataPoint.data[name][0], + display_name: dataPoint.data[name][1], + } + }); + + dataPoint.data[name] = r.id; + } + }); + list._cache[id] = dataPoint.id; + } + list.data.push(dataPoint.id); + }); + self._sortList(list); + return list; + }); + }, + /** + * Do a /search_read to get data for a list resource. This does a + * /search_read because the data may not be static (for ex, a list view). + * + * @param {Object} list + * @returns {Deferred} + */ + _searchReadUngroupedList: function (list) { + var self = this; + return this.performRPC('/web/dataset/search_read', { + model: list.model, + fields: list.fieldNames, + domain: list.domain || [], + offset: list.offset, + limit: list.limit, + sort: this._serializeSort(list.orderedBy), + }).then(function (result) { + list.count = result.length; + var data = _.map(result.records, function (record) { + var dataPoint = self._makeDataPoint({ + data: record, + fields: list.fields, + fieldNames: list.fieldNames, + fieldAttrs: list.fieldAttrs, + modelName: list.model, + }); + + // add many2one records + _.each(dataPoint.fieldNames, function (name) { + var field = dataPoint.fields[name]; + if (field.type === 'many2one') { + var r = self._makeDataPoint({ + modelName: field.relation, + data: { + id: dataPoint.data[name][0], + display_name: dataPoint.data[name][1], + } + }); + + dataPoint.data[name] = r.id; + } + }); + return dataPoint.id; + }); + list.data = data; + list.res_ids = _.pluck(result.records, 'id'); + return list; + }); + }, + /** + * Helper method, generates a string to describe a ordered by sequence for + * SQL. + * + * For example, [{name: 'foo', asc: true}, {name: 'bar', asc: false}] will + * be converted into 'foo ASC, bar DESC' + * + * @param {Object[]} orderBy list of objects {name:..., asc: ...} + * @returns {string} + */ + _serializeSort: function (orderBy) { + return _.map(orderBy, function (order) { + return order.asc ? order.name + ' ASC' : order.name + ' DESC'; + }).join(', '); + }, + /** + * Change the offset of a record. Note that this does not reload the data. + * The offset is used to load a different record in a list of record (for + * example, a form view with a pager. Clicking on next/previous actually + * changes the offset through this method). + * + * @param {string} elementId local id for the resource + * @param {number} offset + */ + _setOffset: function (elementId, offset) { + var element = this.localData[elementId]; + element.offset = offset; + if (element.type === 'record' && element.res_ids.length) { + element.res_id = element.res_ids[offset]; + } + }, + /** + * 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. + * + * @param {Object} list + */ + _sortList: function (list) { + var self = this; + if (list.orderedBy.length) { + // sort records according to ordered_by[0] + var order = list.orderedBy[0]; + list.data.sort(function (r1, r2) { + var data1 = self.localData[r1].data; + var data2 = self.localData[r2].data; + if (data1[order.name] < data2[order.name]) { + return order.asc ? -1 : 1; + } + if (data1[order.name] > data2[order.name]) { + return order.asc ? 1 : -1; + } + return 0; + }); + } + }, + /** + * Helper method. Recursively traverses the data, starting from the element + * record (or list), then following all relations. This is useful when one + * want to determine a property for the current record. + * + * For example, isDirty need to check all relations to find out if something + * has been modified, or not. + * + * @param {Object} element a valid local resource + * @param {callback} fn a function to be called on each visited element + */ + _visitChildren: function (element, fn) { + var self = this; + fn(element); + if (element.type === 'record') { + for (var fieldName in element.data) { + var field = element.fields[fieldName]; + if (_.contains(['one2many', 'many2one', 'many2many'], field.type)) { + var relationalElement = this.localData[element.data[fieldName]]; + + // relationalElement could be empty in the case of a many2one + if (relationalElement) { + self._visitChildren(relationalElement, fn); + } + } + } + } + if (element.type === 'list') { + _.each(element.data, function (elemId) { + var elem = self.localData[elemId]; + self._visitChildren(elem, fn); + }); + } + }, +}); + +return Model; + +}); + -- cgit v1.2.3 From 450e2f4d488ae9723ef668835799c8371461410d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 22 Feb 2017 12:19:18 +0100 Subject: [FIX] web: basicmodel can evaluate context for new records --- addons/web/static/src/js/views/basic/basic_model.js | 4 ++++ 1 file changed, 4 insertions(+) (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 5baf65f1c30..1e94ab8c47d 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -475,12 +475,15 @@ var Model = AbstractModel.extend({ }); record._changes[name] = rec.id; } else if (field.type === 'one2many' || field.type === 'many2many') { + var attrs = record.fieldAttrs[name]; var x2manyList = self._makeDataPoint({ context: record.context, fieldAttrs: field.fieldAttrs, fields: field.relatedFields, limit: field.limit, modelName: field.relation, + parentID: record.id, + rawContext: attrs && attrs.context, res_ids: [], static: true, type: 'list', @@ -1541,6 +1544,7 @@ var Model = AbstractModel.extend({ if (element.rawContext) { context.add(element.rawContext); var evalContext = this.get(element.parentID, {raw: true}).data; + evalContext.id = evalContext.id || false; context.set_eval_context(evalContext); } return context.eval(); -- cgit v1.2.3 From 4e4b3c7b597206a54d6257e7c558b31c5fb32e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 22 Feb 2017 14:28:44 +0100 Subject: [FIX] web: properly transfer context to onchange rpcs --- 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 1e94ab8c47d..afad32597f1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -951,7 +951,7 @@ var Model = AbstractModel.extend({ var self = this; var onchange_spec = this._buildOnchangeSpecs(record); var idList = record.data.id ? [record.data.id] : []; - var context = data.build_context(record, session.user_context).eval(); + var context = this._getContext(record); var rawData = this.get(record.id, {raw: true}).data; var commands = this._generateX2ManyCommands(record); -- cgit v1.2.3 From 05ec284e9c458059260645f0eaa5b82b81a12cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 22 Feb 2017 16:07:18 +0100 Subject: [FIX] web: properly compute field values for onchanges --- addons/web/static/src/js/views/basic/basic_model.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (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 afad32597f1..ac82b5bf2e8 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -168,6 +168,8 @@ var Model = AbstractModel.extend({ fieldAttrs: list.fieldAttrs, fieldNames: list.fieldNames, fields: list.fields, + parentID: list.id, + relationField: list.relationField, }; return this.makeDefaultRecord(list.model, params).then(function(id) { list.count++; @@ -354,6 +356,7 @@ var Model = AbstractModel.extend({ delete element._changes; delete element.static; delete element.parentID; + delete element.relation_field; delete element.rawContext; return element; }, @@ -459,6 +462,8 @@ var Model = AbstractModel.extend({ fieldNames: params.fieldNames, fieldAttrs: params.fieldAttrs, context: params.context, + parentID: params.parentID, + relationField: params.relationField, res_ids: params.res_ids, }); @@ -484,6 +489,7 @@ var Model = AbstractModel.extend({ modelName: field.relation, parentID: record.id, rawContext: attrs && attrs.context, + relationField: field.relation_field, res_ids: [], static: true, type: 'list', @@ -955,7 +961,19 @@ var Model = AbstractModel.extend({ var rawData = this.get(record.id, {raw: true}).data; var commands = this._generateX2ManyCommands(record); + + // compute field values var currentData = _.pick(_.extend(rawData, commands), record.fieldNames); + if (record.relationField) { + // if there is a relation field, this means that record is an elem + // in a one2many. The relation field is the corresponding many2one + + // parent is the list element containing all the records in the o2m + // and parent.parentID is the ID of the main record + var parent = this.localData[record.parentID]; + currentData[record.relationField] = this.get(parent.parentID, {raw:true}).data; + } + if (fields.length === 1) { fields = fields[0]; } @@ -1362,6 +1380,7 @@ var Model = AbstractModel.extend({ type: 'list', parentID: record.id, rawContext: rawContext, + relationField: field.relation_field, }); } record.data[fieldName] = list.id; @@ -1628,6 +1647,7 @@ var Model = AbstractModel.extend({ orderedBy: params.orderedBy || [], parentID: params.parentID, rawContext: params.rawContext, + relationField: params.relationField, static: params.static || false, type: type, // 'record' | 'list' value: value, -- cgit v1.2.3 From 044e7bb59989500bdbdc8ace0402c314534a9975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 22 Feb 2017 16:48:20 +0100 Subject: [FIX] web: display error dialog when onchanges fails When the onchange returns a dictionary with a 'warning' key, it is supposed to fail and display a dialog with the content of the warning. --- addons/web/static/src/js/views/basic/basic_model.js | 6 +++++- 1 file changed, 5 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 ac82b5bf2e8..c2855c8aaa4 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -951,7 +951,8 @@ var Model = AbstractModel.extend({ * * @param {Object} record * @param {string[]} fields changed fields - * @returns + * @returns {Deferred} The returned deferred can fail, in which case the + * fail value will be the warning message received from the server */ _applyOnChange: function (record, fields) { var self = this; @@ -980,6 +981,9 @@ var Model = AbstractModel.extend({ return this.mutex.exec(function () { var args = [idList, currentData, fields, onchange_spec, context]; return self.performModelRPC(record.model, 'onchange', args, {}).then(function (result) { + if (result.warning) { + return $.Deferred().reject(result.warning); + } var defs = []; _.each(result.value, function (val, name) { var field = record.fields[name]; -- cgit v1.2.3 From 136292f6890ff730b5a9fd70eb503d4f6a0f9c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 23 Feb 2017 10:36:13 +0100 Subject: [REF] web, *: move CompoundContext out of data.js We want to remove data.js, but we still need the CompoundContext class, so we take it out, and rename it 'Context' --- addons/web/static/src/js/views/basic/basic_model.js | 3 ++- 1 file changed, 2 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 c2855c8aaa4..2028127c202 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -79,6 +79,7 @@ odoo.define('web.BasicModel', function (require) { var AbstractModel = require('web.AbstractModel'); var concurrency = require('web.concurrency'); +var Context = require('web.Context'); var data = require('web.data'); // TODO: remove dependency to data.js var pyeval = require('web.pyeval'); var session = require('web.session'); @@ -1557,7 +1558,7 @@ var Model = AbstractModel.extend({ * @returns {Object} the evaluated context */ _getContext: function (element, options) { - var context = new data.CompoundContext(session.user_context, element.context); + var context = new Context(session.user_context, element.context); if (options && options.field) { var attrs = element.fieldAttrs[options.field]; if (attrs && attrs.context) { -- cgit v1.2.3 From 3c0190d0a6c0d5ccc130fd9b685681fa9bac1de3 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 23 Feb 2017 14:08:33 +0100 Subject: [IMP] web: use moment object instead of string for date Before this commit, the odoo web client used strings to represent dates. Now, the date is parsed into a moment object when the value is read from the server side. The widget formats the object to display the value. When the widget value changes, the value is parsed to a moment object. The value is automatically formatted to server value in ajax calls (toJSON method). --- addons/web/static/src/js/views/basic/basic_model.js | 15 ++++++++++++++- 1 file changed, 14 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 2028127c202..2f6c3849b86 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -83,6 +83,7 @@ var Context = require('web.Context'); var data = require('web.data'); // TODO: remove dependency to data.js var pyeval = require('web.pyeval'); var session = require('web.session'); +var field_utils = require('web.field_utils'); var x2ManyCommands = { // (0, _, {values}) @@ -536,6 +537,12 @@ var Model = AbstractModel.extend({ _.each(_.keys(many2ones), function (name) { defs.push(self._fetchNameGets(x2manyList, name)); }); + } else if (field.type === 'date') { + // process date: convert into Date object + record._changes[name] = field_utils.parse_date(result[name]); + } else if (field.type === 'datetime') { + // process datetime: convert into Date object + record._changes[name] = field_utils.parse_datetime(result[name]); } else { record._changes[name] = result[name]; } @@ -1252,11 +1259,11 @@ var Model = AbstractModel.extend({ record.data = _.extend({}, record.data, result); return self._postprocess(record); }).then(function (record) { - // process many2one: split [id, nameget] and create corresponding record _.each(fieldNames, function (name) { var field = record.fields[name]; var val = record.data[name]; if (field.type === 'many2one') { + // process many2one: split [id, nameget] and create corresponding record if (val !== false) { // the many2one value is of the form [id, display_name] var r = self._makeDataPoint({ @@ -1271,6 +1278,12 @@ var Model = AbstractModel.extend({ // no value for the many2one record.data[name] = false; } + } else if (field.type === 'date') { + // process data: convert into Date object + record.data[name] = field_utils.parse_date(val); + } else if (field.type === 'datetime') { + // process datetime: convert into Date object + record.data[name] = field_utils.parse_datetime(val); } }); }).then(function () { -- cgit v1.2.3 From 2db5ee0f7be9f22d87fbffa72c743b3ab92bcd60 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 23 Feb 2017 14:19:56 +0100 Subject: [IMP] web: some more linting to follow guidelines --- addons/web/static/src/js/views/basic/basic_model.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 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 2f6c3849b86..5cce92af685 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -123,7 +123,7 @@ var x2ManyCommands = { } }; -var Model = AbstractModel.extend({ +var BasicModel = AbstractModel.extend({ /** * @override */ @@ -149,7 +149,6 @@ var Model = AbstractModel.extend({ // Public //-------------------------------------------------------------------------- - /** * 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. @@ -173,7 +172,7 @@ var Model = AbstractModel.extend({ parentID: list.id, relationField: list.relationField, }; - return this.makeDefaultRecord(list.model, params).then(function(id) { + return this.makeDefaultRecord(list.model, params).then(function (id) { list.count++; if (position === 'top') { list.data.unshift(id); @@ -874,10 +873,10 @@ var Model = AbstractModel.extend({ * @param {string} parentID id of the parent resource to reload * @returns {Deferred -> string} resolves to the parent id */ - toggleActive: function(recordIDs, value, parentID) { + toggleActive: function (recordIDs, value, parentID) { var self = this; var parent = this.localData[parentID]; - var resIDs = _.map(recordIDs, function(recordID) { + var resIDs = _.map(recordIDs, function (recordID) { return self.localData[recordID].res_id; }); return this @@ -997,6 +996,7 @@ var Model = AbstractModel.extend({ 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 @@ -1008,7 +1008,7 @@ var Model = AbstractModel.extend({ var data = _.isArray(val) ? {id: val[0], display_name: val[1]} : {id: val}; - var rec = self._makeDataPoint({ + rec = self._makeDataPoint({ context: context, data: data, modelName: field.relation, @@ -1019,7 +1019,6 @@ var Model = AbstractModel.extend({ record._changes = record._changes || {}; var listId = record._changes[name] || record.data[name]; var list = self.localData[listId]; - var rec; _.each(val, function (command) { if (command[0] === 0 || command[0] === 1) { // CREATE or UPDATE @@ -2032,7 +2031,6 @@ var Model = AbstractModel.extend({ }, }); -return Model; - +return BasicModel; }); -- cgit v1.2.3 From 0c1d9204c3f38770be6a1bcb7bd9288cd60b9508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 23 Feb 2017 16:19:40 +0100 Subject: [REF] web: small refactor + add code in basic model --- .../web/static/src/js/views/basic/basic_model.js | 97 ++++++++++------------ 1 file changed, 46 insertions(+), 51 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 5cce92af685..15921f4332e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -15,35 +15,34 @@ odoo.define('web.BasicModel', function (require) { * its id as key in many methods. * * Here is a description of what those data point look like: - * { - * _changes: { - * data: { fieldName: field_values | [commands] }, - * relational_data: { field_names: datapoint } - * }, - * aggregate_values: { fieldName: number} | undefined - * count: number, default=0, - * context: {string:value}, - * data: {string:value} | [{string:value}], - * field_names: [string], - * fields: {string:field description}, - * grouped_by: [strings] | undefined, - * id: number, - * is_group: boolean, - * is_list: boolean, - * is_open: boolean, - * is_record: boolean, - * limit: number | undefined (default=80 for lists) - * model: string, - * offset: number, default=0, - * openGroupByDefault: boolean, - * ordered_by: [order] | undefined, - * res_id: [number]|undefined, - * res_ids: [number] | undefined, - * relational_data: { - * field_names: datapoints - * }, - * static: boolean, * like previous DatasetStatic - * } + * var dataPoint = { + * _cache: {Object|undefined} + * _changes: {Object|null}, + * aggregateValues: {Object}, + * context: {Object}, + * count: {integer}, + * data: {Object|Object[]}, + * domain: {*[]}, + * fieldNames: {string[]}, + * fields: {Object}, + * fieldAttrs: {Object}, + * groupedBy: {string[]}, + * id: {integer}, + * isOpen: {boolean}, + * limit: {integer}, + * model: {string, + * offset: {integer}, + * openGroupByDefault: {boolean}, + * orderedBy: {string[]}, + * parentID: {string}, + * rawContext: {Object}, + * relationField: {string}, + * res_id: {integer|null}, + * res_ids: {integer[]}, + * static: {boolean}, + * type: {string} 'record' | 'list' + * value: ?, + * }; * * Notes: * - id: is totally unrelated to res_id. id is a web client local concept @@ -1620,55 +1619,51 @@ var BasicModel = AbstractModel.extend({ _makeDataPoint: function (params) { var type = params.type || ('domain' in params && 'list') || 'record'; - var res_id; - var value; + var res_id, value; + var res_ids = params.res_ids || []; if (type === 'record') { res_id = params.res_id || (params.data && params.data.id) || _.uniqueId('virtual_'); } else { - var value_is_array = params.value instanceof Array; - res_id = value_is_array ? params.value[0] : undefined; - value = value_is_array ? params.value[1] : params.value; + var isValueArray = params.value instanceof Array; + res_id = isValueArray ? params.value[0] : undefined; + value = isValueArray ? params.value[1] : params.value; } - params.res_id = res_id; - - var dataPoint = { - context: params.context || {}, - domain: params.domain || [], - groupedBy: params.groupedBy || [], - id: _.uniqueId(params.modelName + '_'), - model: params.modelName, - res_id: res_id, - res_ids: params.res_ids || [], - }; - - this.localData[dataPoint.id] = dataPoint; var fields = _.extend({ display_name: {type: 'char'}, id: {type: 'integer'}, }, params.fields); - _.extend(dataPoint, { + var dataPoint = { _cache: type === 'list' ? {} : undefined, _changes: null, aggregateValues: params.aggregateValues || {}, - count: params.count || dataPoint.res_ids.length, + context: params.context || {}, + count: params.count || res_ids.length, data: params.data || (type === 'record' ? {} : []), + domain: params.domain || [], fieldNames: params.fieldNames || Object.keys(fields), fields: fields, fieldAttrs: params.fieldAttrs || {}, + groupedBy: params.groupedBy || [], + id: _.uniqueId(params.modelName + '_'), isOpen: params.isOpen, limit: type === 'record' ? 1 : params.limit, - offset: params.offset || (type === 'record' ? _.indexOf(dataPoint.res_ids, res_id) : 0), + model: params.modelName, + offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0), openGroupByDefault: params.openGroupByDefault, orderedBy: params.orderedBy || [], parentID: params.parentID, rawContext: params.rawContext, relationField: params.relationField, + res_id: res_id, + res_ids: res_ids, static: params.static || false, type: type, // 'record' | 'list' value: value, - }); + }; + + this.localData[dataPoint.id] = dataPoint; return dataPoint; }, -- cgit v1.2.3 From 4a612171b3bf85f2902c582fcc658eea668a732e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 24 Feb 2017 14:03:58 +0100 Subject: [FIX] web: basic model now parse dates on lists Before this commit, the BasicModel didn't parse the dates when loading data for a list. But the rest of the web client assumes that data is properly parsed into their correct types, so a moment instance for date and datetime. Also, uppercase field_utils and properly reorder imports --- addons/web/static/src/js/views/basic/basic_model.js | 16 +++++++++++----- 1 file changed, 11 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 15921f4332e..9d8df720436 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -80,9 +80,9 @@ var AbstractModel = require('web.AbstractModel'); var concurrency = require('web.concurrency'); var Context = require('web.Context'); var data = require('web.data'); // TODO: remove dependency to data.js +var fieldUtils = require('web.field_utils'); var pyeval = require('web.pyeval'); var session = require('web.session'); -var field_utils = require('web.field_utils'); var x2ManyCommands = { // (0, _, {values}) @@ -537,10 +537,10 @@ var BasicModel = AbstractModel.extend({ }); } else if (field.type === 'date') { // process date: convert into Date object - record._changes[name] = field_utils.parse_date(result[name]); + record._changes[name] = fieldUtils.parse_date(result[name]); } else if (field.type === 'datetime') { // process datetime: convert into Date object - record._changes[name] = field_utils.parse_datetime(result[name]); + record._changes[name] = fieldUtils.parse_datetime(result[name]); } else { record._changes[name] = result[name]; } @@ -1278,10 +1278,10 @@ var BasicModel = AbstractModel.extend({ } } else if (field.type === 'date') { // process data: convert into Date object - record.data[name] = field_utils.parse_date(val); + record.data[name] = fieldUtils.parse_date(val); } else if (field.type === 'datetime') { // process datetime: convert into Date object - record.data[name] = field_utils.parse_datetime(val); + record.data[name] = fieldUtils.parse_datetime(val); } }); }).then(function () { @@ -1926,6 +1926,12 @@ var BasicModel = AbstractModel.extend({ dataPoint.data[name] = r.id; } + if (field.type === 'date') { + dataPoint.data[name] = fieldUtils.parse_date(dataPoint.data[name]); + } + if (field.type === 'datetime') { + dataPoint.data[name] = fieldUtils.parse_datetime(dataPoint.data[name]); + } }); return dataPoint.id; }); -- cgit v1.2.3 From b717c6786429ccd68678462f3d0d994ffe3301b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 24 Feb 2017 15:40:22 +0100 Subject: [REF] web,*: improve the way field_utils are done We replace a bunch of formatting and parsing functions by an object (parse or format) which can be used to dispatch on the correct type. It allows us to remove the format_field and parse_field functions. Also, we camelcase our new code and add some comments. --- addons/web/static/src/js/views/basic/basic_model.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 9d8df720436..54a95f69500 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -537,10 +537,10 @@ var BasicModel = AbstractModel.extend({ }); } else if (field.type === 'date') { // process date: convert into Date object - record._changes[name] = fieldUtils.parse_date(result[name]); + record._changes[name] = fieldUtils.parse.date(result[name]); } else if (field.type === 'datetime') { // process datetime: convert into Date object - record._changes[name] = fieldUtils.parse_datetime(result[name]); + record._changes[name] = fieldUtils.parse.datetime(result[name]); } else { record._changes[name] = result[name]; } @@ -1278,10 +1278,10 @@ var BasicModel = AbstractModel.extend({ } } else if (field.type === 'date') { // process data: convert into Date object - record.data[name] = fieldUtils.parse_date(val); + record.data[name] = fieldUtils.parse.date(val); } else if (field.type === 'datetime') { // process datetime: convert into Date object - record.data[name] = fieldUtils.parse_datetime(val); + record.data[name] = fieldUtils.parse.datetime(val); } }); }).then(function () { @@ -1927,10 +1927,10 @@ var BasicModel = AbstractModel.extend({ dataPoint.data[name] = r.id; } if (field.type === 'date') { - dataPoint.data[name] = fieldUtils.parse_date(dataPoint.data[name]); + dataPoint.data[name] = fieldUtils.parse.date(dataPoint.data[name]); } if (field.type === 'datetime') { - dataPoint.data[name] = fieldUtils.parse_datetime(dataPoint.data[name]); + dataPoint.data[name] = fieldUtils.parse.datetime(dataPoint.data[name]); } }); return dataPoint.id; -- cgit v1.2.3 From 167db67300fde595b9017f90ab87494db37adc6b Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Fri, 24 Feb 2017 17:08:45 +0100 Subject: [FIX] web: _getContext was not returning a consistent context Hopefully, this commit makes it consistent. There were two problems with the previous implementation: - Contexts from field attr (if asked thanks to the options.field parameter) were not evaluated with the right evaluation ctx - "raw" contexts (from parent) had to be evaluated with another evaluation context Also rename the options.field parameter to options.fieldName. --- addons/web/static/src/js/views/basic/basic_model.js | 21 +++++++++++++-------- 1 file changed, 13 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 54a95f69500..4413168cc73 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1564,24 +1564,29 @@ var BasicModel = AbstractModel.extend({ * * @param {Object} element an element from the localData * @param {Object} [options] - * @param {string} [options.field] if given, the context of the field will - * also be added (and evaluated) to the result + * @param {string} [options.fieldName] + * the name of the field whose context needs to be added to the + * result (and evaluated) * @returns {Object} the evaluated context */ _getContext: function (element, options) { var context = new Context(session.user_context, element.context); - if (options && options.field) { - var attrs = element.fieldAttrs[options.field]; + context.set_eval_context(element.data); + if (options && options.fieldName) { + var attrs = element.fieldAttrs[options.fieldName]; if (attrs && attrs.context) { context.add(attrs.context); } } + if (element.rawContext) { - context.add(element.rawContext); + var rawContext = new Context(element.rawContext); var evalContext = this.get(element.parentID, {raw: true}).data; evalContext.id = evalContext.id || false; - context.set_eval_context(evalContext); + rawContext.set_eval_context(evalContext); + context.add(rawContext); } + return context.eval(); }, /** @@ -1701,7 +1706,7 @@ var BasicModel = AbstractModel.extend({ }).then(function (result) { var ids = _.pluck(result.records, 'id'); return self.performModelRPC(field.relation, 'name_get', [ids], { - context: self._getContext(record, {field: name}), + context: self._getContext(record, {fieldName: name}), }).then(function (name_gets) { _.each(result.records, function (record) { var name_get = _.find(name_gets, function (n) { @@ -1717,7 +1722,7 @@ var BasicModel = AbstractModel.extend({ if (field.__always_reload) { if (record.data[name] instanceof Array) { defs.push(self.performModelRPC(field.relation, 'name_get', [record.data[name][0]], { - context: self._getContext(record, {field: name}), + context: self._getContext(record, {fieldName: name}), }).then(function (result) { record.data[name] = result[0]; })); -- cgit v1.2.3 From cd6bba33c2b0b5960c596784dbfcc44b2c9f32d1 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 23 Feb 2017 17:52:23 +0100 Subject: [IMP] web: auto-context for fields model RPC calls Some fields need to do some model RPC calls (name_search, name_create, ...). Before this commit, they did not use the performModelRPC method but an explicit trigger_up so that the BasicController (the FieldManagerMixin) catches it and ask the BasicModel to perform the RPC. This was designed to let the BasicModel handle the context. This commit makes the context be automatically binded without the need to explicitely call the BasicModel for each route in the FieldManagerMixin. It also ensures that the field own context is properly merged with the user/action/search contexts. Now, the fields use the performModelRPC method and the FieldManagerMixin catches the associated "call_service" event. Then, it can get the full context from the BasicModel instance and let the event continue its propagation. To know which field context has to be fetched, the performModelRPC method is overriden by AbstractField to automatically add the dataPointID and the field name in the options parameter. --- .../web/static/src/js/views/basic/basic_model.js | 51 ++++++---------------- 1 file changed, 14 insertions(+), 37 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 4413168cc73..e28daffc587 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -613,42 +613,6 @@ var BasicModel = AbstractModel.extend({ }); return record.id; }, - /** - * This method just does a name_create call to the server. - * - * @todo Maybe this method should also create a local record... - * - * @param {string} model the name of the model - * @param {string} name the desired name of the created record - * @param {Object} context the context to be passed to the rpc - * @returns {Deferred} - */ - nameCreate: function (model, name, context) { - return this.performModelRPC(model, 'name_create', [name], { - context: _.extend({}, session.user_context, context), - }); - }, - /** - * Do a name_search. I think that it should be done directly by the widget - * @todo remove this - * @deprecated - * - * @param {any} model - * @param {any} name - * @param {any} domain - * @param {any} operator - * @param {any} limit - * @returns - */ - nameSearch: function (model, name, domain, operator, limit) { - // fixme: correctly handle context - return this.performModelRPC(model, 'name_search', [], { - name: name || '', - args: domain || [], - operator: operator || 'ilike', - limit: limit || 0, - }); - }, /** * This is an extremely important method. All changes in any field go * through this method. It will then apply them in the local state, check @@ -909,6 +873,20 @@ var BasicModel = AbstractModel.extend({ }); } }, + /** + * Every RPC done by the model need to add some context, which is a + * combination of the context of the session, of the record/list, and/or of + * the concerned field. This method combines all these contexts and evaluate + * them with the proper evalcontext. + * + * @param {string} recordID - the id of an element from the localData + * @see _getContext for the options parameter + * @returns {Object} the evaluated context + */ + getContext: function (recordID, options) { + var record = this.localData[recordID]; + return this._getContext(record, options); + }, //-------------------------------------------------------------------------- // Private @@ -2039,4 +2017,3 @@ var BasicModel = AbstractModel.extend({ return BasicModel; }); - -- cgit v1.2.3 From fdcab23759518db9b4958a52e2492621dbada605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 24 Feb 2017 21:39:28 +0100 Subject: [FIX] web: fix crashes with various views and date/datetimes The problem was that in some cases, the date/datetime values were not properly parsed when they enter the system. --- addons/web/static/src/js/views/basic/basic_model.js | 4 ++++ 1 file changed, 4 insertions(+) (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 e28daffc587..3b35c994443 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1858,6 +1858,10 @@ var BasicModel = AbstractModel.extend({ }); dataPoint.data[name] = r.id; + } else if (field.type === 'date') { + dataPoint.data[name] = fieldUtils.parse.date(dataPoint.data[name]); + } else if (field.type === 'datetime') { + dataPoint.data[name] = fieldUtils.parse.datetime(dataPoint.data[name]); } }); list._cache[id] = dataPoint.id; -- cgit v1.2.3 From ecceee639ed7bfdaf0e2e152fd147ee02e3281e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 26 Feb 2017 09:24:45 +0100 Subject: [FIX] web: formview dialog can now properly evaluate context For this change, I had to pass the handle to the parent datapoint all the way from the controller to the new form view. --- addons/web/static/src/js/views/basic/basic_model.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 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 3b35c994443..ec64eb9b7ad 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1233,7 +1233,10 @@ var BasicModel = AbstractModel.extend({ }).then(function (result) { result = result[0]; record.data = _.extend({}, record.data, result); - return self._postprocess(record); + }).then(function () { + return self._fetchX2Manys(record, oldData, fieldNames).then(function () { + return self._postprocess(record); + }); }).then(function (record) { _.each(fieldNames, function (name) { var field = record.fields[name]; @@ -1262,10 +1265,7 @@ var BasicModel = AbstractModel.extend({ record.data[name] = fieldUtils.parse.datetime(val); } }); - }).then(function () { - return self._fetchX2Manys(record, oldData, fieldNames).then(function () { - return record; - }); + return record; }); }, /** @@ -1549,7 +1549,12 @@ var BasicModel = AbstractModel.extend({ */ _getContext: function (element, options) { var context = new Context(session.user_context, element.context); - context.set_eval_context(element.data); + var evalContext = this.get(element.id, {raw: true}).data; + if (element.parentID) { + var parent = this.get(element.parentID, {raw: true}); + _.extend(evalContext, {parent: parent.data}); + } + context.set_eval_context(evalContext); if (options && options.fieldName) { var attrs = element.fieldAttrs[options.fieldName]; if (attrs && attrs.context) { -- cgit v1.2.3 From 386820abbe8182eacb346f41c46bc702d73e1ac7 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 24 Feb 2017 16:31:39 +0100 Subject: [FIX] web: basic_model: apply onchanges on many2many fields Before this commit, the many2many fields were handled like basic fields, which is obviously wrong. --- 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 ec64eb9b7ad..8b9d7812ca7 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -992,7 +992,7 @@ var BasicModel = AbstractModel.extend({ }); record._changes[name] = rec.id; } - } else if (field.type === 'one2many') { + } 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]; -- cgit v1.2.3 From a1f4fe8616073c556968ecc453493d82b1e98fb6 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 28 Feb 2017 11:19:40 +0100 Subject: [FIX] web: basic_model: no res_id on new records We remove the res_id when exporting new records, as it is meaningless outside the model (it's like "virtual_something"). This fixes a bug as the button box is now correctly hidden in 'create' mode. --- addons/web/static/src/js/views/basic/basic_model.js | 3 +++ 1 file changed, 3 insertions(+) (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 8b9d7812ca7..c5d212ff930 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -336,6 +336,9 @@ var BasicModel = AbstractModel.extend({ delete element.orderedBy; delete element.aggregateValues; delete element.groupedBy; + if (this.isNew(element.id)) { + delete element.res_id; + } } if (element.type === 'list') { // apply changes if any -- cgit v1.2.3 From d7eb116fa8b9d5e9cd75f628a3a9e6e53fcbd744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 28 Feb 2017 16:17:28 +0100 Subject: [FIX] web: properly compute contexts in some cases When we have a many2one widget in a one2many, editable, the context was not properly computed: it expected the parent to be a record object, but in that case, it is a list object. --- addons/web/static/src/js/views/basic/basic_model.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 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 c5d212ff930..5f1e6ccbb9c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1555,6 +1555,9 @@ var BasicModel = AbstractModel.extend({ var evalContext = this.get(element.id, {raw: true}).data; if (element.parentID) { var parent = this.get(element.parentID, {raw: true}); + if (parent.type === 'list' && this.localData[element.parentID].parentID) { + parent = this.get(this.localData[element.parentID].parentID, {raw: true}); + } _.extend(evalContext, {parent: parent.data}); } context.set_eval_context(evalContext); @@ -1846,11 +1849,12 @@ var BasicModel = AbstractModel.extend({ dataPoint = self.localData[list._cache[id]]; } else { dataPoint = self._makeDataPoint({ - modelName: list.model, data: _.findWhere(records, {id: id}), - fields: list.fields, - fieldNames: list.fieldNames, fieldAttrs: list.fieldAttrs, + fieldNames: list.fieldNames, + fields: list.fields, + modelName: list.model, + parentID: list.id, }); // add many2one records @@ -1862,7 +1866,8 @@ var BasicModel = AbstractModel.extend({ data: { id: dataPoint.data[name][0], display_name: dataPoint.data[name][1], - } + }, + parentID: dataPoint.id, }); dataPoint.data[name] = r.id; @@ -1905,6 +1910,7 @@ var BasicModel = AbstractModel.extend({ fieldNames: list.fieldNames, fieldAttrs: list.fieldAttrs, modelName: list.model, + parentID: list.id, }); // add many2one records -- cgit v1.2.3 From 2bd4497b494e6d3dece46026283cce75beb1da91 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 1 Mar 2017 09:47:39 +0100 Subject: [FIX] web: apply onchange even if warning The onchange route returns field values, but it can also return a warning, that needs to be displayed in a dialog. When it does, the new values must be applied anyway, which wasn't the case before this rev. We generalized the 'warning' custom event handled by the webclient so that it can display a warning in a dialog (and not only through the NotificationManager). --- addons/web/static/src/js/views/basic/basic_model.js | 6 +++++- 1 file changed, 5 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 5f1e6ccbb9c..b16bf1e10b0 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -969,7 +969,11 @@ var BasicModel = AbstractModel.extend({ var args = [idList, currentData, fields, onchange_spec, context]; return self.performModelRPC(record.model, 'onchange', args, {}).then(function (result) { if (result.warning) { - return $.Deferred().reject(result.warning); + self.trigger_up('warning', { + message: result.warning.message, + title: result.warning.title, + type: 'dialog', + }); } var defs = []; _.each(result.value, function (val, name) { -- cgit v1.2.3 From f79399bf7720e7a69b95fe2c548208b56b94ce29 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 1 Mar 2017 10:53:03 +0100 Subject: [FIX] web: parse date(time) values after onchange Date and Datetime values are stored as moment instances, not strings. This wasn't the case after an onchange, and it thus produced a crash as we tried to call the format method on a string. --- addons/web/static/src/js/views/basic/basic_model.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 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 b16bf1e10b0..785cf79775b 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -539,10 +539,10 @@ var BasicModel = AbstractModel.extend({ defs.push(self._fetchNameGets(x2manyList, name)); }); } else if (field.type === 'date') { - // process date: convert into Date object + // process date: convert into a moment instance record._changes[name] = fieldUtils.parse.date(result[name]); } else if (field.type === 'datetime') { - // process datetime: convert into Date object + // process datetime: convert into a moment instance record._changes[name] = fieldUtils.parse.datetime(result[name]); } else { record._changes[name] = result[name]; @@ -1042,6 +1042,12 @@ var BasicModel = AbstractModel.extend({ 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; } @@ -1265,10 +1271,10 @@ var BasicModel = AbstractModel.extend({ record.data[name] = false; } } else if (field.type === 'date') { - // process data: convert into Date object + // process data: convert into a moment instance record.data[name] = fieldUtils.parse.date(val); } else if (field.type === 'datetime') { - // process datetime: convert into Date object + // process datetime: convert into a moment instance record.data[name] = fieldUtils.parse.datetime(val); } }); -- cgit v1.2.3 From 206453c83689ad0dc6ceec3686153ab79bf88660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 27 Feb 2017 19:39:41 +0100 Subject: [FIX] web: various fixes with one2many/formviewdialogs We share the basic model record between views, and we have to make sure that everything is properly coordinated. The main issue solved by this commit is the fact that we can now edit a one2many record, then close the dialog, then edit it again and finally cancel the changes, which bring us to the previous step. Also, we had to slightly improve the AbstractView API --- .../web/static/src/js/views/basic/basic_model.js | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) (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 785cf79775b..bc36f454914 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -403,6 +403,7 @@ var BasicModel = AbstractModel.extend({ * of all fields by default * @param {Object} params.fields contains the description of each field * @param {string} [params.type] 'record' or 'list' + * @param {string} [params.recordID] an ID for an existing resource. * @returns {Deferred -> string} resolves to a local id, or handle */ load: function (params) { @@ -733,11 +734,22 @@ var BasicModel = AbstractModel.extend({ * @param {string} record_id local resource * @param {Object} [options] * @param {boolean} [options.reload=true] if true, data will be reloaded + * @param {boolean} [options.localSave=false] if true, the record will only + * be 'locally' saved: its changes will move from the _changes key to the + * data key * @returns {Deferred} */ 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 () { var record = self.localData[record_id]; @@ -799,6 +811,21 @@ var BasicModel = AbstractModel.extend({ } }); }, + /** + * Set and return the fieldattrs on a record element. + * It is mostly useful for the cases where a record element is shared + * between various views, such as a one2many with a tree and a form view. + * + * @param {string} recordID a valid element ID + * @param {Object} attrs the new field attrs + * @return {Object} the previous field attrs + */ + setFieldAttrs: function (recordID, attrs) { + var record = this.localData[recordID]; + var result = record.fieldAttrs; + record.fieldAttrs = attrs; + return result; + }, /** * For list resources, this changes the orderedBy key, then performs the * sort directly, in javascript. This is used for sorting static datasets, -- cgit v1.2.3 From b1bba768f28a7dd315bddb3d3d4e97fb58919996 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 27 Feb 2017 13:34:43 +0100 Subject: [FIX] web: correctly apply changes on relational fields The relational record may contain relational fields as well. --- addons/web/static/src/js/views/basic/basic_model.js | 4 +--- 1 file changed, 1 insertion(+), 3 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 bc36f454914..12738a1235d 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1154,9 +1154,7 @@ var BasicModel = AbstractModel.extend({ defs.push(this.addDefaultRecord(list.id)); break; case 'UPDATE': - var changedRecord = this.localData[command.id]; - changedRecord._changes = changedRecord._changes || {}; - _.extend(changedRecord._changes, _.pick(command.data, list.fieldNames)); + defs.push(this.notifyChanges(command.id, command.data)); break; case 'REMOVE': list._changes = _.without(list._changes, command.id); -- cgit v1.2.3 From 8d2336abb340d06c64ff62c398e4ec7f7eabe54c Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 24 Feb 2017 12:09:56 +0100 Subject: [FIX] web: default_get on x2many may return a list of ids --- .../web/static/src/js/views/basic/basic_model.js | 69 ++++++++++++++-------- 1 file changed, 43 insertions(+), 26 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 12738a1235d..a66e3a9d3ab 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -500,38 +500,55 @@ var BasicModel = AbstractModel.extend({ }); record._changes[name] = x2manyList.id; var many2ones = {}; - _.each(result[name], function (command) { - if (command[0] === 0) { - // CREATE - var r = self._makeDataPoint({ + var r; + _.each(result[name], function (value) { + if (_.isArray(value)) { + // value is a command + if (value[0] === 0) { + // CREATE + r = self._makeDataPoint({ + modelName: x2manyList.model, + context: x2manyList.context, + fields: field.relatedFields, + fieldAttrs: field.fieldAttrs, + }); + x2manyList._changes.push(r.id); + r._changes = value[2]; + + // this is necessary so the fields are initialized + for (var fieldName in value[2]) { + r.data[fieldName] = null; + } + + for (var name in r._changes) { + if (r.fields[name].type === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: r.fields[name].relation, + data: {id: r._changes[name]} + }); + r._changes[name] = rec.id; + many2ones[name] = true; + } + } + } + if (value[0] === 6) { + // REPLACE_WITH + x2manyList._changes = []; + } + } else { + // value is an id + r = self._makeDataPoint({ modelName: x2manyList.model, context: x2manyList.context, fields: field.relatedFields, fieldAttrs: field.fieldAttrs, + res_id: value, }); - x2manyList._changes.push(r.id); - r._changes = command[2]; - - // this is necessary so the fields are initialized - for (var fieldName in command[2]) { - r.data[fieldName] = null; - } - - for (var name in r._changes) { - if (r.fields[name].type === 'many2one') { - var rec = self._makeDataPoint({ - context: r.context, - modelName: r.fields[name].relation, - data: {id: r._changes[name]} - }); - r._changes[name] = rec.id; - many2ones[name] = true; - } + if (!x2manyList._changes) { + x2manyList._changes = []; } - } - if (command[0] === 6) { - // REPLACE_WITH - x2manyList._changes = []; + x2manyList._changes.push(r.id); } }); -- cgit v1.2.3 From e5740270568404d71a3bc093074a085c9c2eded8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 5 Mar 2017 09:34:56 +0100 Subject: [REF] web,*: remove 'web.model' and 'web.dataModel Use this.rpc instead of Model object, with web.ajax if necessary. nb: EventDispatcherMixin include now the custom_events managment instead of widget --- .../web/static/src/js/views/basic/basic_model.js | 983 +++++++++++---------- 1 file changed, 497 insertions(+), 486 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 a66e3a9d3ab..5c0facecef1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -182,7 +182,7 @@ var BasicModel = AbstractModel.extend({ }); }, /** - * Delete a list of records, then, if a parent_id is given, reload the + * Delete a list of records, then, if a parentID is given, reload the * parent. * * @todo we should remove the deleted records from the localData @@ -193,24 +193,26 @@ var BasicModel = AbstractModel.extend({ * @param {string[]} recordIds list of local resources ids. They should all * be of type 'record', and of the same model * @param {string} modelName - * @param {string} parent_id + * @param {string} parentID * @returns {Deferred} */ - deleteRecords: function (recordIds, modelName, parent_id) { + deleteRecords: function (recordIds, modelName, parentID) { var self = this; var records = _.map(recordIds, function (id) { return self.localData[id]; }); - return this.performModelRPC(modelName, 'unlink', [_.pluck(records, 'res_id')], { - context: session.user_context, // todo: combine with view context - }).then(function () { - _.each(records, function (record) { - record.res_ids.splice(record.offset, 1)[0]; - record.res_id = record.res_ids[record.offset]; - record.count--; + return this.rpc(modelName, 'unlink') + .args([_.pluck(records, 'res_id')]) + .withContext(session.user_context) // todo: combine with view context + .exec() + .then(function () { + _.each(records, function (record) { + record.res_ids.splice(record.offset, 1)[0]; + record.res_id = record.res_ids[record.offset]; + record.count--; + }); + if (parentID) { + return self.reload(parentID); + } }); - if (parent_id) { - return self.reload(parent_id); - } - }); }, /** * Discard all changes in a local resource. Basically, it removes @@ -233,20 +235,22 @@ var BasicModel = AbstractModel.extend({ duplicateRecord: function (recordID) { var self = this; var record = this.localData[recordID]; - return this.performModelRPC(record.model, 'copy', [record.data.id], { - context: this._getContext(record), - }).then(function (res_id) { - var index = record.res_ids.indexOf(record.res_id); - record.res_ids.splice(index + 1, 0, res_id); - return self.load({ - fieldAttrs: record.fieldAttrs, - fieldNames: record.fieldNames, - fields: record.fields, - modelName: record.model, - res_id: res_id, - res_ids: record.res_ids.slice(0), + return this.rpc(record.model, 'copy') + .args([record.data.id]) + .withContext(this._getContext(record)) + .exec() + .then(function (res_id) { + var index = record.res_ids.indexOf(record.res_id); + record.res_ids.splice(index + 1, 0, res_id); + return self.load({ + fieldAttrs: record.fieldAttrs, + fieldNames: record.fieldNames, + fields: record.fields, + modelName: record.model, + res_id: res_id, + res_ids: record.res_ids.slice(0), + }); }); - }); }, /** * The get method first argument is the handle returned by the load method. @@ -442,156 +446,158 @@ var BasicModel = AbstractModel.extend({ var self = this; var fields_key = _.without(params.fieldNames, '__last_update'); - return this.performModelRPC(modelName, 'default_get', [fields_key], { - context: params.context, - }).then(function (result) { - // fill default values for missing fields - for (var i = 0; i < params.fieldNames.length; i++) { - var fieldName = params.fieldNames[i]; - if (!(fieldName in result)) { - var field = params.fields[fieldName]; - if (field.type === 'one2many' || field.type === 'many2many') { - result[fieldName] = []; - } else { - result[fieldName] = null; + return this.rpc(modelName, 'default_get') + .args([fields_key]) + .withContext(params.context) + .exec() + .then(function (result) { + // fill default values for missing fields + for (var i = 0; i < params.fieldNames.length; i++) { + var fieldName = params.fieldNames[i]; + if (!(fieldName in result)) { + var field = params.fields[fieldName]; + if (field.type === 'one2many' || field.type === 'many2many') { + result[fieldName] = []; + } else { + result[fieldName] = null; + } } } - } - var data = {}; - var record = self._makeDataPoint({ - modelName: modelName, - data: data, - fields: params.fields, - fieldNames: params.fieldNames, - fieldAttrs: params.fieldAttrs, - context: params.context, - parentID: params.parentID, - relationField: params.relationField, - res_ids: params.res_ids, - }); + var data = {}; + var record = self._makeDataPoint({ + modelName: modelName, + data: data, + fields: params.fields, + fieldNames: params.fieldNames, + fieldAttrs: params.fieldAttrs, + context: params.context, + parentID: params.parentID, + relationField: params.relationField, + res_ids: params.res_ids, + }); - var defs = []; // FIXME: remove defs? - _.each(params.fieldNames, function (name) { - var field = params.fields[name]; - data[name] = null; - record._changes = record._changes || {}; - if (field.type === 'many2one' && result[name]) { - var rec = self._makeDataPoint({ - context: record.context, - data: {id: result[name]}, - modelName: field.relation, - }); - record._changes[name] = rec.id; - } else if (field.type === 'one2many' || field.type === 'many2many') { - var attrs = record.fieldAttrs[name]; - var x2manyList = self._makeDataPoint({ - context: record.context, - fieldAttrs: field.fieldAttrs, - fields: field.relatedFields, - limit: field.limit, - modelName: field.relation, - parentID: record.id, - rawContext: attrs && attrs.context, - relationField: field.relation_field, - res_ids: [], - static: true, - type: 'list', - }); - record._changes[name] = x2manyList.id; - var many2ones = {}; - var r; - _.each(result[name], function (value) { - if (_.isArray(value)) { - // value is a command - if (value[0] === 0) { - // CREATE + var defs = []; // FIXME: remove defs? + _.each(params.fieldNames, function (name) { + var field = params.fields[name]; + data[name] = null; + record._changes = record._changes || {}; + if (field.type === 'many2one' && result[name]) { + var rec = self._makeDataPoint({ + context: record.context, + data: {id: result[name]}, + modelName: field.relation, + }); + record._changes[name] = rec.id; + } else if (field.type === 'one2many' || field.type === 'many2many') { + var attrs = record.fieldAttrs[name]; + var x2manyList = self._makeDataPoint({ + context: record.context, + fieldAttrs: field.fieldAttrs, + fields: field.relatedFields, + limit: field.limit, + modelName: field.relation, + parentID: record.id, + rawContext: attrs && attrs.context, + relationField: field.relation_field, + res_ids: [], + static: true, + type: 'list', + }); + record._changes[name] = x2manyList.id; + var many2ones = {}; + var r; + _.each(result[name], function (value) { + if (_.isArray(value)) { + // value is a command + if (value[0] === 0) { + // CREATE + r = self._makeDataPoint({ + modelName: x2manyList.model, + context: x2manyList.context, + fields: field.relatedFields, + fieldAttrs: field.fieldAttrs, + }); + x2manyList._changes.push(r.id); + r._changes = value[2]; + + // this is necessary so the fields are initialized + for (var fieldName in value[2]) { + r.data[fieldName] = null; + } + + for (var name in r._changes) { + if (r.fields[name].type === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: r.fields[name].relation, + data: {id: r._changes[name]} + }); + r._changes[name] = rec.id; + many2ones[name] = true; + } + } + } + if (value[0] === 6) { + // REPLACE_WITH + x2manyList._changes = []; + } + } else { + // value is an id r = self._makeDataPoint({ modelName: x2manyList.model, context: x2manyList.context, fields: field.relatedFields, fieldAttrs: field.fieldAttrs, + res_id: value, }); - x2manyList._changes.push(r.id); - r._changes = value[2]; - - // this is necessary so the fields are initialized - for (var fieldName in value[2]) { - r.data[fieldName] = null; - } - - for (var name in r._changes) { - if (r.fields[name].type === 'many2one') { - var rec = self._makeDataPoint({ - context: r.context, - modelName: r.fields[name].relation, - data: {id: r._changes[name]} - }); - r._changes[name] = rec.id; - many2ones[name] = true; - } + if (!x2manyList._changes) { + x2manyList._changes = []; } + x2manyList._changes.push(r.id); } - if (value[0] === 6) { - // REPLACE_WITH - x2manyList._changes = []; + }); + + // fetch many2ones display_name + _.each(_.keys(many2ones), function (name) { + defs.push(self._fetchNameGets(x2manyList, name)); + }); + } else if (field.type === 'date') { + // process date: convert into a moment instance + record._changes[name] = fieldUtils.parse.date(result[name]); + } else if (field.type === 'datetime') { + // process datetime: convert into a moment instance + record._changes[name] = fieldUtils.parse.datetime(result[name]); + } else { + record._changes[name] = result[name]; + } + }); + return $.when.apply($, defs) + .then(function () { + var shouldApplyOnchange = false; + var field; + for (var field_name in record.data) { + field = record.fields[field_name]; + if (field.onChange) { + shouldApplyOnchange = true; } + } + if (shouldApplyOnchange) { + return self._applyOnChange(record, fields_key); } else { - // value is an id - r = self._makeDataPoint({ - modelName: x2manyList.model, - context: x2manyList.context, - fields: field.relatedFields, - fieldAttrs: field.fieldAttrs, - res_id: value, - }); - if (!x2manyList._changes) { - x2manyList._changes = []; - } - x2manyList._changes.push(r.id); + return $.when(); } + }) + .then(function () { + return self._fetchRelationalData(record); + }) + .then(function () { + return self._postprocess(record); + }) + .then(function () { + return record.id; }); - - // fetch many2ones display_name - _.each(_.keys(many2ones), function (name) { - defs.push(self._fetchNameGets(x2manyList, name)); - }); - } else if (field.type === 'date') { - // process date: convert into a moment instance - record._changes[name] = fieldUtils.parse.date(result[name]); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record._changes[name] = fieldUtils.parse.datetime(result[name]); - } else { - record._changes[name] = result[name]; - } }); - return $.when.apply($, defs) - .then(function () { - var shouldApplyOnchange = false; - var field; - for (var field_name in record.data) { - field = record.fields[field_name]; - if (field.onChange) { - shouldApplyOnchange = true; - } - } - if (shouldApplyOnchange) { - return self._applyOnChange(record, fields_key); - } else { - return $.when(); - } - }) - .then(function () { - return self._fetchRelationalData(record); - }) - .then(function () { - return self._postprocess(record); - }) - .then(function () { - return record.id; - }); - }); }, /** * This helper method is designed to help developpers that want to use a @@ -808,21 +814,23 @@ var BasicModel = AbstractModel.extend({ // in the case of a write, only perform the RPC if there are changes to save if (method === 'create' || Object.keys(changes).length) { var args = method === 'write' ? [[record.data.id], changes] : [changes]; - return self.performModelRPC(record.model, method, args, { - context: session.user_context, // todo: combine with view context - }).then(function (id) { - if (method === 'create') { - record.res_id = id; // create returns an id, write returns a boolean - record.data.id = id; - record.offset = record.res_ids.length; - record.res_ids.push(id); - record.count++; - } - // erase changes as they have been applied - record._changes = {}; + return self.rpc(record.model, method) + .args(args) + .withContext(session.user_context) // todo: combine with view context + .exec() + .then(function (id) { + if (method === 'create') { + record.res_id = id; // create returns an id, write returns a boolean + record.data.id = id; + record.offset = record.res_ids.length; + record.res_ids.push(id); + record.count++; + } + // erase changes as they have been applied + record._changes = {}; - return shouldReload ? self._fetchRecord(record) : false; - }); + return shouldReload ? self._fetchRecord(record) : false; + }); } else { return $.when(record_id); } @@ -881,7 +889,7 @@ var BasicModel = AbstractModel.extend({ * @param {Array} recordIDs local ids of the records to (un)archive * @param {boolean} value false to archive, true to unarchive (value of the active field) * @param {string} parentID id of the parent resource to reload - * @returns {Deferred -> string} resolves to the parent id + * @returns {Deferred} resolves to the parent id */ toggleActive: function (recordIDs, value, parentID) { var self = this; @@ -889,8 +897,9 @@ var BasicModel = AbstractModel.extend({ var resIDs = _.map(recordIDs, function (recordID) { return self.localData[recordID].res_id; }); - return this - .performModelRPC(parent.model, 'write', [resIDs, { active: value }]) + return this.rpc(parent.model, 'write') + .args([resIDs, { active: value }]) + .exec() .then(this.reload.bind(this, parentID)); }, @@ -960,11 +969,13 @@ var BasicModel = AbstractModel.extend({ var def; if (!('display_name' in rel_data)) { var field = record.fields[fieldName]; - def = this.performModelRPC(field.relation, 'name_get', [data.id], { - context: record.context, - }).then(function (result) { - rel_data.display_name = result[0][1]; - }); + def = this.rpc(field.relation, 'name_get') + .args([data.id]) + .withContext(record.context) + .exec() + .then(function (result) { + rel_data.display_name = result[0][1]; + }); } return $.when(def).then(function () { var rec = self._makeDataPoint({ @@ -1010,96 +1021,98 @@ var BasicModel = AbstractModel.extend({ fields = fields[0]; } return this.mutex.exec(function () { - var args = [idList, currentData, fields, onchange_spec, context]; - return self.performModelRPC(record.model, 'onchange', args, {}).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 = { + return self.rpc(record.model, 'onchange') + .args([idList, currentData, fields, onchange_spec, context]) + .exec() + .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, - fields: list.fields, - fieldAttrs: list.fieldAttrs, - 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; - } + data: data, + modelName: field.relation, }); - 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 = []; + record._changes[name] = rec.id; } - }); - } 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; + } 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, + fieldAttrs: list.fieldAttrs, + 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; + }); }); - }); }); }, /** @@ -1158,8 +1171,9 @@ var BasicModel = AbstractModel.extend({ // _changes so this is a very specific case) // this could be optimized by registering the fetched records in the list's _cache // so that if a record is removed and then re-added, it won't be fetched twice - var def = this - .performModelRPC(list.model, 'read', [_.pluck(data, 'id'), list.fieldNames], {}) + var def = this.rpc(list.model, 'read') + .args([_.pluck(data, 'id'), list.fieldNames]) + .exec() .then(function (records) { _.each(records, function (record) { list_records[record.id].data = record; @@ -1241,14 +1255,16 @@ var BasicModel = AbstractModel.extend({ _fetchMany2OneGroup: function (group) { var ids = _.uniq(_.pluck(group, 'res_id')); - return this.performModelRPC(group[0].model, 'name_get', [ids], { - context: group[0].context, - }).then(function (name_gets) { - _.each(group, function (record) { - var nameGet = _.find(name_gets, function (n) { return n[0] === record.res_id;}); - record.data.display_name = nameGet[1]; + return this.rpc(group[0].model, 'name_get') + .args([ids]) + .withContext(group[0].context) + .exec() + .then(function (name_gets) { + _.each(group, function (record) { + var nameGet = _.find(name_gets, function (n) { return n[0] === record.res_id;}); + record.data.display_name = nameGet[1]; + }); }); - }); }, _fetchNameGets: function (list, fieldName) { var self = this; @@ -1263,13 +1279,15 @@ var BasicModel = AbstractModel.extend({ model = many2oneRecord.model; return many2oneRecord.res_id; }); - return this.performModelRPC(model, 'name_get', [ids], { - context: list.context, - }).then(function (name_gets) { - for (var i = 0; i < name_gets.length; i++) { - records[i].data.display_name = name_gets[i][1]; - } - }); + return this.rpc(model, 'name_get') + .args([ids]) + .withContext(list.context) + .exec() + .then(function (name_gets) { + for (var i = 0; i < name_gets.length; i++) { + records[i].data.display_name = name_gets[i][1]; + } + }); }, /** * For a given resource of type 'record', fetch all data. @@ -1283,45 +1301,47 @@ var BasicModel = AbstractModel.extend({ var self = this; fieldNames = fieldNames || _.uniq(record.fieldNames.concat(['display_name'])); var oldData = record.data; - return this.performModelRPC(record.model, 'read', [record.res_id, fieldNames], { - // context: { 'bin_size': true }, // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field - }).then(function (result) { - result = result[0]; - record.data = _.extend({}, record.data, result); - }).then(function () { - return self._fetchX2Manys(record, oldData, fieldNames).then(function () { - return self._postprocess(record); - }); - }).then(function (record) { - _.each(fieldNames, function (name) { - var field = record.fields[name]; - var val = record.data[name]; - if (field.type === 'many2one') { - // process many2one: split [id, nameget] and create corresponding record - if (val !== false) { - // the many2one value is of the form [id, display_name] - var r = self._makeDataPoint({ - modelName: field.relation, - data: { - display_name: val[1], - id: val[0], - }, - }); - record.data[name] = r.id; - } else { - // no value for the many2one - record.data[name] = false; + return this.rpc(record.model, 'read') + .args([[record.res_id], fieldNames]) + // .withContext({ bin_size: true }) // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field + .exec() + .then(function (result) { + result = result[0]; + record.data = _.extend({}, record.data, result); + }).then(function () { + return self._fetchX2Manys(record, oldData, fieldNames).then(function () { + return self._postprocess(record); + }); + }).then(function (record) { + _.each(fieldNames, function (name) { + var field = record.fields[name]; + var val = record.data[name]; + if (field.type === 'many2one') { + // process many2one: split [id, nameget] and create corresponding record + if (val !== false) { + // the many2one value is of the form [id, display_name] + var r = self._makeDataPoint({ + modelName: field.relation, + data: { + display_name: val[1], + id: val[0], + }, + }); + record.data[name] = r.id; + } else { + // no value for the many2one + record.data[name] = false; + } + } else if (field.type === 'date') { + // process data: convert into a moment instance + record.data[name] = fieldUtils.parse.date(val); + } else if (field.type === 'datetime') { + // process datetime: convert into a moment instance + record.data[name] = fieldUtils.parse.datetime(val); } - } else if (field.type === 'date') { - // process data: convert into a moment instance - record.data[name] = fieldUtils.parse.date(val); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record.data[name] = fieldUtils.parse.datetime(val); - } + }); + return record; }); - return record; - }); }, /** * This method is incorrectly named. It should be named something like @@ -1473,33 +1493,35 @@ var BasicModel = AbstractModel.extend({ } // step 2: fetch data from server - return this.performModelRPC(field.relation, 'read', [ids, _.keys(field.relatedFields)], { - context: {}, // FIXME - }).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: field.relatedFields, - fieldAttrs: field.fieldAttrs, + return this.rpc(field.relation, 'read') + .args([ids, _.keys(field.relatedFields)]) + .withContext({}) // FIXME + .exec() + .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: field.relatedFields, + fieldAttrs: field.fieldAttrs, + }); }); - }); - _.each(list.data, function (dataPoint) { - var record = self.localData[dataPoint]; - var m2mList = self.localData[record.data[fieldName]]; + _.each(list.data, function (dataPoint) { + var record = self.localData[dataPoint]; + 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; + m2mList.data = []; + _.each(m2mList.res_ids, function (res_id) { + var dataPoint = _.find(dataPoints, function (d) { + return d.res_id === res_id; + }); + m2mList.data.push(dataPoint.id); + m2mList.count++; }); - m2mList.data.push(dataPoint.id); - m2mList.count++; }); }); - }); }, /** * batch request for x2ms for datapoint of type list @@ -1740,46 +1762,50 @@ var BasicModel = AbstractModel.extend({ var _domain = new data.CompoundDomain(field.domain || {}).set_eval_context(field_values); var domain = pyeval.eval('domain', _domain); var fold_field = pyeval.py_eval(attrs.options || '{}').fold_field; - var fetch_status_information = self.performRPC('/web/dataset/search_read', { - model: field.relation, - fields: ['id'].concat(fold_field ? [fold_field] : []), - domain: domain, - }).then(function (result) { - var ids = _.pluck(result.records, 'id'); - return self.performModelRPC(field.relation, 'name_get', [ids], { - context: self._getContext(record, {fieldName: name}), - }).then(function (name_gets) { - _.each(result.records, function (record) { - var name_get = _.find(name_gets, function (n) { - return n[0] === record.id; + var fetch_status_information = self.rpc(field.relation, 'search_read') + .withFields(['id'].concat(fold_field ? [fold_field] : [])) + .withDomain(domain) + .exec() + .then(function (result) { + var ids = _.pluck(result.records, 'id'); + return self.rpc(field.relation, 'name_get') + .args([ids]) + .withContext(self._getContext(record, {fieldName: name})) + .exec() + .then(function (name_gets) { + _.each(result.records, function (record) { + var name_get = _.find(name_gets, function (n) { + return n[0] === record.id; + }); + record.display_name = name_get[1]; + }); + field.__status_information = result.records; }); - record.display_name = name_get[1]; - }); - field.__status_information = result.records; }); - }); defs.push(fetch_status_information); } if (field.__always_reload) { if (record.data[name] instanceof Array) { - defs.push(self.performModelRPC(field.relation, 'name_get', [record.data[name][0]], { - context: self._getContext(record, {fieldName: name}), - }).then(function (result) { - record.data[name] = result[0]; - })); + defs.push(self.rpc(field.relation, 'name_get') + .args([record.data[name][0]]) + .withContext(self._getContext(record, {fieldName: name})) + .exec() + .then(function (result) { + record.data[name] = result[0]; + })); } } if (field.__fetch_selection && !self.many2ones[field.relation]) { - var fetchSelection = self - .performModelRPC(field.relation, 'name_search', [], {}) + var fetchSelection = self.rpc(field.relation, 'name_search') + .exec() .then(function (result) { self.many2ones[field.relation] = result; }); defs.push(fetchSelection); } if (field.__fetch_many2manys) { - var fetchMany2Manys = self - .performModelRPC(field.relation, 'name_search', [], {}) + var fetchMany2Manys = self.rpc(field.relation, 'name_search') + .exec() .then(function (result) { self.many2manys[field.relation] = result; }); @@ -1802,69 +1828,68 @@ var BasicModel = AbstractModel.extend({ _readGroup: function (list) { var self = this; var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); - return this.performModelRPC(list.model, 'read_group', [], { - fields: fields, - context: list.context, - groupby: list.groupedBy, - domain: list.domain, - orderby: false, - lazy: true, - }).then(function (groups) { - var rawGroupBy = list.groupedBy[0].split(':')[0]; - var previousGroups = _.map(list.data, function (groupID) { - return self.localData[groupID]; - }); - list.data = []; - list.count = groups.length; - var defs = []; - - _.each(groups, function (group) { - var aggregateValues = {}; - _.each(group, function (value, key) { - if (_.contains(fields, key) && key !== list.groupedBy[0]) { - aggregateValues[key] = value; - } + return this.rpc(list.model, 'read_group') + .withFields(fields) + .withContext(list.context) + .groupBy(list.groupedBy) + .lazy(true) + .exec() + .then(function (groups) { + var rawGroupBy = list.groupedBy[0].split(':')[0]; + var previousGroups = _.map(list.data, function (groupID) { + return self.localData[groupID]; }); - var newGroup = self._makeDataPoint({ - modelName: list.model, - count: group[rawGroupBy + '_count'], - domain: group.__domain, - context: list.context, - fields: list.fields, - fieldNames: list.fieldNames, - fieldAttrs: list.fieldAttrs, - value: group[rawGroupBy], - aggregateValues: aggregateValues, - groupedBy: list.groupedBy.slice(1), - orderedBy: list.orderedBy, - limit: list.limit, - openGroupByDefault: list.openGroupByDefault, - type: 'list', // nested groupedBys not handled yet + list.data = []; + list.count = groups.length; + var defs = []; + + _.each(groups, function (group) { + var aggregateValues = {}; + _.each(group, function (value, key) { + if (_.contains(fields, key) && key !== list.groupedBy[0]) { + aggregateValues[key] = value; + } + }); + var newGroup = self._makeDataPoint({ + modelName: list.model, + count: group[rawGroupBy + '_count'], + domain: group.__domain, + context: list.context, + fields: list.fields, + fieldNames: list.fieldNames, + fieldAttrs: list.fieldAttrs, + value: group[rawGroupBy], + aggregateValues: aggregateValues, + groupedBy: list.groupedBy.slice(1), + orderedBy: list.orderedBy, + limit: list.limit, + openGroupByDefault: list.openGroupByDefault, + type: 'list', // nested groupedBys not handled yet + }); + list.data.push(newGroup.id); + var old_group = _.find(previousGroups, function (g) { + return g.res_id === newGroup.res_id && g.value === newGroup.value; + }); + if (old_group) { + newGroup.isOpen = old_group.isOpen; + } else if (!newGroup.openGroupByDefault) { + newGroup.isOpen = false; + } else { + newGroup.isOpen = '__fold' in group ? !group.__fold : true; + } + if (newGroup.isOpen && newGroup.count > 0) { + defs.push(self._fetchUngroupedList(newGroup)); + } }); - list.data.push(newGroup.id); - var old_group = _.find(previousGroups, function (g) { - return g.res_id === newGroup.res_id && g.value === newGroup.value; + return $.when.apply($, defs).then(function () { + // generate the res_ids of the main list, being the concatenation + // of the fetched res_ids in each group + list.res_ids = _.flatten(_.map(arguments, function (group) { + return group ? group.res_ids : []; + })); + return list; }); - if (old_group) { - newGroup.isOpen = old_group.isOpen; - } else if (!newGroup.openGroupByDefault) { - newGroup.isOpen = false; - } else { - newGroup.isOpen = '__fold' in group ? !group.__fold : true; - } - if (newGroup.isOpen && newGroup.count > 0) { - defs.push(self._fetchUngroupedList(newGroup)); - } }); - return $.when.apply($, defs).then(function () { - // generate the res_ids of the main list, being the concatenation - // of the fetched res_ids in each group - list.res_ids = _.flatten(_.map(arguments, function (group) { - return group ? group.res_ids : []; - })); - return list; - }); - }); }, /** * For 'static' list, such as one2manys in a form view, we can do a /read @@ -1887,9 +1912,10 @@ var BasicModel = AbstractModel.extend({ } } if (missingIds.length) { - def = this.performModelRPC(list.model, 'read', [missingIds, list.fieldNames], { - context: {}, // FIXME - }); + def = this.rpc(list.model, 'read') + .args([missingIds, list.fieldNames]) + .withContext({}) // FIXME + .exec(); } else { def = $.when(); } @@ -1946,67 +1972,52 @@ var BasicModel = AbstractModel.extend({ */ _searchReadUngroupedList: function (list) { var self = this; - return this.performRPC('/web/dataset/search_read', { - model: list.model, - fields: list.fieldNames, - domain: list.domain || [], - offset: list.offset, - limit: list.limit, - sort: this._serializeSort(list.orderedBy), - }).then(function (result) { - list.count = result.length; - var data = _.map(result.records, function (record) { - var dataPoint = self._makeDataPoint({ - data: record, - fields: list.fields, - fieldNames: list.fieldNames, - fieldAttrs: list.fieldAttrs, - modelName: list.model, - parentID: list.id, - }); + return this.rpc(list.model, 'search_read') + .withFields(list.fieldNames) + .withDomain(list.domain || []) + .withLimit(list.limit) + .withOffset(list.offset) + .orderBy(list.orderedBy) + .exec() + .then(function (result) { + list.count = result.length; + var data = _.map(result.records, function (record) { + var dataPoint = self._makeDataPoint({ + data: record, + fields: list.fields, + fieldNames: list.fieldNames, + fieldAttrs: list.fieldAttrs, + modelName: list.model, + parentID: list.id, + }); - // add many2one records - _.each(dataPoint.fieldNames, function (name) { - var field = dataPoint.fields[name]; - if (field.type === 'many2one') { - var r = self._makeDataPoint({ - modelName: field.relation, - data: { - id: dataPoint.data[name][0], - display_name: dataPoint.data[name][1], - } - }); + // add many2one records + _.each(dataPoint.fieldNames, function (name) { + var field = dataPoint.fields[name]; + if (field.type === 'many2one') { + var r = self._makeDataPoint({ + modelName: field.relation, + data: { + id: dataPoint.data[name][0], + display_name: dataPoint.data[name][1], + } + }); - dataPoint.data[name] = r.id; - } - if (field.type === 'date') { - dataPoint.data[name] = fieldUtils.parse.date(dataPoint.data[name]); - } - if (field.type === 'datetime') { - dataPoint.data[name] = fieldUtils.parse.datetime(dataPoint.data[name]); - } + dataPoint.data[name] = r.id; + } + if (field.type === 'date') { + dataPoint.data[name] = fieldUtils.parse.date(dataPoint.data[name]); + } + if (field.type === 'datetime') { + dataPoint.data[name] = fieldUtils.parse.datetime(dataPoint.data[name]); + } + }); + return dataPoint.id; }); - return dataPoint.id; + list.data = data; + list.res_ids = _.pluck(result.records, 'id'); + return list; }); - list.data = data; - list.res_ids = _.pluck(result.records, 'id'); - return list; - }); - }, - /** - * Helper method, generates a string to describe a ordered by sequence for - * SQL. - * - * For example, [{name: 'foo', asc: true}, {name: 'bar', asc: false}] will - * be converted into 'foo ASC, bar DESC' - * - * @param {Object[]} orderBy list of objects {name:..., asc: ...} - * @returns {string} - */ - _serializeSort: function (orderBy) { - return _.map(orderBy, function (order) { - return order.asc ? order.name + ' ASC' : order.name + ' DESC'; - }).join(', '); }, /** * Change the offset of a record. Note that this does not reload the data. -- cgit v1.2.3 From 5804ed100ec790a36c5ff30a217914950cd78098 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Tue, 28 Feb 2017 11:48:00 +0100 Subject: [IMP] web, *: compute contexts and domains from datapoint This commit tries to refactore the management of contexts and domains in the webclient. - Get rid of CompoundDomain: use standard array manipulation to join multiple domains or use the Domain class (to be improved) - Get rid of data.js build_domain and build_context methods - Link this.record to the data point from the basic model for all fields (not only relational fields) - Add a getContext and a getDomain function to the basic model data point (localData element) so that AbstractField implementations can use them thanks to their this.record element. - Remove the autocontext binding for field RPCs (see FieldManagerMixin) --- .../web/static/src/js/views/basic/basic_model.js | 99 ++++++++++++++++------ 1 file changed, 71 insertions(+), 28 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 5c0facecef1..2c7ec01f9c1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -79,7 +79,7 @@ odoo.define('web.BasicModel', function (require) { var AbstractModel = require('web.AbstractModel'); var concurrency = require('web.concurrency'); var Context = require('web.Context'); -var data = require('web.data'); // TODO: remove dependency to data.js +var Domain = require('web.Domain'); var fieldUtils = require('web.field_utils'); var pyeval = require('web.pyeval'); var session = require('web.session'); @@ -365,6 +365,10 @@ var BasicModel = AbstractModel.extend({ delete element.parentID; delete element.relation_field; delete element.rawContext; + + element.getContext = this._getContext.bind(this, record); + element.getDomain = this._getDomain.bind(this, record); + return element; }, /** @@ -929,20 +933,6 @@ var BasicModel = AbstractModel.extend({ }); } }, - /** - * Every RPC done by the model need to add some context, which is a - * combination of the context of the session, of the record/list, and/or of - * the concerned field. This method combines all these contexts and evaluate - * them with the proper evalcontext. - * - * @param {string} recordID - the id of an element from the localData - * @see _getContext for the options parameter - * @returns {Object} the evaluated context - */ - getContext: function (recordID, options) { - var record = this.localData[recordID]; - return this._getContext(record, options); - }, //-------------------------------------------------------------------------- // Private @@ -1626,25 +1616,22 @@ var BasicModel = AbstractModel.extend({ */ _getContext: function (element, options) { var context = new Context(session.user_context, element.context); - var evalContext = this.get(element.id, {raw: true}).data; - if (element.parentID) { - var parent = this.get(element.parentID, {raw: true}); - if (parent.type === 'list' && this.localData[element.parentID].parentID) { - parent = this.get(this.localData[element.parentID].parentID, {raw: true}); - } - _.extend(evalContext, {parent: parent.data}); - } - context.set_eval_context(evalContext); + context.set_eval_context(this._getEvalContext(element)); + if (options && options.fieldName) { var attrs = element.fieldAttrs[options.fieldName]; if (attrs && attrs.context) { context.add(attrs.context); + } else { + var fieldParams = element.fields[options.fieldName]; + if (fieldParams.context) { + context.add(fieldParams.context); + } } } - if (element.rawContext) { var rawContext = new Context(element.rawContext); - var evalContext = this.get(element.parentID, {raw: true}).data; + var evalContext = this._getEvalContext(this.localData[element.parentID]); evalContext.id = evalContext.id || false; rawContext.set_eval_context(evalContext); context.add(rawContext); @@ -1652,6 +1639,63 @@ var BasicModel = AbstractModel.extend({ return context.eval(); }, + /** + * Some records are associated to a/some domain(s). This method allows to + * retrieve them, evaluated. + * + * @param {Object} element an element from the localData + * @param {Object} [options] + * @param {string} [options.fieldName] + * the name of the field whose domain needs to be returned + * @returns {Array} the evaluated domain + */ + _getDomain: function (element, options) { + if (options && options.fieldName) { + var attrs = element.fieldAttrs[options.fieldName]; + if (attrs && attrs.domain) { + return Domain.prototype.stringToArray( + attrs.domain, + this._getEvalContext(element) + ); + } + var fieldParams = element.fields[options.fieldName]; + if (fieldParams.domain) { + return Domain.prototype.stringToArray( + fieldParams.domain, + this._getEvalContext(element) + ); + } + return []; + } + + return Domain.prototype.stringToArray( + element.domain, + this._getEvalContext(element) + ); + }, + /** + * Returns the evaluation context that should be used when evaluating the + * context/domain associated to a given element from the localData. + * + * @param {Object} element - an element from the localData + * @returns {Object} + */ + _getEvalContext: function (element) { + var evalContext = this.get(element.id, {raw: true}).data; + evalContext.active_model = element.model; + if (evalContext.id) { + evalContext.active_id = evalContext.id; + evalContext.active_ids = [evalContext.id]; + } + if (element.parentID) { + var parent = this.get(element.parentID, {raw: true}); + if (parent.type === 'list' && this.localData[element.parentID].parentID) { + parent = this.get(this.localData[element.parentID].parentID, {raw: true}); + } + _.extend(evalContext, {parent: parent.data}); + } + return evalContext; + }, /** * Helper method for the load entry point. * @@ -1759,8 +1803,7 @@ var BasicModel = AbstractModel.extend({ } return val; }); - var _domain = new data.CompoundDomain(field.domain || {}).set_eval_context(field_values); - var domain = pyeval.eval('domain', _domain); + var domain = Domain.prototype.stringToArray(field.domain || [], field_values); var fold_field = pyeval.py_eval(attrs.options || '{}').fold_field; var fetch_status_information = self.rpc(field.relation, 'search_read') .withFields(['id'].concat(fold_field ? [fold_field] : [])) -- cgit v1.2.3 From a9475e14af861899d56259908e7f0f703fd35d1b Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Mon, 6 Mar 2017 12:09:08 +0100 Subject: [FIX] web: basic_model: try to call name_search without name value --- addons/web/static/src/js/views/basic/basic_model.js | 2 ++ 1 file changed, 2 insertions(+) (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 2c7ec01f9c1..119be6e7482 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1840,6 +1840,7 @@ var BasicModel = AbstractModel.extend({ } if (field.__fetch_selection && !self.many2ones[field.relation]) { var fetchSelection = self.rpc(field.relation, 'name_search') + .args(['', field.domain]) .exec() .then(function (result) { self.many2ones[field.relation] = result; @@ -1848,6 +1849,7 @@ var BasicModel = AbstractModel.extend({ } if (field.__fetch_many2manys) { var fetchMany2Manys = self.rpc(field.relation, 'name_search') + .args(['', field.domain]) .exec() .then(function (result) { self.many2manys[field.relation] = result; -- cgit v1.2.3 From 04f1e50ff360d8621cc42b2fec2041740c852252 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Mon, 6 Mar 2017 12:55:55 +0100 Subject: [FIX] web: date value must be parsed to moment object --- addons/web/static/src/js/views/basic/basic_model.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) (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 119be6e7482..f04cac96400 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1167,6 +1167,18 @@ var BasicModel = AbstractModel.extend({ .then(function (records) { _.each(records, function (record) { list_records[record.id].data = record; + _.each(record, function (val, fieldName) { + var field = list.fields[fieldName]; + if (field.type === 'date') { + // process data: convert into a moment instance + record[fieldName] = fieldUtils.parse.date(val); + } else if (field.type === 'datetime') { + // process datetime: convert into a moment instance + record[fieldName] = fieldUtils.parse.datetime(val); + } else { + record[fieldName] = val; + } + }); }); }); defs.push(def); -- cgit v1.2.3 From 86a87aa8b0dde2237cfe1bb769724e8ed269b62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 7 Mar 2017 16:14:05 +0100 Subject: [REF] *: use _rpc method for all rpcs The _rpc method is now the preferred method to do a rpc, so we need to update all calls. --- .../web/static/src/js/views/basic/basic_model.js | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 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 f04cac96400..04c80764e00 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -199,7 +199,7 @@ var BasicModel = AbstractModel.extend({ deleteRecords: function (recordIds, modelName, parentID) { var self = this; var records = _.map(recordIds, function (id) { return self.localData[id]; }); - return this.rpc(modelName, 'unlink') + return this._rpc(modelName, 'unlink') .args([_.pluck(records, 'res_id')]) .withContext(session.user_context) // todo: combine with view context .exec() @@ -235,7 +235,7 @@ var BasicModel = AbstractModel.extend({ duplicateRecord: function (recordID) { var self = this; var record = this.localData[recordID]; - return this.rpc(record.model, 'copy') + return this._rpc(record.model, 'copy') .args([record.data.id]) .withContext(this._getContext(record)) .exec() @@ -450,7 +450,7 @@ var BasicModel = AbstractModel.extend({ var self = this; var fields_key = _.without(params.fieldNames, '__last_update'); - return this.rpc(modelName, 'default_get') + return this._rpc(modelName, 'default_get') .args([fields_key]) .withContext(params.context) .exec() @@ -818,7 +818,7 @@ var BasicModel = AbstractModel.extend({ // in the case of a write, only perform the RPC if there are changes to save if (method === 'create' || Object.keys(changes).length) { var args = method === 'write' ? [[record.data.id], changes] : [changes]; - return self.rpc(record.model, method) + return self._rpc(record.model, method) .args(args) .withContext(session.user_context) // todo: combine with view context .exec() @@ -901,7 +901,7 @@ var BasicModel = AbstractModel.extend({ var resIDs = _.map(recordIDs, function (recordID) { return self.localData[recordID].res_id; }); - return this.rpc(parent.model, 'write') + return this._rpc(parent.model, 'write') .args([resIDs, { active: value }]) .exec() .then(this.reload.bind(this, parentID)); @@ -959,7 +959,7 @@ var BasicModel = AbstractModel.extend({ var def; if (!('display_name' in rel_data)) { var field = record.fields[fieldName]; - def = this.rpc(field.relation, 'name_get') + def = this._rpc(field.relation, 'name_get') .args([data.id]) .withContext(record.context) .exec() @@ -1011,7 +1011,7 @@ var BasicModel = AbstractModel.extend({ fields = fields[0]; } return this.mutex.exec(function () { - return self.rpc(record.model, 'onchange') + return self._rpc(record.model, 'onchange') .args([idList, currentData, fields, onchange_spec, context]) .exec() .then(function (result) { @@ -1161,7 +1161,7 @@ var BasicModel = AbstractModel.extend({ // _changes so this is a very specific case) // this could be optimized by registering the fetched records in the list's _cache // so that if a record is removed and then re-added, it won't be fetched twice - var def = this.rpc(list.model, 'read') + var def = this._rpc(list.model, 'read') .args([_.pluck(data, 'id'), list.fieldNames]) .exec() .then(function (records) { @@ -1257,7 +1257,7 @@ var BasicModel = AbstractModel.extend({ _fetchMany2OneGroup: function (group) { var ids = _.uniq(_.pluck(group, 'res_id')); - return this.rpc(group[0].model, 'name_get') + return this._rpc(group[0].model, 'name_get') .args([ids]) .withContext(group[0].context) .exec() @@ -1281,7 +1281,7 @@ var BasicModel = AbstractModel.extend({ model = many2oneRecord.model; return many2oneRecord.res_id; }); - return this.rpc(model, 'name_get') + return this._rpc(model, 'name_get') .args([ids]) .withContext(list.context) .exec() @@ -1303,7 +1303,7 @@ var BasicModel = AbstractModel.extend({ var self = this; fieldNames = fieldNames || _.uniq(record.fieldNames.concat(['display_name'])); var oldData = record.data; - return this.rpc(record.model, 'read') + return this._rpc(record.model, 'read') .args([[record.res_id], fieldNames]) // .withContext({ bin_size: true }) // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field .exec() @@ -1495,7 +1495,7 @@ var BasicModel = AbstractModel.extend({ } // step 2: fetch data from server - return this.rpc(field.relation, 'read') + return this._rpc(field.relation, 'read') .args([ids, _.keys(field.relatedFields)]) .withContext({}) // FIXME .exec() @@ -1817,13 +1817,13 @@ var BasicModel = AbstractModel.extend({ }); var domain = Domain.prototype.stringToArray(field.domain || [], field_values); var fold_field = pyeval.py_eval(attrs.options || '{}').fold_field; - var fetch_status_information = self.rpc(field.relation, 'search_read') + var fetch_status_information = self._rpc(field.relation, 'search_read') .withFields(['id'].concat(fold_field ? [fold_field] : [])) .withDomain(domain) .exec() .then(function (result) { var ids = _.pluck(result.records, 'id'); - return self.rpc(field.relation, 'name_get') + return self._rpc(field.relation, 'name_get') .args([ids]) .withContext(self._getContext(record, {fieldName: name})) .exec() @@ -1841,7 +1841,7 @@ var BasicModel = AbstractModel.extend({ } if (field.__always_reload) { if (record.data[name] instanceof Array) { - defs.push(self.rpc(field.relation, 'name_get') + defs.push(self._rpc(field.relation, 'name_get') .args([record.data[name][0]]) .withContext(self._getContext(record, {fieldName: name})) .exec() @@ -1851,7 +1851,7 @@ var BasicModel = AbstractModel.extend({ } } if (field.__fetch_selection && !self.many2ones[field.relation]) { - var fetchSelection = self.rpc(field.relation, 'name_search') + var fetchSelection = self._rpc(field.relation, 'name_search') .args(['', field.domain]) .exec() .then(function (result) { @@ -1860,7 +1860,7 @@ var BasicModel = AbstractModel.extend({ defs.push(fetchSelection); } if (field.__fetch_many2manys) { - var fetchMany2Manys = self.rpc(field.relation, 'name_search') + var fetchMany2Manys = self._rpc(field.relation, 'name_search') .args(['', field.domain]) .exec() .then(function (result) { @@ -1885,7 +1885,7 @@ var BasicModel = AbstractModel.extend({ _readGroup: function (list) { var self = this; var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); - return this.rpc(list.model, 'read_group') + return this._rpc(list.model, 'read_group') .withFields(fields) .withContext(list.context) .groupBy(list.groupedBy) @@ -1969,7 +1969,7 @@ var BasicModel = AbstractModel.extend({ } } if (missingIds.length) { - def = this.rpc(list.model, 'read') + def = this._rpc(list.model, 'read') .args([missingIds, list.fieldNames]) .withContext({}) // FIXME .exec(); @@ -2029,7 +2029,7 @@ var BasicModel = AbstractModel.extend({ */ _searchReadUngroupedList: function (list) { var self = this; - return this.rpc(list.model, 'search_read') + return this._rpc(list.model, 'search_read') .withFields(list.fieldNames) .withDomain(list.domain || []) .withLimit(list.limit) -- cgit v1.2.3 From 7a1d7cb2414f708a1d9f167412747bf98e2654e7 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 7 Mar 2017 16:29:09 +0100 Subject: [FIX] web: allow edition of one2many with widget many2many It is quite rare, but it happens that a one2many field has to be edited with a many2many widget: it allows the fact that records are directly created, and only later linked to the main record. But the BasicModel only used the field type to determine which commands it had to send. In this commit, we introduce a magic flag _forceM2MLink, which, if set to true, tells the BasicModel to treat the field as a many2many. --- addons/web/static/src/js/views/basic/basic_model.js | 5 ++++- 1 file changed, 4 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 04c80764e00..8afd87aedfe 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -361,6 +361,7 @@ var BasicModel = AbstractModel.extend({ delete element._cache; delete element._changes; + delete element._forceM2MLink; delete element.static; delete element.parentID; delete element.relation_field; @@ -1141,6 +1142,8 @@ var BasicModel = AbstractModel.extend({ list._changes.push(newRecord.id); break; case 'ADD_M2M': + // force to use link command instead of create command + list._forceM2MLink = true; // handle multiple add: command[2] may be a dict of values (1 // record added) or an array of dict of values var data = _.isArray(command.ids) ? command.ids : [command.ids]; @@ -1576,7 +1579,7 @@ var BasicModel = AbstractModel.extend({ relIds = _.pluck(relData, 'res_id'); commands[fieldName] = []; - if (type === 'many2many') { + 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 -- cgit v1.2.3 From 23f3d9524835d95473226aa566054344fe7cf349 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 7 Mar 2017 16:46:30 +0100 Subject: [REF] web: factorise some code in BasicModel Each data information coming from the server was serialized, and need to be deserialized in the proper format. This was done in 3 different places. --- .../web/static/src/js/views/basic/basic_model.js | 119 +++++++-------------- 1 file changed, 41 insertions(+), 78 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 8afd87aedfe..8239692711e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1170,18 +1170,7 @@ var BasicModel = AbstractModel.extend({ .then(function (records) { _.each(records, function (record) { list_records[record.id].data = record; - _.each(record, function (val, fieldName) { - var field = list.fields[fieldName]; - if (field.type === 'date') { - // process data: convert into a moment instance - record[fieldName] = fieldUtils.parse.date(val); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record[fieldName] = fieldUtils.parse.datetime(val); - } else { - record[fieldName] = val; - } - }); + self._parseServerData(list.fieldNames, list.fields, record); }); }); defs.push(def); @@ -1318,33 +1307,7 @@ var BasicModel = AbstractModel.extend({ return self._postprocess(record); }); }).then(function (record) { - _.each(fieldNames, function (name) { - var field = record.fields[name]; - var val = record.data[name]; - if (field.type === 'many2one') { - // process many2one: split [id, nameget] and create corresponding record - if (val !== false) { - // the many2one value is of the form [id, display_name] - var r = self._makeDataPoint({ - modelName: field.relation, - data: { - display_name: val[1], - id: val[0], - }, - }); - record.data[name] = r.id; - } else { - // no value for the many2one - record.data[name] = false; - } - } else if (field.type === 'date') { - // process data: convert into a moment instance - record.data[name] = fieldUtils.parse.date(val); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record.data[name] = fieldUtils.parse.datetime(val); - } - }); + self._parseServerData(fieldNames, record.fields, record.data); return record; }); }, @@ -1794,6 +1757,43 @@ var BasicModel = AbstractModel.extend({ return dataPoint; }, + /** + * parse the server values to javascript framwork + * + * @param {[string]} fieldNames + * @param {Object} fields + * @param {Object} record + */ + _parseServerData: function (fieldNames, fields, record) { + var self = this; + _.each(fieldNames, function (fieldName) { + var field = fields[fieldName]; + var val = record[fieldName]; + if (field.type === 'many2one') { + // process many2one: split [id, nameget] and create corresponding record + if (val !== false) { + // the many2one value is of the form [id, display_name] + var r = self._makeDataPoint({ + modelName: field.relation, + data: { + display_name: val[1], + id: val[0], + }, + }); + record[fieldName] = r.id; + } else { + // no value for the many2one + record[fieldName] = false; + } + } else if (field.type === 'date') { + // process data: convert into a moment instance + record[fieldName] = fieldUtils.parse.date(val); + } else if (field.type === 'datetime') { + // process datetime: convert into a moment instance + record[fieldName] = fieldUtils.parse.datetime(val); + } + }); + }, /** * Once a record is created and some data has been fetched, we need to do * quite a lot of computations to determine what needs to be fetched. This @@ -1996,25 +1996,7 @@ var BasicModel = AbstractModel.extend({ }); // add many2one records - _.each(dataPoint.fieldNames, function (name) { - var field = dataPoint.fields[name]; - if (field.type === 'many2one') { - var r = self._makeDataPoint({ - modelName: field.relation, - data: { - id: dataPoint.data[name][0], - display_name: dataPoint.data[name][1], - }, - parentID: dataPoint.id, - }); - - dataPoint.data[name] = r.id; - } else if (field.type === 'date') { - dataPoint.data[name] = fieldUtils.parse.date(dataPoint.data[name]); - } else if (field.type === 'datetime') { - dataPoint.data[name] = fieldUtils.parse.datetime(dataPoint.data[name]); - } - }); + self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); list._cache[id] = dataPoint.id; } list.data.push(dataPoint.id); @@ -2052,26 +2034,7 @@ var BasicModel = AbstractModel.extend({ }); // add many2one records - _.each(dataPoint.fieldNames, function (name) { - var field = dataPoint.fields[name]; - if (field.type === 'many2one') { - var r = self._makeDataPoint({ - modelName: field.relation, - data: { - id: dataPoint.data[name][0], - display_name: dataPoint.data[name][1], - } - }); - - dataPoint.data[name] = r.id; - } - if (field.type === 'date') { - dataPoint.data[name] = fieldUtils.parse.date(dataPoint.data[name]); - } - if (field.type === 'datetime') { - dataPoint.data[name] = fieldUtils.parse.datetime(dataPoint.data[name]); - } - }); + self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); return dataPoint.id; }); list.data = data; -- cgit v1.2.3 From 454b21538917558cfb71c0b98bf49a1d8efa7eee Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 8 Mar 2017 15:49:52 +0100 Subject: [FIX] web: properly transfer field properties Before this commit, the field properties (attrs, fieldNames, fields) were not correctly given to sub views in many case expected: * use widget many2many on one2many field * create record when click on save in formDialog * do not send create command to the server * use the fields defined in the field node (instead of tree view fields) * use the fieldNames instead of the record to get field values --- .../web/static/src/js/views/basic/basic_model.js | 32 ++++++++++++++++++---- 1 file changed, 26 insertions(+), 6 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 8239692711e..992af62387e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -302,6 +302,9 @@ var BasicModel = AbstractModel.extend({ for (var fieldName in element.data) { field = element.fields[fieldName]; + if (!field) { + continue; + } // get relational datapoint if (field.type === 'many2one') { @@ -842,18 +845,32 @@ var BasicModel = AbstractModel.extend({ }); }, /** - * Set and return the fieldattrs on a record element. + * Set fieldNames. + * Set fields on a record element (current fields as defaults) + * Set fieldattrs. * It is mostly useful for the cases where a record element is shared * between various views, such as a one2many with a tree and a form view. * * @param {string} recordID a valid element ID - * @param {Object} attrs the new field attrs - * @return {Object} the previous field attrs + * @param {Object} props + * @param {Object} props.fieldNames + * @param {Object} props.fields + * @param {Object} props.fieldAttrs + * @return {Object} + * @return {Object} result.fieldNames the previous field names + * @return {Object} result.fields the previous description fields + * @return {Object} result.fieldattrs the previous field attrs */ - setFieldAttrs: function (recordID, attrs) { + setFieldProps: function (recordID, props) { var record = this.localData[recordID]; - var result = record.fieldAttrs; - record.fieldAttrs = attrs; + var result = { + fieldNames: record.fieldNames, + fields: record.fields, + fieldAttrs: record.fieldAttrs, + }; + record.fieldNames = props.fieldNames; + record.fields = _.defaults(props.fields, record.fields); + record.fieldAttrs = props.fieldAttrs; return result; }, /** @@ -2100,6 +2117,9 @@ var BasicModel = AbstractModel.extend({ if (element.type === 'record') { for (var fieldName in element.data) { var field = element.fields[fieldName]; + if (!field) { + continue; + } if (_.contains(['one2many', 'many2one', 'many2many'], field.type)) { var relationalElement = this.localData[element.data[fieldName]]; -- cgit v1.2.3 From 61b09bbe47ca88f411840778a2134d8c7f860338 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 9 Mar 2017 15:42:51 +0100 Subject: [IMP] web: handle required attributes and modifiers in form view --- addons/web/static/src/js/views/basic/basic_model.js | 3 +++ 1 file changed, 3 insertions(+) (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 992af62387e..764406eca61 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -302,6 +302,9 @@ var BasicModel = AbstractModel.extend({ for (var fieldName in element.data) { field = element.fields[fieldName]; + if (element.data[fieldName] === null) { + element.data[fieldName] = false; + } if (!field) { continue; } -- cgit v1.2.3 From 228e18e81ae2439c307825cd8127e800dfec571c Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 8 Mar 2017 16:03:28 +0100 Subject: [REF] web, mail: include evaled field options in datapoint Field options do not depend on any context/data. They can so be "computed" once and for all in the BasicView implementation instead of in the AbstractField's one. Also, remove the flag __always_reload and leave it be in the options object (always_reload). --- addons/web/static/src/js/views/basic/basic_model.js | 9 ++++----- 1 file changed, 4 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 764406eca61..ab2b7dcbbee 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -81,7 +81,6 @@ var concurrency = require('web.concurrency'); var Context = require('web.Context'); var Domain = require('web.Domain'); var fieldUtils = require('web.field_utils'); -var pyeval = require('web.pyeval'); var session = require('web.session'); var x2ManyCommands = { @@ -1829,7 +1828,8 @@ var BasicModel = AbstractModel.extend({ var defs = []; _.each(record.fieldNames, function (name) { var field = record.fields[name]; - var attrs = record.fieldAttrs[name]; + var attrs = record.fieldAttrs[name] || {}; + var options = attrs.options || {}; if (field.__fetch_status && !field.__status_information) { var field_values = _.mapObject(record.data, function (val, key) { var fieldType = record.fields[key].type; @@ -1839,9 +1839,8 @@ var BasicModel = AbstractModel.extend({ return val; }); var domain = Domain.prototype.stringToArray(field.domain || [], field_values); - var fold_field = pyeval.py_eval(attrs.options || '{}').fold_field; var fetch_status_information = self._rpc(field.relation, 'search_read') - .withFields(['id'].concat(fold_field ? [fold_field] : [])) + .withFields(['id'].concat(options.fold_field ? [options.fold_field] : [])) .withDomain(domain) .exec() .then(function (result) { @@ -1862,7 +1861,7 @@ var BasicModel = AbstractModel.extend({ }); defs.push(fetch_status_information); } - if (field.__always_reload) { + if (options.always_reload) { if (record.data[name] instanceof Array) { defs.push(self._rpc(field.relation, 'name_get') .args([record.data[name][0]]) -- cgit v1.2.3 From c24d1f615ca549ff134fbd250afafc359a591431 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Thu, 9 Mar 2017 17:04:04 +0100 Subject: [REF] web: make getContext and getDomain part of dataPoints Before this commit, the getContext and getDomain functions of a dataPoint were only available from a BasicModel "get" call result. As the code is moving towards the creation of a DataPoint class, these two functions should be a part of the dataPoint itself. --- addons/web/static/src/js/views/basic/basic_model.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 ab2b7dcbbee..9f2242751e6 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -26,6 +26,8 @@ odoo.define('web.BasicModel', function (require) { * fieldNames: {string[]}, * fields: {Object}, * fieldAttrs: {Object}, + * getContext: {function}, + * getDomain: {function}, * groupedBy: {string[]}, * id: {integer}, * isOpen: {boolean}, @@ -372,9 +374,6 @@ var BasicModel = AbstractModel.extend({ delete element.relation_field; delete element.rawContext; - element.getContext = this._getContext.bind(this, record); - element.getDomain = this._getDomain.bind(this, record); - return element; }, /** @@ -1772,6 +1771,9 @@ var BasicModel = AbstractModel.extend({ value: value, }; + dataPoint.getContext = this._getContext.bind(this, dataPoint); + dataPoint.getDomain = this._getDomain.bind(this, dataPoint); + this.localData[dataPoint.id] = dataPoint; return dataPoint; -- cgit v1.2.3 From ba1a2d7145606e22ae316a636a1852a7ae80a3df Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Fri, 10 Mar 2017 12:27:57 +0100 Subject: [FIX] web: DataModel _postprocess was called at a wrong time The _postprocess method should always be called after _parseServerData, otherwise the localData between each call is inconsistent for the relational fields (Array or key pointing to another localData). --- addons/web/static/src/js/views/basic/basic_model.js | 19 +++++++++++-------- 1 file changed, 11 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 9f2242751e6..f6c2021f2e8 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1320,13 +1320,14 @@ var BasicModel = AbstractModel.extend({ .then(function (result) { result = result[0]; record.data = _.extend({}, record.data, result); - }).then(function () { + }) + .then(function () { + self._parseServerData(fieldNames, record.fields, record.data); + }) + .then(function () { return self._fetchX2Manys(record, oldData, fieldNames).then(function () { return self._postprocess(record); }); - }).then(function (record) { - self._parseServerData(fieldNames, record.fields, record.data); - return record; }); }, /** @@ -1836,7 +1837,8 @@ var BasicModel = AbstractModel.extend({ var field_values = _.mapObject(record.data, function (val, key) { var fieldType = record.fields[key].type; if (fieldType === 'many2one') { - return val instanceof Array && val[0]; + var element = self.localData[val]; + return element && element.data.id || false; } return val; }); @@ -1864,13 +1866,14 @@ var BasicModel = AbstractModel.extend({ defs.push(fetch_status_information); } if (options.always_reload) { - if (record.data[name] instanceof Array) { + if (record.fields[name].type === 'many2one' && record.data[name]) { + var element = self.localData[record.data[name]]; defs.push(self._rpc(field.relation, 'name_get') - .args([record.data[name][0]]) + .args([element.data.id]) .withContext(self._getContext(record, {fieldName: name})) .exec() .then(function (result) { - record.data[name] = result[0]; + element.data.display_name = result[0][1]; })); } } -- cgit v1.2.3 From b5b649e5b21d41279f12f0dbe6cfa53ca2c52580 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Fri, 10 Mar 2017 12:28:26 +0100 Subject: [FIX] web: fetching special data in field widgets Some field widgets need additional data to render themself. These data should ideally be fetched in the data fetching phase. In 10.0, these data were fetched during field rendering which implied that the data fetching was done sequentially. To limit this effect, before this commit, these special fields were flagged by the basic view according to some field properties and the basic model flagged the fields with the required data. Now, the system has been improved. The field widget classes can define a key "specialData" which refers to a BasicModel method. These methods will be used to fetch the required data at the right time and these data will be available at `this.record.specialData[this.name]` in the AbstractField instance. The new system also fixes a bug: the required data was fetched only at record loading but should in fact be re-fetched at each view change. --- .../web/static/src/js/views/basic/basic_model.js | 221 ++++++++++++++------- 1 file changed, 152 insertions(+), 69 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 f6c2021f2e8..cdb8bb61f7c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -41,6 +41,8 @@ odoo.define('web.BasicModel', function (require) { * relationField: {string}, * res_id: {integer|null}, * res_ids: {integer[]}, + * specialData: {Object}, + * _specialDataCache: {Object}, * static: {boolean}, * type: {string} 'record' | 'list' * value: ?, @@ -133,14 +135,6 @@ var BasicModel = AbstractModel.extend({ // save is performed. this.mutex = new concurrency.Mutex(); - // this dictionary is used to hold all many2one informations for widget - // selection. - this.many2ones = {}; - - // this dictionary is used to hold all many2many informations for widget - // many2many_checkboxes. - this.many2manys = {}; - this.localData = Object.create(null); this._super.apply(this, arguments); }, @@ -330,16 +324,6 @@ var BasicModel = AbstractModel.extend({ element.data[fieldName] = this.get(element.data[fieldName]) || []; } } - - // weird way to get out many2ones for widget=selection - if (field.__fetch_selection) { - field.__selection_information = self.many2ones[field.relation]; - } - - // weird way to get out many2manys for widget=many2many_checkboxes - if (field.__fetch_many2manys) { - field.__many2many_information = self.many2manys[field.relation]; - } } // this is not strictly necessary, but it hides some implementation @@ -697,7 +681,11 @@ var BasicModel = AbstractModel.extend({ delete record._changes[name]; } }); - return fieldNames; + 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); + }); }); }); }, @@ -1371,6 +1359,147 @@ var BasicModel = AbstractModel.extend({ return $.when.apply($, _.map(groups, this._fetchMany2OneGroup.bind(this))); }, + /** + * Check the AbstractField specializations that are (will be) used by the + * given record and fetch the special data they will need. Special data are + * data that the rendering of the record won't need if it was not using + * particular widgets (example of these can be found at the methods which + * start with _fetchSpecial). + * + * @param {Object} record - an element from the localData + * @returns {Deferred} + * The deferred is resolved with an array containing the names of + * the field whose special data has been changed. + */ + _fetchSpecialData: function (record) { + var self = this; + var fieldNames = []; + return $.when.apply($, _.map(record.fieldNames, function (name) { + var attrs = record.fieldAttrs[name] || {}; + var Widget = attrs.Widget; + if (Widget && Widget.prototype.specialData) { + return self[Widget.prototype.specialData](record, name).then(function (data) { + if (data === undefined) { + return; + } + record.specialData[name] = data; + fieldNames.push(name); + }); + } + })).then(function () { + return fieldNames; + }); + }, + /** + * Fetches all the m2o records associated to the given fieldName. If the + * given fieldName is not a m2o field, nothing is done. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @param {string[]} [fieldsToRead] - the m2os fields to read (id and + * display_name are automatic). + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialMany2ones: function (record, fieldName, fieldsToRead) { + var field = record.fields[fieldName]; + if (field.type !== "many2one") { + return $.when(); + } + + var context = record.getContext({fieldName: fieldName}); + var domain = record.getDomain({fieldName: fieldName}); + if (domain.length) { + var localID = record.data[fieldName]; + var element = this.localData[localID]; + domain = ["|", ["id", "=", element.data.id]].concat(domain); + } + + // avoid rpc if not necessary + var specialDataCache = {context: context, domain: domain}; + if (_.isEqual(record._specialDataCache[fieldName], specialDataCache)) { + return $.when(); + } + record._specialDataCache[fieldName] = specialDataCache; + + var self = this; + return this._rpc(field.relation, 'search_read') + .withFields(["id"].concat(fieldsToRead || [])) + .withContext(context) + .withDomain(domain) + .exec() + .then(function (result) { + var ids = _.pluck(result.records, 'id'); + return self._rpc(field.relation, 'name_get') + .args([ids]) + .withContext(context) + .exec() + .then(function (name_gets) { + _.each(result.records, function (rec) { + var name_get = _.find(name_gets, function (n) { + return n[0] === rec.id; + }); + rec.display_name = name_get[1]; + }); + return result.records; + }); + }); + }, + /** + * Fetches all the relation records associated to the given fieldName. If + * the given fieldName is not a relational field, nothing is done. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialRelation: function (record, fieldName) { + var field = record.fields[fieldName]; + if (!_.contains(["many2one", "many2many", "one2many"], field.type)) { + return $.when(); + } + + var context = record.getContext({fieldName: fieldName}); + var domain = record.getDomain({fieldName: fieldName}); + + // avoid rpc if not necessary + var specialDataCache = {context: context, domain: domain}; + if (_.isEqual(record._specialDataCache[fieldName], specialDataCache)) { + return $.when(); + } + record._specialDataCache[fieldName] = specialDataCache; + + return this._rpc(field.relation, "name_search") + .args(["", domain]) + .withContext(context) + .exec(); + }, + /** + * Fetches all the m2o records associated to the given fieldName. If the + * given fieldName is not a m2o field, nothing is done. The difference with + * _fetchSpecialMany2ones is that the field given by options.fold_field is + * also fetched. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialStatus: function (record, fieldName) { + var foldField = record.fieldAttrs[fieldName].options.fold_field; + var fieldsToRead = foldField ? [foldField] : []; + return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead); + }, /** * Fetch all data in a ungrouped list * @@ -1767,6 +1896,8 @@ var BasicModel = AbstractModel.extend({ relationField: params.relationField, res_id: res_id, res_ids: res_ids, + specialData: {}, + _specialDataCache: {}, static: params.static || false, type: type, // 'record' | 'list' value: value, @@ -1833,38 +1964,6 @@ var BasicModel = AbstractModel.extend({ var field = record.fields[name]; var attrs = record.fieldAttrs[name] || {}; var options = attrs.options || {}; - if (field.__fetch_status && !field.__status_information) { - var field_values = _.mapObject(record.data, function (val, key) { - var fieldType = record.fields[key].type; - if (fieldType === 'many2one') { - var element = self.localData[val]; - return element && element.data.id || false; - } - return val; - }); - var domain = Domain.prototype.stringToArray(field.domain || [], field_values); - var fetch_status_information = self._rpc(field.relation, 'search_read') - .withFields(['id'].concat(options.fold_field ? [options.fold_field] : [])) - .withDomain(domain) - .exec() - .then(function (result) { - var ids = _.pluck(result.records, 'id'); - return self._rpc(field.relation, 'name_get') - .args([ids]) - .withContext(self._getContext(record, {fieldName: name})) - .exec() - .then(function (name_gets) { - _.each(result.records, function (record) { - var name_get = _.find(name_gets, function (n) { - return n[0] === record.id; - }); - record.display_name = name_get[1]; - }); - field.__status_information = result.records; - }); - }); - defs.push(fetch_status_information); - } if (options.always_reload) { if (record.fields[name].type === 'many2one' && record.data[name]) { var element = self.localData[record.data[name]]; @@ -1877,26 +1976,10 @@ var BasicModel = AbstractModel.extend({ })); } } - if (field.__fetch_selection && !self.many2ones[field.relation]) { - var fetchSelection = self._rpc(field.relation, 'name_search') - .args(['', field.domain]) - .exec() - .then(function (result) { - self.many2ones[field.relation] = result; - }); - defs.push(fetchSelection); - } - if (field.__fetch_many2manys) { - var fetchMany2Manys = self._rpc(field.relation, 'name_search') - .args(['', field.domain]) - .exec() - .then(function (result) { - self.many2manys[field.relation] = result; - }); - defs.push(fetchMany2Manys); - } }); + defs.push(this._fetchSpecialData(record)); + return $.when.apply($, defs).then(function () { return record; }); -- cgit v1.2.3 From 29cd7154e66a30fb656b88e16b4301d17fa7d958 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 8 Mar 2017 16:43:44 +0100 Subject: [FIX] web: restore FieldStatus behavior FieldStatus was broken: - With a selection, the statusbar_visible attribute forces the choices to display, the discarded ones do not appear at all (even folded) - With a m2o, the fold_field option tells the field to check in the relation to know if the choice has to be folded or not, this was not properly supported - Changing a m2o value was broken as the parseMany2one function did not exist - The status were misordered - ... * Also refactor the way the rendering is handled. * Also modify all statusbar tests so that the statusbar widget is placed in the form header tag (as it is the common use case). --- addons/web/static/src/js/views/basic/basic_model.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 cdb8bb61f7c..afac5c1c5ff 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -964,7 +964,7 @@ var BasicModel = AbstractModel.extend({ } var rel_data = _.pick(data, 'id', 'display_name'); var def; - if (!('display_name' in rel_data)) { + if (rel_data.display_name === undefined) { var field = record.fields[fieldName]; def = this._rpc(field.relation, 'name_get') .args([data.id]) @@ -1498,7 +1498,12 @@ var BasicModel = AbstractModel.extend({ _fetchSpecialStatus: function (record, fieldName) { var foldField = record.fieldAttrs[fieldName].options.fold_field; var fieldsToRead = foldField ? [foldField] : []; - return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead); + return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead).then(function (m2os) { + _.each(m2os, function (m2o) { + m2o.fold = foldField ? m2o[foldField] : false; + }); + return m2os; + }); }, /** * Fetch all data in a ungrouped list -- cgit v1.2.3 From fc8cfc57627bf57686b7e93749c8b4f9a05622f5 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Mon, 13 Mar 2017 11:54:30 +0100 Subject: [FIX] web: fix crash in one2many in one2many Before this fix we can not use a one2many field in a list view one2many field, because the one2many and many2many were not converted to datapoint. --- addons/web/static/src/js/views/basic/basic_model.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (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 afac5c1c5ff..2cfa96a3015 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2110,6 +2110,27 @@ var BasicModel = AbstractModel.extend({ // add many2one records self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); list._cache[id] = dataPoint.id; + + _.each(list.fieldNames, function (fieldName) { + var field = dataPoint.fields[fieldName]; + var val = dataPoint.data[fieldName]; + if (field.type === 'one2many' || field.type === 'many2many') { + var r = self._makeDataPoint({ + count: val.length, + fieldAttrs: field.fieldAttrs, + fields: field.relatedFields, + limit: field.limit, + modelName: field.relation, + res_ids: val, + static: true, + type: 'list', + parentID: list.id, + rawContext: field.context, + relationField: field.relation_field, + }); + dataPoint.data[fieldName] = r.id; + } + }); } list.data.push(dataPoint.id); }); -- cgit v1.2.3 From c1256d1453078d9ceb64ea308efc39acb6cd4465 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Mon, 13 Mar 2017 11:48:03 +0100 Subject: [FIX] web: record with field status with domain If a m2o field status has an associated domain, it is used to match the records to display as status choices. This domain is extended so that it always matches the current status value. The code which was in charge of that was buggy when the status had no value (in that case the domain should not be extended). --- addons/web/static/src/js/views/basic/basic_model.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 2cfa96a3015..8e534d4eda1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1414,8 +1414,10 @@ var BasicModel = AbstractModel.extend({ var domain = record.getDomain({fieldName: fieldName}); if (domain.length) { var localID = record.data[fieldName]; - var element = this.localData[localID]; - domain = ["|", ["id", "=", element.data.id]].concat(domain); + if (localID) { + var element = this.localData[localID]; + domain = ["|", ["id", "=", element.data.id]].concat(domain); + } } // avoid rpc if not necessary -- cgit v1.2.3 From 9636e05bf01d708da148591423bc32dd8e100434 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Mon, 13 Mar 2017 13:56:39 +0100 Subject: [FIX] web: use correct domain when fetching status values The m2o choices that a status field works with are determined using the associated domain. If the current value is not matched by this domain, the current value should still be visible so the domain is automatically extended to match this current value. The problem is that the current value in edit mode or in create mode is not given by the `data` attribute of the associated datapoint. Indeed, user changes and default get are stored in the `_changes` key and it is those values that must be used if they exist. --- 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 8e534d4eda1..7797abfc3f6 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1413,7 +1413,7 @@ var BasicModel = AbstractModel.extend({ var context = record.getContext({fieldName: fieldName}); var domain = record.getDomain({fieldName: fieldName}); if (domain.length) { - var localID = record.data[fieldName]; + var localID = record._changes && record._changes[fieldName] || record.data[fieldName]; if (localID) { var element = this.localData[localID]; domain = ["|", ["id", "=", element.data.id]].concat(domain); -- cgit v1.2.3 From eb5d65cda78802a5c4287c3fcce4ed5658491371 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Mon, 13 Mar 2017 16:40:14 +0100 Subject: [REF] web: factorize the special data cache saving code The _fetchSpecialXXX methods all save values in the _specialDataCache variable associated to a given record and fieldName. This code is being regrouped in a private function now. --- .../web/static/src/js/views/basic/basic_model.js | 35 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 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 7797abfc3f6..a664344ddf5 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1421,11 +1421,13 @@ var BasicModel = AbstractModel.extend({ } // avoid rpc if not necessary - var specialDataCache = {context: context, domain: domain}; - if (_.isEqual(record._specialDataCache[fieldName], specialDataCache)) { + var hasChanged = this._saveSpecialDataCache(record, fieldName, { + context: context, + domain: domain, + }); + if (!hasChanged) { return $.when(); } - record._specialDataCache[fieldName] = specialDataCache; var self = this; return this._rpc(field.relation, 'search_read') @@ -1472,11 +1474,13 @@ var BasicModel = AbstractModel.extend({ var domain = record.getDomain({fieldName: fieldName}); // avoid rpc if not necessary - var specialDataCache = {context: context, domain: domain}; - if (_.isEqual(record._specialDataCache[fieldName], specialDataCache)) { + var hasChanged = this._saveSpecialDataCache(record, fieldName, { + context: context, + domain: domain, + }); + if (!hasChanged) { return $.when(); } - record._specialDataCache[fieldName] = specialDataCache; return this._rpc(field.relation, "name_search") .args(["", domain]) @@ -2140,6 +2144,25 @@ var BasicModel = AbstractModel.extend({ return list; }); }, + /** + * Allows to save a value in the specialData cache associated to a given + * record and fieldName. If the value in the cache was already the given + * one, nothing is done and the method indicates it by returning false + * instead of true. + * + * @private + * @param {Object} record - an element from the localData + * @param {string} fieldName - the name of the field + * @param {*} value - the cache value to save + * @returns {boolean} false if the value was already the given one + */ + _saveSpecialDataCache: function (record, fieldName, value) { + if (_.isEqual(record._specialDataCache[fieldName], value)) { + return false; + } + record._specialDataCache[fieldName] = value; + return true; + }, /** * Do a /search_read to get data for a list resource. This does a * /search_read because the data may not be static (for ex, a list view). -- cgit v1.2.3 From 07c581975c7ec4fa5589969695eb7beb49d99733 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Mon, 13 Mar 2017 16:42:48 +0100 Subject: [REF] web: use `specialData` system in domain field The domain field needs two pieces of information that traditional fields do not have without extra RPCs and computation: the value of another field in the view to know the model to use and the number of records matched in this model with the current field value. While the first one can be retrieved synchronously by the field widget, the second one required an extra RPC during the rendering phase. The `_fetchSpecialData` system introduced in the `BasicModel` implementation allows to stop doing this extra RPC during rendering. As the RPC requires the model value to be done, the information is also fetched by the `_fetchSpecialData` code and given directly to the widget. This also removes the utility of the `resetOnAnyFieldChange` option used by this field widget. --- .../web/static/src/js/views/basic/basic_model.js | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) (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 a664344ddf5..333a74cd238 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1511,6 +1511,56 @@ var BasicModel = AbstractModel.extend({ return m2os; }); }, + /** + * Fetches the number of records associated to the domain the value of the + * given field represents. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialDomain: function (record, fieldName) { + var context = record.getContext({fieldName: fieldName}); + + var domainModel = record.fieldAttrs[fieldName].options.model; + if (record.data.hasOwnProperty(domainModel)) { + domainModel = record._changes && record._changes[domainModel] || record.data[domainModel]; + } + var domainValue = record._changes && record._changes[fieldName] || record.data[fieldName]; + + // avoid rpc if not necessary + var hasChanged = this._saveSpecialDataCache(record, fieldName, { + context: context, + domainModel: domainModel, + domainValue: domainValue, + }); + if (!hasChanged) { + return $.when(); + } + + var def = $.Deferred(); + + this._rpc(domainModel, "search_count") + .args([domainValue]) + .withContext(context) + .exec() + .then(_.identity, function (error, e) { + e.preventDefault(); // prevent traceback (the search_count might be intended to break) + return false; + }) + .always(function (nbRecords) { + def.resolve({ + model: domainModel, + nbRecords: nbRecords, + }); + }); + + return def; + }, /** * Fetch all data in a ungrouped list * -- cgit v1.2.3 From 0111882af13aeb80ecc4c118918a6cae9f3d0624 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 14 Mar 2017 15:29:54 +0100 Subject: [FIX] web: BasicModel: recursively fetch x2manys Before this commit, x2many fields embedded in x2many fields were not fetched. This was for example the case for many2many_tags in one2many fields. This commit enables the recursive fetch of x2many fields. There is still a missing piece, as the list of fields to fetch for those embedded x2manys is still unknown. This will come in a future commit. --- .../web/static/src/js/views/basic/basic_model.js | 26 ++++------------------ 1 file changed, 4 insertions(+), 22 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 333a74cd238..8ae01e87bbc 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1631,7 +1631,10 @@ var BasicModel = AbstractModel.extend({ } record.data[fieldName] = list.id; if (!field.__no_fetch) { - defs.push(self._readUngroupedList(list)); + var def = self._readUngroupedList(list).then(function () { + return self._fetchX2ManysBatched(list); + }); + defs.push(def); } } }); @@ -2166,27 +2169,6 @@ var BasicModel = AbstractModel.extend({ // add many2one records self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); list._cache[id] = dataPoint.id; - - _.each(list.fieldNames, function (fieldName) { - var field = dataPoint.fields[fieldName]; - var val = dataPoint.data[fieldName]; - if (field.type === 'one2many' || field.type === 'many2many') { - var r = self._makeDataPoint({ - count: val.length, - fieldAttrs: field.fieldAttrs, - fields: field.relatedFields, - limit: field.limit, - modelName: field.relation, - res_ids: val, - static: true, - type: 'list', - parentID: list.id, - rawContext: field.context, - relationField: field.relation_field, - }); - dataPoint.data[fieldName] = r.id; - } - }); } list.data.push(dataPoint.id); }); -- cgit v1.2.3 From ce36786038b7b9bfa3e292ce40207d1ea46f3c92 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 15 Mar 2017 12:06:29 +0100 Subject: [FIX] web: _buildOnchangeSpecs must return only the present fields Before: the method '_buildOnchangeSpecs' returned all fields, but some models like 'res.users' have redefined the fields_get and add some fake fields which cause a crash. --- addons/web/static/src/js/views/basic/basic_model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 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 8ae01e87bbc..7d33d51257f 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1239,8 +1239,8 @@ var BasicModel = AbstractModel.extend({ var field = record.fields[name]; specs[name] = (field.onChange) || ""; _.each(field.views, function (view) { - _.each(view.fields, function (field, subname) { - specs[name + '.' + subname] = (field.onChange) || ""; + _.each(view.fieldAttrs, function (field, subname) { + specs[name + '.' + subname] = (view.fields[subname].onChange) || ""; }); }); }); -- cgit v1.2.3 From 1e9b695ef16baf5e28915115fd34f72a2d767cf9 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 15 Mar 2017 14:45:13 +0100 Subject: [FIX] web: BasicModel: always reload x2many fields Before this commit, the relational data of a record was only fetched once, and then retrieved from the cache. This was premature optimization, that can lead to bugs as those relational data displayed in the webclient might be desynchronized with the actual data in DB. --- .../web/static/src/js/views/basic/basic_model.js | 49 ++++++++-------------- 1 file changed, 18 insertions(+), 31 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 7d33d51257f..6c59d242cb0 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1300,7 +1300,6 @@ var BasicModel = AbstractModel.extend({ _fetchRecord: function (record, fieldNames) { var self = this; fieldNames = fieldNames || _.uniq(record.fieldNames.concat(['display_name'])); - var oldData = record.data; return this._rpc(record.model, 'read') .args([[record.res_id], fieldNames]) // .withContext({ bin_size: true }) // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field @@ -1313,7 +1312,7 @@ var BasicModel = AbstractModel.extend({ self._parseServerData(fieldNames, record.fields, record.data); }) .then(function () { - return self._fetchX2Manys(record, oldData, fieldNames).then(function () { + return self._fetchX2Manys(record, fieldNames).then(function () { return self._postprocess(record); }); }); @@ -1591,44 +1590,32 @@ var BasicModel = AbstractModel.extend({ * useless rpcs. * * @param {Object} record local resource - * @param {Object} oldData the data fetched previously * @param {string[]} [fieldNames] list of fields to fetch * @returns {Deferred} */ - _fetchX2Manys: function (record, oldData, fieldNames) { + _fetchX2Manys: function (record, fieldNames) { var self = this; var defs = []; fieldNames = fieldNames || record.fieldNames; _.each(fieldNames, function (fieldName) { var field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { - var list; - - // a previously loaded list exists - if (oldData[fieldName]) { - list = self.localData[oldData[fieldName]]; - list.res_ids = record.data[fieldName]; - list.count = list.res_ids.length; - list.offset = list.offset < list.count ? list.offset : 0; - } else { - // need to create a list from scratch - var attrs = record.fieldAttrs[fieldName]; - var rawContext = attrs && attrs.context; - var ids = record.data[fieldName] || []; - list = self._makeDataPoint({ - count: ids.length, - fieldAttrs: field.fieldAttrs, - fields: field.relatedFields, - limit: field.limit, - modelName: field.relation, - res_ids: ids, - static: true, - type: 'list', - parentID: record.id, - rawContext: rawContext, - relationField: field.relation_field, - }); - } + var attrs = record.fieldAttrs[fieldName]; + var rawContext = attrs && attrs.context; + var ids = record.data[fieldName] || []; + var list = self._makeDataPoint({ + count: ids.length, + fieldAttrs: field.fieldAttrs, + fields: field.relatedFields, + limit: field.limit, + modelName: field.relation, + res_ids: ids, + static: true, + type: 'list', + parentID: record.id, + rawContext: rawContext, + relationField: field.relation_field, + }); record.data[fieldName] = list.id; if (!field.__no_fetch) { var def = self._readUngroupedList(list).then(function () { -- cgit v1.2.3 From 378d6813ff03a428537b98bb7232c09ef4d957d5 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 15 Mar 2017 15:40:21 +0100 Subject: [IMP] web: list view: nested groups Before this commit, only one level of groups was supported in list views. This commit enables the nested groups support. --- addons/web/static/src/js/views/basic/basic_model.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 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 6c59d242cb0..5180cc41656 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -933,7 +933,7 @@ var BasicModel = AbstractModel.extend({ group.isOpen = true; var def; if (group.count > 0) { - def = this._fetchUngroupedList(group); + def = this._load(group); } return $.when(def).then(function () { return groupId; @@ -2048,6 +2048,7 @@ var BasicModel = AbstractModel.extend({ var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); return this._rpc(list.model, 'read_group') .withFields(fields) + .withDomain(list.domain) .withContext(list.context) .groupBy(list.groupedBy) .lazy(true) @@ -2071,7 +2072,7 @@ var BasicModel = AbstractModel.extend({ var newGroup = self._makeDataPoint({ modelName: list.model, count: group[rawGroupBy + '_count'], - domain: group.__domain, + domain: list.domain.concat(group.__domain), context: list.context, fields: list.fields, fieldNames: list.fieldNames, @@ -2082,7 +2083,7 @@ var BasicModel = AbstractModel.extend({ orderedBy: list.orderedBy, limit: list.limit, openGroupByDefault: list.openGroupByDefault, - type: 'list', // nested groupedBys not handled yet + type: 'list', }); list.data.push(newGroup.id); var old_group = _.find(previousGroups, function (g) { @@ -2090,13 +2091,15 @@ var BasicModel = AbstractModel.extend({ }); if (old_group) { newGroup.isOpen = old_group.isOpen; + // copy the oldGroup data to keep 'isOpen' state of sub-groups + newGroup.data = newGroup.isOpen ? old_group.data : []; } else if (!newGroup.openGroupByDefault) { newGroup.isOpen = false; } else { newGroup.isOpen = '__fold' in group ? !group.__fold : true; } if (newGroup.isOpen && newGroup.count > 0) { - defs.push(self._fetchUngroupedList(newGroup)); + defs.push(self._load(newGroup)); } }); return $.when.apply($, defs).then(function () { -- cgit v1.2.3 From b1cb25353be5f635320ad70bf34197030a328979 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 16 Mar 2017 14:28:05 +0100 Subject: [FIX] web: BasicModel: list count when grouped Before this rev., the count attribute of list dataPoints was basically the length of the data Array. It means that for an ungrouped list, it was the number of records in the list, and for a list containing groups, it was the number of groups. This caused a problem in the list view with nested groups, as the counter in the group header is supposed to display the number of records in the group, whether or not the group contains other subgroups or directly records. This commit changes a bit the semantics of the count attribute, such that it always represents the number of records inside the list (i.e. the sums of records inside subgroups if the list is grouped). --- addons/web/static/src/js/views/basic/basic_model.js | 3 ++- 1 file changed, 2 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 5180cc41656..010cc37d8d6 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2059,7 +2059,7 @@ var BasicModel = AbstractModel.extend({ return self.localData[groupID]; }); list.data = []; - list.count = groups.length; + list.count = 0; var defs = []; _.each(groups, function (group) { @@ -2086,6 +2086,7 @@ var BasicModel = AbstractModel.extend({ type: 'list', }); list.data.push(newGroup.id); + list.count += newGroup.count; var old_group = _.find(previousGroups, function (g) { return g.res_id === newGroup.res_id && g.value === newGroup.value; }); -- cgit v1.2.3 From 6e1875ac7e9a6fea9c44d00b3fe7a2c23c9bbb6f Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 16 Mar 2017 16:44:32 +0100 Subject: [FIX] web: MockServer: read_group __domain read_group returns a __domain for each group, which is the conjunction of the domain given for the read_group with the domain matching the given group. The MockServer didn't reproduce that behavior. Moreover, the BasicModel did again that conjunction, leading to domains with redundant subdomains. --- 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 010cc37d8d6..19500a0e442 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2072,7 +2072,7 @@ var BasicModel = AbstractModel.extend({ var newGroup = self._makeDataPoint({ modelName: list.model, count: group[rawGroupBy + '_count'], - domain: list.domain.concat(group.__domain), + domain: group.__domain, context: list.context, fields: list.fields, fieldNames: list.fieldNames, -- cgit v1.2.3 From 740a2b1265d9f8958beb76a91fd79d1a120e85cf Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 21 Mar 2017 08:55:20 +0100 Subject: [FIX] web: BasicModel: keep state of groups on reload The 'isOpen' state of groups was already kept when reloading a (e.g.) list view. The limit and offset should be kept as well. --- addons/web/static/src/js/views/basic/basic_model.js | 12 +++++++----- 1 file changed, 7 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 19500a0e442..d5f3c86cc8e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2087,13 +2087,15 @@ var BasicModel = AbstractModel.extend({ }); list.data.push(newGroup.id); list.count += newGroup.count; - var old_group = _.find(previousGroups, function (g) { + var oldGroup = _.find(previousGroups, function (g) { return g.res_id === newGroup.res_id && g.value === newGroup.value; }); - if (old_group) { - newGroup.isOpen = old_group.isOpen; - // copy the oldGroup data to keep 'isOpen' state of sub-groups - newGroup.data = newGroup.isOpen ? old_group.data : []; + if (oldGroup) { + // restore the internal state of the group + _.extend(newGroup, _.pick(oldGroup, 'limit', 'isOpen', 'offset')); + // if open, also restore the oldGroup data to keep internal + // state of sub-groups + newGroup.data = oldGroup.isOpen ? oldGroup.data : []; } else if (!newGroup.openGroupByDefault) { newGroup.isOpen = false; } else { -- cgit v1.2.3 From eaa66d11568b415b8d6b4c7a17a058b5ec1df4a6 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 21 Mar 2017 15:30:58 +0100 Subject: [REF] web: change internal representation of fields and view meta datas * Data manager load_view returns a viewInfo object * ViewInfo contains: the arch, fields and fieldsInfo objects, and some extra informations * The fields come from the server and are frozen, without any view attributes (depend of the model). * The fieldsInfo contains every view attributes and informations. --- .../web/static/src/js/views/basic/basic_model.js | 142 ++++++++++++--------- 1 file changed, 81 insertions(+), 61 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 d5f3c86cc8e..023571d19a4 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -25,7 +25,7 @@ odoo.define('web.BasicModel', function (require) { * domain: {*[]}, * fieldNames: {string[]}, * fields: {Object}, - * fieldAttrs: {Object}, + * fieldsInfo: {Object}, * getContext: {function}, * getDomain: {function}, * groupedBy: {string[]}, @@ -160,7 +160,7 @@ var BasicModel = AbstractModel.extend({ var position = (options && options.position) || 'top'; var params = { context: context, - fieldAttrs: list.fieldAttrs, + fieldsInfo: list.fieldsInfo, fieldNames: list.fieldNames, fields: list.fields, parentID: list.id, @@ -238,7 +238,7 @@ var BasicModel = AbstractModel.extend({ var index = record.res_ids.indexOf(record.res_id); record.res_ids.splice(index + 1, 0, res_id); return self.load({ - fieldAttrs: record.fieldAttrs, + fieldsInfo: record.fieldsInfo, fieldNames: record.fieldNames, fields: record.fields, modelName: record.model, @@ -395,7 +395,7 @@ var BasicModel = AbstractModel.extend({ * @todo document all params * * @param {any} params - * @param {Object} [params.fieldAttrs={}] contains the attrs of each field + * @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field * @param {Array} [params.fieldNames] the name of fields to load, the list * of all fields by default * @param {Object} params.fields contains the description of each field @@ -428,7 +428,7 @@ var BasicModel = AbstractModel.extend({ * @param {any} params * @param {string} modelName model name * @param {Object} params.context the context for the new record - * @param {Object} [params.fieldAttrs={}] contains the attrs of each field + * @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field * @param {Array} params.fieldNames the name of fields to load, the list * of all fields by default * @param {Object} params.fields contains the description of each field @@ -463,7 +463,7 @@ var BasicModel = AbstractModel.extend({ data: data, fields: params.fields, fieldNames: params.fieldNames, - fieldAttrs: params.fieldAttrs, + fieldsInfo: params.fieldsInfo, context: params.context, parentID: params.parentID, relationField: params.relationField, @@ -483,15 +483,17 @@ var BasicModel = AbstractModel.extend({ }); record._changes[name] = rec.id; } else if (field.type === 'one2many' || field.type === 'many2many') { - var attrs = record.fieldAttrs[name]; + var fieldInfo = record.fieldsInfo[name]; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; + var x2manyList = self._makeDataPoint({ context: record.context, - fieldAttrs: field.fieldAttrs, - fields: field.relatedFields, - limit: field.limit, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: fieldInfo.relatedFields || view && view.fields, + limit: fieldInfo.limit, modelName: field.relation, parentID: record.id, - rawContext: attrs && attrs.context, + rawContext: fieldInfo && fieldInfo.context, relationField: field.relation_field, res_ids: [], static: true, @@ -508,8 +510,8 @@ var BasicModel = AbstractModel.extend({ r = self._makeDataPoint({ modelName: x2manyList.model, context: x2manyList.context, - fields: field.relatedFields, - fieldAttrs: field.fieldAttrs, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: fieldInfo.relatedFields || view && view.fields, }); x2manyList._changes.push(r.id); r._changes = value[2]; @@ -540,8 +542,8 @@ var BasicModel = AbstractModel.extend({ r = self._makeDataPoint({ modelName: x2manyList.model, context: x2manyList.context, - fields: field.relatedFields, - fieldAttrs: field.fieldAttrs, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: fieldInfo.relatedFields || view && view.fields, res_id: value, }); if (!x2manyList._changes) { @@ -599,10 +601,10 @@ var BasicModel = AbstractModel.extend({ * * @param {string} model name of the model * @param {Object[]} fields a description of field properties - * @param {Object} [attrs] various field attrs that we want to set + * @param {Object} [fieldInfo] various field fieldInfo that we want to set * @returns {string} the local id for the created resource */ - makeRecord: function (model, fields, attrs) { + makeRecord: function (model, fields, fieldInfo) { var self = this; var record_fields = {}; _.each(fields, function (field) { @@ -611,7 +613,7 @@ var BasicModel = AbstractModel.extend({ var record = this._makeDataPoint({ modelName: model, fields: record_fields, - fieldAttrs: attrs, + fieldsInfo: fieldInfo, }); _.each(fields, function (field) { if ('value' in field) { @@ -704,7 +706,7 @@ var BasicModel = AbstractModel.extend({ if ('currentId' in options && !options.currentId) { var params = { context: element.context, - fieldAttrs: element.fieldAttrs, + fieldsInfo: element.fieldsInfo, fieldNames: element.fieldNames, fields: element.fields, }; @@ -836,7 +838,7 @@ var BasicModel = AbstractModel.extend({ /** * Set fieldNames. * Set fields on a record element (current fields as defaults) - * Set fieldattrs. + * Set fieldsInfo. * It is mostly useful for the cases where a record element is shared * between various views, such as a one2many with a tree and a form view. * @@ -844,22 +846,23 @@ var BasicModel = AbstractModel.extend({ * @param {Object} props * @param {Object} props.fieldNames * @param {Object} props.fields - * @param {Object} props.fieldAttrs + * @param {Object} props.fieldsInfo * @return {Object} * @return {Object} result.fieldNames the previous field names * @return {Object} result.fields the previous description fields - * @return {Object} result.fieldattrs the previous field attrs + * @return {Object} result.fieldsInfo the previous field fieldInfo */ setFieldProps: function (recordID, props) { var record = this.localData[recordID]; var result = { fieldNames: record.fieldNames, fields: record.fields, - fieldAttrs: record.fieldAttrs, + fieldsInfo: record.fieldsInfo, }; - record.fieldNames = props.fieldNames; + record.mode = props.mode; + record.fieldNames = Object.keys(props.fieldsInfo); record.fields = _.defaults(props.fields, record.fields); - record.fieldAttrs = props.fieldAttrs; + record.fieldsInfo = props.fieldsInfo; return result; }, /** @@ -979,6 +982,7 @@ var BasicModel = AbstractModel.extend({ context: record.context, data: rel_data, fields: {}, + fieldsInfo: {}, modelName: record.fields[fieldName].relation, }); record._changes[fieldName] = rec.id; @@ -1063,7 +1067,7 @@ var BasicModel = AbstractModel.extend({ var params = { context: context, fields: list.fields, - fieldAttrs: list.fieldAttrs, + fieldsInfo: list.fieldsInfo, modelName: list.model, fieldNames: _.keys(command[2]), }; @@ -1128,6 +1132,8 @@ var BasicModel = AbstractModel.extend({ var self = this; var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; var field = record.fields[fieldName]; + var fieldInfo = record.fieldsInfo[fieldName]; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var rec; var defs = []; list._changes = list._changes || list.data; @@ -1158,8 +1164,8 @@ var BasicModel = AbstractModel.extend({ rec = self._makeDataPoint({ context: record.context, modelName: field.relation, - fields: field.relatedFields, - fieldAttrs: field.fieldAttrs, + fields: fieldInfo.relatedFields || view && view.fields, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, res_id: d.id, }); list_records[d.id] = rec; @@ -1237,9 +1243,10 @@ var BasicModel = AbstractModel.extend({ var specs = {}; _.each(record.fieldNames, function (name) { var field = record.fields[name]; + var fieldInfo = record.fieldsInfo[name]; specs[name] = (field.onChange) || ""; - _.each(field.views, function (view) { - _.each(view.fieldAttrs, function (field, subname) { + _.each(fieldInfo.views, function (view) { + _.each(view.fieldsInfo, function (field, subname) { specs[name + '.' + subname] = (view.fields[subname].onChange) || ""; }); }); @@ -1374,8 +1381,8 @@ var BasicModel = AbstractModel.extend({ var self = this; var fieldNames = []; return $.when.apply($, _.map(record.fieldNames, function (name) { - var attrs = record.fieldAttrs[name] || {}; - var Widget = attrs.Widget; + var fieldInfo = record.fieldsInfo[name] || {}; + var Widget = fieldInfo.Widget; if (Widget && Widget.prototype.specialData) { return self[Widget.prototype.specialData](record, name).then(function (data) { if (data === undefined) { @@ -1501,7 +1508,7 @@ var BasicModel = AbstractModel.extend({ * the undefined value. */ _fetchSpecialStatus: function (record, fieldName) { - var foldField = record.fieldAttrs[fieldName].options.fold_field; + var foldField = record.fieldsInfo[fieldName].options.fold_field; var fieldsToRead = foldField ? [foldField] : []; return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead).then(function (m2os) { _.each(m2os, function (m2o) { @@ -1525,7 +1532,7 @@ var BasicModel = AbstractModel.extend({ _fetchSpecialDomain: function (record, fieldName) { var context = record.getContext({fieldName: fieldName}); - var domainModel = record.fieldAttrs[fieldName].options.model; + var domainModel = record.fieldsInfo[fieldName].options.model; if (record.data.hasOwnProperty(domainModel)) { domainModel = record._changes && record._changes[domainModel] || record.data[domainModel]; } @@ -1600,14 +1607,15 @@ var BasicModel = AbstractModel.extend({ _.each(fieldNames, function (fieldName) { var field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { - var attrs = record.fieldAttrs[fieldName]; - var rawContext = attrs && attrs.context; + var fieldInfo = record.fieldsInfo[fieldName]; + var rawContext = fieldInfo && fieldInfo.context; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var ids = record.data[fieldName] || []; var list = self._makeDataPoint({ count: ids.length, - fieldAttrs: field.fieldAttrs, - fields: field.relatedFields, - limit: field.limit, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: fieldInfo.relatedFields || view && view.fields, + limit: fieldInfo.limit, modelName: field.relation, res_ids: ids, static: true, @@ -1617,7 +1625,7 @@ var BasicModel = AbstractModel.extend({ relationField: field.relation_field, }); record.data[fieldName] = list.id; - if (!field.__no_fetch) { + if (!fieldInfo.__no_fetch) { var def = self._readUngroupedList(list).then(function () { return self._fetchX2ManysBatched(list); }); @@ -1638,6 +1646,8 @@ var BasicModel = AbstractModel.extend({ _fetchX2ManyBatched: function (list, fieldName) { var self = this; var field = list.fields[fieldName]; + var fieldInfo = list.fieldsInfo[fieldName]; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; // step 1: collect ids var ids = []; @@ -1645,8 +1655,8 @@ var BasicModel = AbstractModel.extend({ var record = self.localData[dataPoint]; ids = _.unique(ids.concat(record.data[fieldName] || [])); var m2mList = self._makeDataPoint({ - fieldAttrs: field.fieldAttrs, - fields: field.relatedFields, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: fieldInfo.relatedFields || view && view.fields, modelName: field.relation, res_ids: record.data[fieldName], static: true, @@ -1655,13 +1665,13 @@ var BasicModel = AbstractModel.extend({ record.data[fieldName] = m2mList.id; }); - if (!ids.length || field.__no_fetch) { + if (!ids.length || fieldInfo.__no_fetch) { return $.when(); } // step 2: fetch data from server return this._rpc(field.relation, 'read') - .args([ids, _.keys(field.relatedFields)]) + .args([ids, _.keys(fieldInfo.relatedFields)]) .withContext({}) // FIXME .exec() .then(function (results) { @@ -1670,8 +1680,8 @@ var BasicModel = AbstractModel.extend({ return self._makeDataPoint({ modelName: field.relation, data: result, - fields: field.relatedFields, - fieldAttrs: field.fieldAttrs, + fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: fieldInfo.relatedFields || view && view.fields, }); }); @@ -1796,9 +1806,9 @@ var BasicModel = AbstractModel.extend({ context.set_eval_context(this._getEvalContext(element)); if (options && options.fieldName) { - var attrs = element.fieldAttrs[options.fieldName]; - if (attrs && attrs.context) { - context.add(attrs.context); + var fieldInfo = element.fieldsInfo[options.fieldName]; + if (fieldInfo && fieldInfo.context) { + context.add(fieldInfo.context); } else { var fieldParams = element.fields[options.fieldName]; if (fieldParams.context) { @@ -1828,10 +1838,10 @@ var BasicModel = AbstractModel.extend({ */ _getDomain: function (element, options) { if (options && options.fieldName) { - var attrs = element.fieldAttrs[options.fieldName]; - if (attrs && attrs.domain) { + var fieldInfo = element.fieldsInfo[options.fieldName]; + if (fieldInfo && fieldInfo.domain) { return Domain.prototype.stringToArray( - attrs.domain, + fieldInfo.domain, this._getEvalContext(element) ); } @@ -1899,7 +1909,7 @@ var BasicModel = AbstractModel.extend({ * the resource in the localData object. * * @param {Object} params - * @param {Object} [params.fieldAttrs={}] contains the attrs of each field + * @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field * @param {Array} [params.fieldNames] the name of fields to load, the list * of all fields by default * @param {Object} params.fields contains the description of each field @@ -1907,7 +1917,6 @@ var BasicModel = AbstractModel.extend({ */ _makeDataPoint: function (params) { var type = params.type || ('domain' in params && 'list') || 'record'; - var res_id, value; var res_ids = params.res_ids || []; if (type === 'record') { @@ -1923,6 +1932,13 @@ var BasicModel = AbstractModel.extend({ id: {type: 'integer'}, }, params.fields); + if (!params.fieldsInfo) { + params.fieldsInfo = {}; + for (var fieldName in fields) { + params.fieldsInfo[fieldName] = {}; + } + } + var dataPoint = { _cache: type === 'list' ? {} : undefined, _changes: null, @@ -1931,9 +1947,9 @@ var BasicModel = AbstractModel.extend({ count: params.count || res_ids.length, data: params.data || (type === 'record' ? {} : []), domain: params.domain || [], - fieldNames: params.fieldNames || Object.keys(fields), + fieldNames: params.fieldNames || Object.keys(params.fieldsInfo), fields: fields, - fieldAttrs: params.fieldAttrs || {}, + fieldsInfo: params.fieldsInfo || {}, groupedBy: params.groupedBy || [], id: _.uniqueId(params.modelName + '_'), isOpen: params.isOpen, @@ -1979,6 +1995,10 @@ var BasicModel = AbstractModel.extend({ // the many2one value is of the form [id, display_name] var r = self._makeDataPoint({ modelName: field.relation, + fields: { + display_name: {type: 'char'}, + id: {type: 'integer'}, + }, data: { display_name: val[1], id: val[0], @@ -2013,8 +2033,8 @@ var BasicModel = AbstractModel.extend({ var defs = []; _.each(record.fieldNames, function (name) { var field = record.fields[name]; - var attrs = record.fieldAttrs[name] || {}; - var options = attrs.options || {}; + var fieldInfo = record.fieldsInfo[name] || {}; + var options = fieldInfo.options || {}; if (options.always_reload) { if (record.fields[name].type === 'many2one' && record.data[name]) { var element = self.localData[record.data[name]]; @@ -2076,7 +2096,7 @@ var BasicModel = AbstractModel.extend({ context: list.context, fields: list.fields, fieldNames: list.fieldNames, - fieldAttrs: list.fieldAttrs, + fieldsInfo: list.fieldsInfo, value: group[rawGroupBy], aggregateValues: aggregateValues, groupedBy: list.groupedBy.slice(1), @@ -2152,7 +2172,7 @@ var BasicModel = AbstractModel.extend({ } else { dataPoint = self._makeDataPoint({ data: _.findWhere(records, {id: id}), - fieldAttrs: list.fieldAttrs, + fieldsInfo: list.fieldsInfo, fieldNames: list.fieldNames, fields: list.fields, modelName: list.model, @@ -2211,7 +2231,7 @@ var BasicModel = AbstractModel.extend({ data: record, fields: list.fields, fieldNames: list.fieldNames, - fieldAttrs: list.fieldAttrs, + fieldsInfo: list.fieldsInfo, modelName: list.model, parentID: list.id, }); -- cgit v1.2.3 From 3f8569a05702a8d14f18e8303e2f9418ec65a287 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 22 Mar 2017 14:55:17 +0100 Subject: [FIX] web: send array domains to the server in domain field The `FieldDomain` was sending string domains instead of array domains to the server (by its associated `BasicModel._fetchSpecialDomain` function). The server does not do the conversion string -> array for domains, so the RPC broke everytime. This commit also modifies the `MockServer` so that tests also break when sending string domains. It also improves the way failed rpc are handled. Indeed when an rpc fails, its deferred is rejected with two arguments: the error object, and a JQuery event (that will display the error if not preventDefaulted). The mockRPC didn't pass the second one and thus there was a crash when reaching code that e.g. calls preventDefault on that event. Note: this also removes the stringify/parse of the error object as it is not a correct simulation of a server error object anyway. --- 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 023571d19a4..0c2bbd4d23e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1551,7 +1551,7 @@ var BasicModel = AbstractModel.extend({ var def = $.Deferred(); this._rpc(domainModel, "search_count") - .args([domainValue]) + .args([Domain.prototype.stringToArray(domainValue)]) .withContext(context) .exec() .then(_.identity, function (error, e) { -- cgit v1.2.3 From b3015960c1e4dd282d948fb5f56490639d81c343 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 22 Mar 2017 13:44:09 +0100 Subject: [FIX] web: BasicModel: read_group: empty groups When reloading a grouped Kanban view (e.g. after specifying a new domain), the internal state of the (sub-)groups is kept so that it can be restored (like which are the open groups and the limit and offset inside the groups). However, for models and fields whose read_group returns empty groups (e.g. project.task grouped by stade_id) this was wrong if the specified domain filtered out all records of an open group, because as its data wasn't reloaded (because there is no record), the old data were still in place, so the filtered out records still appeared in the Kanban. --- addons/web/static/src/js/views/basic/basic_model.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 0c2bbd4d23e..deb14b8bf83 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2113,9 +2113,11 @@ var BasicModel = AbstractModel.extend({ if (oldGroup) { // restore the internal state of the group _.extend(newGroup, _.pick(oldGroup, 'limit', 'isOpen', 'offset')); - // if open, also restore the oldGroup data to keep internal - // state of sub-groups - newGroup.data = oldGroup.isOpen ? oldGroup.data : []; + // if the group is open and contains subgroups, also + // restore its data to keep internal state of sub-groups + if (newGroup.isOpen && newGroup.groupedBy.length) { + newGroup.data = oldGroup.data; + } } else if (!newGroup.openGroupByDefault) { newGroup.isOpen = false; } else { -- cgit v1.2.3 From 2106e3dd2ecd61096d7c7531c2f18b00fb30aeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 20 Mar 2017 15:23:19 +0100 Subject: [REF] web, *: simplify our RPC system (in JS) The RPC system was not completely satisfactory, we decided to prepare the future and do it properly. This commit introduces the new rpc system, which replace the previous new one. We now simply have a method, this._rpc, which takes a dictionary of parameters. The idea is that depending on the parameters, it is able to add correct default value when necessary. For situations where we don't have the this._rpc method, we can use the rpc.query method, which takes the same arguments, but directly calls ajax.rpc instead of triggering up some events. --- .../web/static/src/js/views/basic/basic_model.js | 253 ++++++++++++--------- 1 file changed, 146 insertions(+), 107 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 deb14b8bf83..e3455658d50 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -194,10 +194,12 @@ var BasicModel = AbstractModel.extend({ deleteRecords: function (recordIds, modelName, parentID) { var self = this; var records = _.map(recordIds, function (id) { return self.localData[id]; }); - return this._rpc(modelName, 'unlink') - .args([_.pluck(records, 'res_id')]) - .withContext(session.user_context) // todo: combine with view context - .exec() + return this._rpc({ + model: modelName, + method: 'unlink', + args: [_.pluck(records, 'res_id')], + context: session.user_context, // todo: combine with view context + }) .then(function () { _.each(records, function (record) { record.res_ids.splice(record.offset, 1)[0]; @@ -230,10 +232,12 @@ var BasicModel = AbstractModel.extend({ duplicateRecord: function (recordID) { var self = this; var record = this.localData[recordID]; - return this._rpc(record.model, 'copy') - .args([record.data.id]) - .withContext(this._getContext(record)) - .exec() + return this._rpc({ + model: record.model, + method: 'copy', + args: [record.data.id], + context: this._getContext(record), + }) .then(function (res_id) { var index = record.res_ids.indexOf(record.res_id); record.res_ids.splice(index + 1, 0, res_id); @@ -439,10 +443,12 @@ var BasicModel = AbstractModel.extend({ var self = this; var fields_key = _.without(params.fieldNames, '__last_update'); - return this._rpc(modelName, 'default_get') - .args([fields_key]) - .withContext(params.context) - .exec() + return this._rpc({ + model: modelName, + method: 'default_get', + args: [fields_key], + context: params.context, + }) .then(function (result) { // fill default values for missing fields for (var i = 0; i < params.fieldNames.length; i++) { @@ -813,11 +819,12 @@ var BasicModel = AbstractModel.extend({ // in the case of a write, only perform the RPC if there are changes to save if (method === 'create' || Object.keys(changes).length) { var args = method === 'write' ? [[record.data.id], changes] : [changes]; - return self._rpc(record.model, method) - .args(args) - .withContext(session.user_context) // todo: combine with view context - .exec() - .then(function (id) { + return self._rpc({ + model: record.model, + method: method, + args: args, + context: session.user_context // todo: combine with view context + }).then(function (id) { if (method === 'create') { record.res_id = id; // create returns an id, write returns a boolean record.data.id = id; @@ -911,9 +918,11 @@ var BasicModel = AbstractModel.extend({ var resIDs = _.map(recordIDs, function (recordID) { return self.localData[recordID].res_id; }); - return this._rpc(parent.model, 'write') - .args([resIDs, { active: value }]) - .exec() + return this._rpc({ + model: parent.model, + method: 'write', + args: [resIDs, { active: value }], + }) .then(this.reload.bind(this, parentID)); }, @@ -969,10 +978,12 @@ var BasicModel = AbstractModel.extend({ var def; if (rel_data.display_name === undefined) { var field = record.fields[fieldName]; - def = this._rpc(field.relation, 'name_get') - .args([data.id]) - .withContext(record.context) - .exec() + def = this._rpc({ + model: field.relation, + method: 'name_get', + args: [data.id], + context: record.context, + }) .then(function (result) { rel_data.display_name = result[0][1]; }); @@ -1022,9 +1033,11 @@ var BasicModel = AbstractModel.extend({ fields = fields[0]; } return this.mutex.exec(function () { - return self._rpc(record.model, 'onchange') - .args([idList, currentData, fields, onchange_spec, context]) - .exec() + 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', { @@ -1176,9 +1189,11 @@ var BasicModel = AbstractModel.extend({ // _changes so this is a very specific case) // this could be optimized by registering the fetched records in the list's _cache // so that if a record is removed and then re-added, it won't be fetched twice - var def = this._rpc(list.model, 'read') - .args([_.pluck(data, 'id'), list.fieldNames]) - .exec() + var def = this._rpc({ + model: list.model, + method: 'read', + args: [_.pluck(data, 'id'), list.fieldNames] + }) .then(function (records) { _.each(records, function (record) { list_records[record.id].data = record; @@ -1262,10 +1277,12 @@ var BasicModel = AbstractModel.extend({ _fetchMany2OneGroup: function (group) { var ids = _.uniq(_.pluck(group, 'res_id')); - return this._rpc(group[0].model, 'name_get') - .args([ids]) - .withContext(group[0].context) - .exec() + return this._rpc({ + model: group[0].model, + method: 'name_get', + args: [ids], + context: group[0].context + }) .then(function (name_gets) { _.each(group, function (record) { var nameGet = _.find(name_gets, function (n) { return n[0] === record.res_id;}); @@ -1286,10 +1303,12 @@ var BasicModel = AbstractModel.extend({ model = many2oneRecord.model; return many2oneRecord.res_id; }); - return this._rpc(model, 'name_get') - .args([ids]) - .withContext(list.context) - .exec() + return this._rpc({ + model: model, + method: 'name_get', + args: [ids], + context: list.context, + }) .then(function (name_gets) { for (var i = 0; i < name_gets.length; i++) { records[i].data.display_name = name_gets[i][1]; @@ -1307,10 +1326,12 @@ var BasicModel = AbstractModel.extend({ _fetchRecord: function (record, fieldNames) { var self = this; fieldNames = fieldNames || _.uniq(record.fieldNames.concat(['display_name'])); - return this._rpc(record.model, 'read') - .args([[record.res_id], fieldNames]) - // .withContext({ bin_size: true }) // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field - .exec() + return this._rpc({ + model: record.model, + method: 'read', + args: [[record.res_id], fieldNames], + // context: {bin_size: true} // FIXME: when editing a subrecord in the partner form view, it tries to write the bin_size on the image field + }) .then(function (result) { result = result[0]; record.data = _.extend({}, record.data, result); @@ -1436,25 +1457,29 @@ var BasicModel = AbstractModel.extend({ } var self = this; - return this._rpc(field.relation, 'search_read') - .withFields(["id"].concat(fieldsToRead || [])) - .withContext(context) - .withDomain(domain) - .exec() - .then(function (result) { - var ids = _.pluck(result.records, 'id'); - return self._rpc(field.relation, 'name_get') - .args([ids]) - .withContext(context) - .exec() + return this._rpc({ + model: field.relation, + method: 'search_read', + fields: ["id"].concat(fieldsToRead || []), + context: context, + domain: domain, + }) + .then(function (records) { + var ids = _.pluck(records, 'id'); + return self._rpc({ + model: field.relation, + method: 'name_get', + args: [ids], + context: context, + }) .then(function (name_gets) { - _.each(result.records, function (rec) { + _.each(records, function (rec) { var name_get = _.find(name_gets, function (n) { return n[0] === rec.id; }); rec.display_name = name_get[1]; }); - return result.records; + return records; }); }); }, @@ -1488,10 +1513,12 @@ var BasicModel = AbstractModel.extend({ return $.when(); } - return this._rpc(field.relation, "name_search") - .args(["", domain]) - .withContext(context) - .exec(); + return this._rpc({ + model: field.relation, + method: 'name_search', + args: ["", domain], + context: context + }); }, /** * Fetches all the m2o records associated to the given fieldName. If the @@ -1550,10 +1577,12 @@ var BasicModel = AbstractModel.extend({ var def = $.Deferred(); - this._rpc(domainModel, "search_count") - .args([Domain.prototype.stringToArray(domainValue)]) - .withContext(context) - .exec() + this._rpc({ + model: domainModel, + method: 'search_count', + args: [Domain.prototype.stringToArray(domainValue)], + context: context + }) .then(_.identity, function (error, e) { e.preventDefault(); // prevent traceback (the search_count might be intended to break) return false; @@ -1670,10 +1699,12 @@ var BasicModel = AbstractModel.extend({ } // step 2: fetch data from server - return this._rpc(field.relation, 'read') - .args([ids, _.keys(fieldInfo.relatedFields)]) - .withContext({}) // FIXME - .exec() + return this._rpc({ + model: field.relation, + method: 'read', + args: [ids, _.keys(fieldInfo.relatedFields)], + context: {}, // FIXME + }) .then(function (results) { // step 3: assign values to correct datapoints var dataPoints = _.map(results, function (result) { @@ -2038,10 +2069,12 @@ var BasicModel = AbstractModel.extend({ if (options.always_reload) { if (record.fields[name].type === 'many2one' && record.data[name]) { var element = self.localData[record.data[name]]; - defs.push(self._rpc(field.relation, 'name_get') - .args([element.data.id]) - .withContext(self._getContext(record, {fieldName: name})) - .exec() + defs.push(self._rpc({ + model: field.relation, + method: 'name_get', + args: [element.data.id], + context: self._getContext(record, {fieldName: name}), + }) .then(function (result) { element.data.display_name = result[0][1]; })); @@ -2066,13 +2099,15 @@ var BasicModel = AbstractModel.extend({ _readGroup: function (list) { var self = this; var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); - return this._rpc(list.model, 'read_group') - .withFields(fields) - .withDomain(list.domain) - .withContext(list.context) - .groupBy(list.groupedBy) - .lazy(true) - .exec() + return this._rpc({ + model: list.model, + method: 'read_group', + fields: fields, + domain: list.domain, + context: list.context, + groupBy: list.groupedBy, + lazy: true, + }) .then(function (groups) { var rawGroupBy = list.groupedBy[0].split(':')[0]; var previousGroups = _.map(list.data, function (groupID) { @@ -2158,10 +2193,12 @@ var BasicModel = AbstractModel.extend({ } } if (missingIds.length) { - def = this._rpc(list.model, 'read') - .args([missingIds, list.fieldNames]) - .withContext({}) // FIXME - .exec(); + def = this._rpc({ + model: list.model, + method: 'read', + args: [missingIds, list.fieldNames], + context: {} // FIXME + }); } else { def = $.when(); } @@ -2219,33 +2256,35 @@ var BasicModel = AbstractModel.extend({ */ _searchReadUngroupedList: function (list) { var self = this; - return this._rpc(list.model, 'search_read') - .withFields(list.fieldNames) - .withDomain(list.domain || []) - .withLimit(list.limit) - .withOffset(list.offset) - .orderBy(list.orderedBy) - .exec() - .then(function (result) { - list.count = result.length; - var data = _.map(result.records, function (record) { - var dataPoint = self._makeDataPoint({ - data: record, - fields: list.fields, - fieldNames: list.fieldNames, - fieldsInfo: list.fieldsInfo, - modelName: list.model, - parentID: list.id, - }); - - // add many2one records - self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); - return dataPoint.id; + return this._rpc({ + route: '/web/dataset/search_read', + model: list.model, + fields: list.fieldNames, + domain: list.domain || [], + limit: list.limit, + offset: list.offset, + orderBy: list.orderedBy, + }) + .then(function (result) { + list.count = result.length; + var data = _.map(result.records, function (record) { + var dataPoint = self._makeDataPoint({ + data: record, + fields: list.fields, + fieldNames: list.fieldNames, + fieldsInfo: list.fieldsInfo, + modelName: list.model, + parentID: list.id, }); - list.data = data; - list.res_ids = _.pluck(result.records, 'id'); - return list; + + // add many2one records + self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); + return dataPoint.id; }); + list.data = data; + list.res_ids = _.pluck(result.records, 'id'); + return list; + }); }, /** * Change the offset of a record. Note that this does not reload the data. -- cgit v1.2.3 From e3f84e592ea5b734201d0532253fe0790eab49a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 28 Mar 2017 17:29:35 +0200 Subject: [REF] web: use a mutex to serialize all operations in basic model --- .../web/static/src/js/views/basic/basic_model.js | 303 +++++++++++---------- 1 file changed, 156 insertions(+), 147 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 e3455658d50..b2c061f49e7 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -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 - - 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({ + 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, - 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); + fields: list.fields, + fieldsInfo: list.fieldsInfo, + modelName: list.model, + fieldNames: _.keys(command[2]), + }; + if (command[0] === 1) { + params.res_id = command[1]; } - if (command[0] === 5) { - // DELETE ALL - list._changes = []; + 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; } - }); - } 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; - }); + 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); -- cgit v1.2.3 From cbc63ca8696d8a89c97a88f73743fb0bd5a7c1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 30 Mar 2017 16:28:19 +0200 Subject: [FIX] web: properly handle onchanges after discard When an onchange is resolved after a discard, it caused a traceback. We now make sure that it does not happen. Note: I think that the test is quite neat. --- addons/web/static/src/js/views/basic/basic_model.js | 7 +++++++ 1 file changed, 7 insertions(+) (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 b2c061f49e7..b4d343d7eb0 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1049,6 +1049,13 @@ var BasicModel = AbstractModel.extend({ args: [idList, currentData, fields, onchange_spec, context], }) .then(function (result) { + if (!record._changes) { + // if the _changes key does not exist anymore, it means that + // it was removed by discarding the changes after the rpc + // to onchange. So, in that case, the proper response is to + // ignore the onchange. + return; + } if (result.warning) { self.trigger_up('warning', { message: result.warning.message, -- cgit v1.2.3 From 3d959382fd92aca6c48ea2413c646edcc1f178f4 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 31 Mar 2017 14:10:46 +0200 Subject: [FIX] web,mail: one2many records: multiple views A record of the basic model may need to be displayed in different views (e.g. a o2m record is displayed in the, e.g., Kanban view inside the main form view, and if clicked, it is also displayed in the form view opened in a dialog). However, the fields present in those views may differ. The previous implementation of fieldsInfo in the record didn't handle that, as it was a dict whose keys were field names. With this commit, the fieldsInfo is dict whose keys are view types, and the values are dicts whose keys are field names (the fields of the corresponding views, with they attrs in the view). This allows to create multiple o2m records in a row in a form view. --- .../web/static/src/js/views/basic/basic_model.js | 589 +++++++++++---------- 1 file changed, 312 insertions(+), 277 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 b4d343d7eb0..00e2fc43f6d 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -23,7 +23,6 @@ odoo.define('web.BasicModel', function (require) { * count: {integer}, * data: {Object|Object[]}, * domain: {*[]}, - * fieldNames: {string[]}, * fields: {Object}, * fieldsInfo: {Object}, * getContext: {function}, @@ -145,7 +144,7 @@ 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. + * record with the _makeDefaultRecord method, then add it to the list object. * * @param {string} listID a valid handle for a list object * @param {Object} [options] @@ -160,13 +159,13 @@ var BasicModel = AbstractModel.extend({ var position = (options && options.position) || 'top'; var params = { context: context, - fieldsInfo: list.fieldsInfo, - fieldNames: list.fieldNames, fields: list.fields, + fieldsInfo: list.fieldsInfo, parentID: list.id, relationField: list.relationField, + viewType: list.viewType, }; - return this.makeDefaultRecord(list.model, params).then(function (id) { + return this._makeDefaultRecord(list.model, params).then(function (id) { list.count++; if (position === 'top') { list.data.unshift(id); @@ -243,11 +242,11 @@ var BasicModel = AbstractModel.extend({ record.res_ids.splice(index + 1, 0, res_id); return self.load({ fieldsInfo: record.fieldsInfo, - fieldNames: record.fieldNames, fields: record.fields, modelName: record.model, res_id: res_id, res_ids: record.res_ids.slice(0), + viewType: record.viewType, }); }); }, @@ -400,8 +399,6 @@ var BasicModel = AbstractModel.extend({ * * @param {any} params * @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field - * @param {Array} [params.fieldNames] the name of fields to load, the list - * of all fields by default * @param {Object} params.fields contains the description of each field * @param {string} [params.type] 'record' or 'list' * @param {string} [params.recordID] an ID for an existing resource. @@ -409,197 +406,26 @@ var BasicModel = AbstractModel.extend({ */ load: function (params) { params.type = params.type || (params.res_id !== undefined ? 'record' : 'list'); + // FIXME: the following seems only to be used by the basic_model_tests + // so it should probably be removed and the tests should be adapted + params.viewType = params.viewType || 'default'; + if (!params.fieldsInfo) { + var fieldsInfo = {}; + for (var fieldName in params.fieldNames) { + fieldsInfo[params.fieldNames[fieldName]] = {}; + } + params.fieldsInfo = {}; + params.fieldsInfo[params.viewType] = fieldsInfo; + } if (params.type === 'record' && params.res_id === undefined) { - return this.makeDefaultRecord(params.modelName, params); + return this._makeDefaultRecord(params.modelName, params); } var dataPoint = this._makeDataPoint(params); return this._load(dataPoint).then(function () { return dataPoint.id; }); }, - /** - * When one needs to create a record from scratch, a not so simple process - * needs to be done: - * - call the /default_get route to get default values - * - fetch all relational data - * - apply all onchanges if necessary - * - fetch all relational data - * - * This method tries to optimize the process as much as possible. Also, - * it is quite horrible and should be refactored at some point. - * - * @param {any} params - * @param {string} modelName model name - * @param {Object} params.context the context for the new record - * @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field - * @param {Array} params.fieldNames the name of fields to load, the list - * of all fields by default - * @param {Object} params.fields contains the description of each field - * @param {Object} params.context the context for the new record - * @returns {Deferred -> string} resolves to the id for the created resource - */ - makeDefaultRecord: function (modelName, params) { - var self = this; - var fields_key = _.without(params.fieldNames, '__last_update'); - - return this._rpc({ - model: modelName, - method: 'default_get', - args: [fields_key], - context: params.context, - }) - .then(function (result) { - // fill default values for missing fields - for (var i = 0; i < params.fieldNames.length; i++) { - var fieldName = params.fieldNames[i]; - if (!(fieldName in result)) { - var field = params.fields[fieldName]; - if (field.type === 'one2many' || field.type === 'many2many') { - result[fieldName] = []; - } else { - result[fieldName] = null; - } - } - } - - var data = {}; - var record = self._makeDataPoint({ - modelName: modelName, - data: data, - fields: params.fields, - fieldNames: params.fieldNames, - fieldsInfo: params.fieldsInfo, - context: params.context, - parentID: params.parentID, - relationField: params.relationField, - res_ids: params.res_ids, - }); - - var defs = []; // FIXME: remove defs? - _.each(params.fieldNames, function (name) { - var field = params.fields[name]; - data[name] = null; - record._changes = record._changes || {}; - if (field.type === 'many2one' && result[name]) { - var rec = self._makeDataPoint({ - context: record.context, - data: {id: result[name]}, - modelName: field.relation, - }); - record._changes[name] = rec.id; - } else if (field.type === 'one2many' || field.type === 'many2many') { - var fieldInfo = record.fieldsInfo[name]; - var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; - - var x2manyList = self._makeDataPoint({ - context: record.context, - fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, - fields: fieldInfo.relatedFields || view && view.fields, - limit: fieldInfo.limit, - modelName: field.relation, - parentID: record.id, - rawContext: fieldInfo && fieldInfo.context, - relationField: field.relation_field, - res_ids: [], - static: true, - type: 'list', - }); - record._changes[name] = x2manyList.id; - var many2ones = {}; - var r; - _.each(result[name], function (value) { - if (_.isArray(value)) { - // value is a command - if (value[0] === 0) { - // CREATE - r = self._makeDataPoint({ - modelName: x2manyList.model, - context: x2manyList.context, - fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, - fields: fieldInfo.relatedFields || view && view.fields, - }); - x2manyList._changes.push(r.id); - r._changes = value[2]; - - // this is necessary so the fields are initialized - for (var fieldName in value[2]) { - r.data[fieldName] = null; - } - - for (var name in r._changes) { - if (r.fields[name].type === 'many2one') { - var rec = self._makeDataPoint({ - context: r.context, - modelName: r.fields[name].relation, - data: {id: r._changes[name]} - }); - r._changes[name] = rec.id; - many2ones[name] = true; - } - } - } - if (value[0] === 6) { - // REPLACE_WITH - x2manyList._changes = []; - } - } else { - // value is an id - r = self._makeDataPoint({ - modelName: x2manyList.model, - context: x2manyList.context, - fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, - fields: fieldInfo.relatedFields || view && view.fields, - res_id: value, - }); - if (!x2manyList._changes) { - x2manyList._changes = []; - } - x2manyList._changes.push(r.id); - } - }); - - // fetch many2ones display_name - _.each(_.keys(many2ones), function (name) { - defs.push(self._fetchNameGets(x2manyList, name)); - }); - } else if (field.type === 'date') { - // process date: convert into a moment instance - record._changes[name] = fieldUtils.parse.date(result[name]); - } else if (field.type === 'datetime') { - // process datetime: convert into a moment instance - record._changes[name] = fieldUtils.parse.datetime(result[name]); - } else { - record._changes[name] = result[name]; - } - }); - return $.when.apply($, defs) - .then(function () { - var shouldApplyOnchange = false; - var field; - for (var field_name in record.data) { - field = record.fields[field_name]; - if (field.onChange) { - shouldApplyOnchange = true; - } - } - if (shouldApplyOnchange) { - return self._applyOnChange(record, fields_key); - } else { - return $.when(); - } - }) - .then(function () { - return self._fetchRelationalData(record); - }) - .then(function () { - return self._postprocess(record); - }) - .then(function () { - return record.id; - }); - }); - }, /** * This helper method is designed to help developpers that want to use a * field widget outside of a view. In that case, we want a way to create @@ -607,7 +433,7 @@ var BasicModel = AbstractModel.extend({ * * @param {string} model name of the model * @param {Object[]} fields a description of field properties - * @param {Object} [fieldInfo] various field fieldInfo that we want to set + * @param {Object} [fieldInfo] various field info that we want to set * @returns {string} the local id for the created resource */ makeRecord: function (model, fields, fieldInfo) { @@ -616,10 +442,15 @@ var BasicModel = AbstractModel.extend({ _.each(fields, function (field) { record_fields[field.name] = _.pick(field, 'type', 'relation', 'domain'); }); + var viewInfo = {}; + viewInfo.default = fieldInfo || _.mapObject(fields, function () { + return {}; + }); var record = this._makeDataPoint({ modelName: model, fields: record_fields, - fieldsInfo: fieldInfo, + fieldsInfo: viewInfo, + viewType: 'default', }); _.each(fields, function (field) { if ('value' in field) { @@ -670,10 +501,10 @@ var BasicModel = AbstractModel.extend({ var params = { context: element.context, fieldsInfo: element.fieldsInfo, - fieldNames: element.fieldNames, fields: element.fields, + viewType: element.viewType, }; - return this.makeDefaultRecord(element.model, params); + return this._makeDefaultRecord(element.model, params); } this.discardChanges(id); } @@ -703,7 +534,7 @@ var BasicModel = AbstractModel.extend({ if (element.type === 'record') { element.offset = _.indexOf(element.res_ids, element.res_id); } - var loadOptions = _.pick(options, 'fieldNames'); + var loadOptions = _.pick(options, 'fieldNames', 'viewType'); return this._load(element, loadOptions).then(function (result) { return result.id; }); @@ -762,7 +593,8 @@ var BasicModel = AbstractModel.extend({ } if (method === 'create') { - _.each(record.fieldNames, function (name) { + var fieldNames = record.getFieldNames(); + _.each(fieldNames, function (name) { changes[name] = changes[name] || record.data[name]; if (changes[name] === null) { delete changes[name]; @@ -799,34 +631,19 @@ var BasicModel = AbstractModel.extend({ }); }, /** - * Set fieldNames. - * Set fields on a record element (current fields as defaults) - * Set fieldsInfo. - * It is mostly useful for the cases where a record element is shared - * between various views, such as a one2many with a tree and a form view. + * Completes the fields and fieldsInfo of a dataPoint with the given ones. + * It is useful for the cases where a record element is shared between + * various views, such as a one2many with a tree and a form view. * * @param {string} recordID a valid element ID - * @param {Object} props - * @param {Object} props.fieldNames - * @param {Object} props.fields - * @param {Object} props.fieldsInfo - * @return {Object} - * @return {Object} result.fieldNames the previous field names - * @return {Object} result.fields the previous description fields - * @return {Object} result.fieldsInfo the previous field fieldInfo + * @param {Object} viewInfo + * @param {Object} viewInfo.fields + * @param {Object} viewInfo.fieldsInfo */ - setFieldProps: function (recordID, props) { + addFieldsInfo: function (recordID, viewInfo) { var record = this.localData[recordID]; - var result = { - fieldNames: record.fieldNames, - fields: record.fields, - fieldsInfo: record.fieldsInfo, - }; - record.mode = props.mode; - record.fieldNames = Object.keys(props.fieldsInfo); - record.fields = _.defaults(props.fields, record.fields); - record.fieldsInfo = props.fieldsInfo; - return result; + record.fields = _.defaults(record.fields, viewInfo.fields); + record.fieldsInfo = _.defaults(record.fieldsInfo, viewInfo.fieldsInfo); }, /** * For list resources, this changes the orderedBy key, then performs the @@ -1029,7 +846,8 @@ var BasicModel = AbstractModel.extend({ var commands = this._generateX2ManyCommands(record); // compute field values - var currentData = _.pick(_.extend(rawData, commands), record.fieldNames); + var fieldNames = record.getFieldNames(); + var currentData = _.pick(_.extend(rawData, commands), fieldNames); if (record.relationField) { // if there is a relation field, this means that record is an elem // in a one2many. The relation field is the corresponding many2one @@ -1099,16 +917,16 @@ var BasicModel = AbstractModel.extend({ fields: list.fields, fieldsInfo: list.fieldsInfo, modelName: list.model, - fieldNames: _.keys(command[2]), + viewType: list.viewType, }; if (command[0] === 1) { params.res_id = command[1]; } rec = self._makeDataPoint(params); var data = command[2]; - _.each(rec.fieldNames, function (name) { + _.each(Object.keys(data), function (name) { var field = rec.fields[name]; - if (field.type === 'many2one') { + if (field && field.type === 'many2one') { var r = self._makeDataPoint({ modelName: field.relation, data: { @@ -1161,7 +979,7 @@ var BasicModel = AbstractModel.extend({ var self = this; var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; var field = record.fields[fieldName]; - var fieldInfo = record.fieldsInfo[fieldName]; + var fieldInfo = record.fieldsInfo[record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var rec; var defs = []; @@ -1179,7 +997,10 @@ var BasicModel = AbstractModel.extend({ // list's fields with those in the created record var newRecord = this.localData[command.id]; _.defaults(list.fields, newRecord.fields); + _.defaults(list.fieldsInfo, newRecord.fieldsInfo); newRecord.fields = list.fields; + newRecord.fieldsInfo = list.fieldsInfo; + newRecord.viewType = list.viewType; list._changes.push(newRecord.id); break; case 'ADD_M2M': @@ -1193,9 +1014,10 @@ var BasicModel = AbstractModel.extend({ rec = self._makeDataPoint({ context: record.context, modelName: field.relation, - fields: fieldInfo.relatedFields || view && view.fields, - fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, + fields: view ? view.fields : fieldInfo.relatedFields, + fieldsInfo: view ? view.fieldsInfo : fieldInfo.fieldsInfo, res_id: d.id, + viewType: view ? view.type : fieldInfo.viewType, }); list_records[d.id] = rec; list._changes.push(rec.id); @@ -1205,15 +1027,16 @@ var BasicModel = AbstractModel.extend({ // _changes so this is a very specific case) // this could be optimized by registering the fetched records in the list's _cache // so that if a record is removed and then re-added, it won't be fetched twice + var fieldNames = list.getFieldNames(); var def = this._rpc({ model: list.model, method: 'read', - args: [_.pluck(data, 'id'), list.fieldNames] + args: [_.pluck(data, 'id'), fieldNames] }) .then(function (records) { _.each(records, function (record) { list_records[record.id].data = record; - self._parseServerData(list.fieldNames, list.fields, record); + self._parseServerData(fieldNames, list.fields, record); }); }); defs.push(def); @@ -1272,12 +1095,12 @@ var BasicModel = AbstractModel.extend({ _buildOnchangeSpecs: function (record) { // TODO: replace this function by some generic tree function in utils var specs = {}; - _.each(record.fieldNames, function (name) { + _.each(record.getFieldNames(), function (name) { var field = record.fields[name]; - var fieldInfo = record.fieldsInfo[name]; + var fieldInfo = record.fieldsInfo[record.viewType][name]; specs[name] = (field.onChange) || ""; _.each(fieldInfo.views, function (view) { - _.each(view.fieldsInfo, function (field, subname) { + _.each(view.fieldsInfo[view.type], function (field, subname) { specs[name + '.' + subname] = (view.fields[subname].onChange) || ""; }); }); @@ -1335,13 +1158,18 @@ var BasicModel = AbstractModel.extend({ * For a given resource of type 'record', fetch all data. * * @param {Object} record local resource - * @param {string[]} [fieldNames] the list of fields to fetch. If not given, - * fetch all the fields in record.fieldNames (+ display_name) + * @param {Object} [options] + * @param {string[]} [options.fieldNames] the list of fields to fetch. If + * not given, fetch all the fields in record.fieldNames (+ display_name) + * @param {string} [optinos.viewType] the type of view for which the record + * is fetched (usefull to load the adequate fields), by defaults, uses + * record.viewType * @returns {Deferred -> Object} resolves to the record */ - _fetchRecord: function (record, fieldNames) { + _fetchRecord: function (record, options) { var self = this; - fieldNames = fieldNames || _.uniq(record.fieldNames.concat(['display_name'])); + var fieldNames = options && options.fieldNames || + _.uniq(record.getFieldNames().concat(['display_name'])); return this._rpc({ model: record.model, method: 'read', @@ -1356,7 +1184,7 @@ var BasicModel = AbstractModel.extend({ self._parseServerData(fieldNames, record.fields, record.data); }) .then(function () { - return self._fetchX2Manys(record, fieldNames).then(function () { + return self._fetchX2Manys(record, options).then(function () { return self._postprocess(record); }); }); @@ -1369,7 +1197,7 @@ var BasicModel = AbstractModel.extend({ * batching the requests if possible (for example, if 3 many2ones are in * relation on the same model, then we can probably fetch them in one rpc) * - * This method is currently only called by makeDefaultRecord, it should be + * This method is currently only called by _makeDefaultRecord, it should be * called by the onchange methods at some point. * * @todo fix bug: returns a list of deferred, not a deferred @@ -1382,7 +1210,7 @@ var BasicModel = AbstractModel.extend({ var toBeFetched = []; // find all many2one related records to be fetched - _.each(record.fieldNames, function (name) { + _.each(record.getFieldNames(), function (name) { var field = record.fields[name]; if (field.type === 'many2one') { var localId = (record._changes && record._changes[name]) || record.data[name]; @@ -1417,8 +1245,8 @@ var BasicModel = AbstractModel.extend({ _fetchSpecialData: function (record) { var self = this; var fieldNames = []; - return $.when.apply($, _.map(record.fieldNames, function (name) { - var fieldInfo = record.fieldsInfo[name] || {}; + return $.when.apply($, _.map(record.getFieldNames(), function (name) { + var fieldInfo = record.fieldsInfo[record.viewType][name] || {}; var Widget = fieldInfo.Widget; if (Widget && Widget.prototype.specialData) { return self[Widget.prototype.specialData](record, name).then(function (data) { @@ -1551,7 +1379,7 @@ var BasicModel = AbstractModel.extend({ * the undefined value. */ _fetchSpecialStatus: function (record, fieldName) { - var foldField = record.fieldsInfo[fieldName].options.fold_field; + var foldField = record.fieldsInfo[record.viewType][fieldName].options.fold_field; var fieldsToRead = foldField ? [foldField] : []; return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead).then(function (m2os) { _.each(m2os, function (m2o) { @@ -1575,7 +1403,7 @@ var BasicModel = AbstractModel.extend({ _fetchSpecialDomain: function (record, fieldName) { var context = record.getContext({fieldName: fieldName}); - var domainModel = record.fieldsInfo[fieldName].options.model; + var domainModel = record.fieldsInfo[record.viewType][fieldName].options.model; if (record.data.hasOwnProperty(domainModel)) { domainModel = record._changes && record._changes[domainModel] || record.data[domainModel]; } @@ -1642,24 +1470,31 @@ var BasicModel = AbstractModel.extend({ * useless rpcs. * * @param {Object} record local resource - * @param {string[]} [fieldNames] list of fields to fetch + * @param {Object} [options] + * @param {string[]} [options.fieldNames] the list of fields to fetch. + * If not given, fetch all the fields in record.fieldNames + * @param {string} [options.viewType] the type of view for which the main + * record is fetched (useful to load the adequate fields), by defaults, + * uses record.viewType * @returns {Deferred} */ - _fetchX2Manys: function (record, fieldNames) { + _fetchX2Manys: function (record, options) { var self = this; var defs = []; - fieldNames = fieldNames || record.fieldNames; + var fieldNames = options && options.fieldNames || record.getFieldNames(); + var viewType = options && options.viewType || record.viewType; _.each(fieldNames, function (fieldName) { var field = record.fields[fieldName]; if (field.type === 'one2many' || field.type === 'many2many') { - var fieldInfo = record.fieldsInfo[fieldName]; + var fieldInfo = record.fieldsInfo[viewType][fieldName]; var rawContext = fieldInfo && fieldInfo.context; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; + var fieldsInfo = view ? view.fieldsInfo : (fieldInfo.fieldsInfo || {}); var ids = record.data[fieldName] || []; var list = self._makeDataPoint({ count: ids.length, - fieldsInfo: fieldInfo.fieldsInfo || view && view.fieldsInfo, - fields: fieldInfo.relatedFields || view && view.fields, + fieldsInfo: fieldsInfo, + fields: view ? view.fields : fieldInfo.relatedFields, limit: fieldInfo.limit, modelName: field.relation, res_ids: ids, @@ -1668,6 +1503,7 @@ var BasicModel = AbstractModel.extend({ parentID: record.id, rawContext: rawContext, relationField: field.relation_field, + viewType: view ? view.type : fieldInfo.viewType, }); record.data[fieldName] = list.id; if (!fieldInfo.__no_fetch) { @@ -1691,7 +1527,7 @@ var BasicModel = AbstractModel.extend({ _fetchX2ManyBatched: function (list, fieldName) { var self = this; var field = list.fields[fieldName]; - var fieldInfo = list.fieldsInfo[fieldName]; + var fieldInfo = list.fieldsInfo[list.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; // step 1: collect ids @@ -1755,10 +1591,11 @@ var BasicModel = AbstractModel.extend({ */ _fetchX2ManysBatched: function (list) { var defs = []; - for (var i = 0; i < list.fieldNames.length; i++) { - var field = list.fields[list.fieldNames[i]]; + var fieldNames = list.getFieldNames(); + for (var i = 0; i < fieldNames.length; i++) { + var field = list.fields[fieldNames[i]]; if (field.type === 'many2many' || field.type === 'one2many') { - defs.push(this._fetchX2ManyBatched(list, list.fieldNames[i])); + defs.push(this._fetchX2ManyBatched(list, fieldNames[i])); } } return $.when.apply($, defs); @@ -1853,7 +1690,8 @@ var BasicModel = AbstractModel.extend({ context.set_eval_context(this._getEvalContext(element)); if (options && options.fieldName) { - var fieldInfo = element.fieldsInfo[options.fieldName]; + var viewType = options.viewType || element.viewType; + var fieldInfo = element.fieldsInfo[viewType][options.fieldName]; if (fieldInfo && fieldInfo.context) { context.add(fieldInfo.context); } else { @@ -1885,7 +1723,8 @@ var BasicModel = AbstractModel.extend({ */ _getDomain: function (element, options) { if (options && options.fieldName) { - var fieldInfo = element.fieldsInfo[options.fieldName]; + var viewType = options.viewType || element.viewType; + var fieldInfo = element.fieldsInfo[viewType][options.fieldName]; if (fieldInfo && fieldInfo.domain) { return Domain.prototype.stringToArray( fieldInfo.domain, @@ -1930,6 +1769,16 @@ var BasicModel = AbstractModel.extend({ } return evalContext; }, + /** + * Returns the list of field names of the given element according to its + * default view type. + * + * @param {Object} element an element from the localData + * @returns {string[]} the list of field names + */ + _getFieldNames: function (element) { + return Object.keys(element.fieldsInfo[element.viewType] || {}); + }, /** * Helper method for the load entry point. * @@ -1942,7 +1791,7 @@ var BasicModel = AbstractModel.extend({ */ _load: function (dataPoint, options) { if (dataPoint.type === 'record') { - return this._fetchRecord(dataPoint, options && options.fieldNames); + return this._fetchRecord(dataPoint, options); } if (dataPoint.type === 'list' && dataPoint.groupedBy.length) { return this._readGroup(dataPoint); @@ -1979,13 +1828,6 @@ var BasicModel = AbstractModel.extend({ id: {type: 'integer'}, }, params.fields); - if (!params.fieldsInfo) { - params.fieldsInfo = {}; - for (var fieldName in fields) { - params.fieldsInfo[fieldName] = {}; - } - } - var dataPoint = { _cache: type === 'list' ? {} : undefined, _changes: null, @@ -1994,9 +1836,8 @@ var BasicModel = AbstractModel.extend({ count: params.count || res_ids.length, data: params.data || (type === 'record' ? {} : []), domain: params.domain || [], - fieldNames: params.fieldNames || Object.keys(params.fieldsInfo), fields: fields, - fieldsInfo: params.fieldsInfo || {}, + fieldsInfo: params.fieldsInfo, groupedBy: params.groupedBy || [], id: _.uniqueId(params.modelName + '_'), isOpen: params.isOpen, @@ -2015,15 +1856,207 @@ var BasicModel = AbstractModel.extend({ static: params.static || false, type: type, // 'record' | 'list' value: value, + viewType: params.viewType, }; dataPoint.getContext = this._getContext.bind(this, dataPoint); dataPoint.getDomain = this._getDomain.bind(this, dataPoint); + dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint); this.localData[dataPoint.id] = dataPoint; return dataPoint; }, + /** + * When one needs to create a record from scratch, a not so simple process + * needs to be done: + * - call the /default_get route to get default values + * - fetch all relational data + * - apply all onchanges if necessary + * - fetch all relational data + * + * This method tries to optimize the process as much as possible. Also, + * it is quite horrible and should be refactored at some point. + * + * @private + * @param {any} params + * @param {string} modelName model name + * @param {Object} params.context the context for the new record + * @param {Object} params.fieldsInfo contains the fieldInfo of each view, + * for each field + * @param {Object} params.fields contains the description of each field + * @param {Object} params.context the context for the new record + * @param {string} params.viewType the key in fieldsInfo of the fields to load + * @returns {Deferred -> string} resolves to the id for the created resource + */ + _makeDefaultRecord: function (modelName, params) { + var self = this; + var fieldNames = Object.keys(params.fieldsInfo[params.viewType]); + var fields_key = _.without(fieldNames, '__last_update'); + + return this._rpc({ + model: modelName, + method: 'default_get', + args: [fields_key], + context: params.context, + }) + .then(function (result) { + // fill default values for missing fields + for (var i = 0; i < fieldNames.length; i++) { + var fieldName = fieldNames[i]; + if (!(fieldName in result)) { + var field = params.fields[fieldName]; + if (field.type === 'one2many' || field.type === 'many2many') { + result[fieldName] = []; + } else { + result[fieldName] = null; + } + } + } + + var data = {}; + var record = self._makeDataPoint({ + modelName: modelName, + data: data, + fields: params.fields, + fieldsInfo: params.fieldsInfo, + context: params.context, + parentID: params.parentID, + relationField: params.relationField, + res_ids: params.res_ids, + viewType: params.viewType, + }); + + var defs = []; // FIXME: remove defs? + _.each(fieldNames, function (name) { + var field = params.fields[name]; + data[name] = null; + record._changes = record._changes || {}; + if (field.type === 'many2one' && result[name]) { + var rec = self._makeDataPoint({ + context: record.context, + data: {id: result[name]}, + modelName: field.relation, + }); + record._changes[name] = rec.id; + } else if (field.type === 'one2many' || field.type === 'many2many') { + var fieldInfo = record.fieldsInfo[record.viewType][name]; + var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; + var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo; + var fields = view ? view.fields : fieldInfo.relatedFields; + var viewType = view ? view.type : fieldInfo.viewType; + + var x2manyList = self._makeDataPoint({ + context: record.context, + fieldsInfo: fieldsInfo, + fields: fields, + limit: fieldInfo.limit, + modelName: field.relation, + parentID: record.id, + rawContext: fieldInfo && fieldInfo.context, + relationField: field.relation_field, + res_ids: [], + static: true, + type: 'list', + viewType: viewType, + }); + record._changes[name] = x2manyList.id; + var many2ones = {}; + var r; + _.each(result[name], function (value) { + if (_.isArray(value)) { + // value is a command + if (value[0] === 0) { + // CREATE + r = self._makeDataPoint({ + modelName: x2manyList.model, + context: x2manyList.context, + fieldsInfo: fieldsInfo, + fields: fields, + viewType: viewType, + }); + x2manyList._changes.push(r.id); + r._changes = value[2]; + + // this is necessary so the fields are initialized + for (var fieldName in value[2]) { + r.data[fieldName] = null; + } + + for (var name in r._changes) { + if (r.fields[name].type === 'many2one') { + var rec = self._makeDataPoint({ + context: r.context, + modelName: r.fields[name].relation, + data: {id: r._changes[name]} + }); + r._changes[name] = rec.id; + many2ones[name] = true; + } + } + } + if (value[0] === 6) { + // REPLACE_WITH + x2manyList._changes = []; + } + } else { + // value is an id + r = self._makeDataPoint({ + modelName: x2manyList.model, + context: x2manyList.context, + fieldsInfo: fieldsInfo, + fields: fields, + res_id: value, + viewType: viewType, + }); + if (!x2manyList._changes) { + x2manyList._changes = []; + } + x2manyList._changes.push(r.id); + } + }); + + // fetch many2ones display_name + _.each(_.keys(many2ones), function (name) { + defs.push(self._fetchNameGets(x2manyList, name)); + }); + } else if (field.type === 'date') { + // process date: convert into a moment instance + record._changes[name] = fieldUtils.parse.date(result[name]); + } else if (field.type === 'datetime') { + // process datetime: convert into a moment instance + record._changes[name] = fieldUtils.parse.datetime(result[name]); + } else { + record._changes[name] = result[name]; + } + }); + return $.when.apply($, defs) + .then(function () { + var shouldApplyOnchange = false; + var field; + for (var field_name in record.data) { + field = record.fields[field_name]; + if (field.onChange) { + shouldApplyOnchange = true; + } + } + if (shouldApplyOnchange) { + return self._applyOnChange(record, fields_key); + } else { + return $.when(); + } + }) + .then(function () { + return self._fetchRelationalData(record); + }) + .then(function () { + return self._postprocess(record); + }) + .then(function () { + return record.id; + }); + }); + }, /** * parse the server values to javascript framwork * @@ -2070,7 +2103,7 @@ var BasicModel = AbstractModel.extend({ * quite a lot of computations to determine what needs to be fetched. This * method is doing that. * - * @see _fetchRecord @see makeDefaultRecord + * @see _fetchRecord @see _makeDefaultRecord * * @param {any} record * @returns {Deferred -> Object} resolves to the finished resource @@ -2078,9 +2111,9 @@ var BasicModel = AbstractModel.extend({ _postprocess: function (record) { var self = this; var defs = []; - _.each(record.fieldNames, function (name) { + _.each(record.getFieldNames(), function (name) { var field = record.fields[name]; - var fieldInfo = record.fieldsInfo[name] || {}; + var fieldInfo = record.fieldsInfo[record.viewType][name] || {}; var options = fieldInfo.options || {}; if (options.always_reload) { if (record.fields[name].type === 'many2one' && record.data[name]) { @@ -2114,7 +2147,7 @@ var BasicModel = AbstractModel.extend({ */ _readGroup: function (list) { var self = this; - var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); + var fields = _.uniq(list.getFieldNames().concat(list.groupedBy)); return this._rpc({ model: list.model, method: 'read_group', @@ -2146,7 +2179,6 @@ var BasicModel = AbstractModel.extend({ domain: group.__domain, context: list.context, fields: list.fields, - fieldNames: list.fieldNames, fieldsInfo: list.fieldsInfo, value: group[rawGroupBy], aggregateValues: aggregateValues, @@ -2155,6 +2187,7 @@ var BasicModel = AbstractModel.extend({ limit: list.limit, openGroupByDefault: list.openGroupByDefault, type: 'list', + viewType: list.viewType, }); list.data.push(newGroup.id); list.count += newGroup.count; @@ -2201,6 +2234,7 @@ var BasicModel = AbstractModel.extend({ var ids = []; var missingIds = []; var upper_bound = list.limit ? Math.min(list.offset + list.limit, list.count) : list.count; + var fieldNames = list.getFieldNames(); for (var i = list.offset; i < upper_bound; i++) { var id = list.res_ids[i]; ids.push(id); @@ -2212,7 +2246,7 @@ var BasicModel = AbstractModel.extend({ def = this._rpc({ model: list.model, method: 'read', - args: [missingIds, list.fieldNames], + args: [missingIds, fieldNames], context: {} // FIXME }); } else { @@ -2228,14 +2262,14 @@ var BasicModel = AbstractModel.extend({ dataPoint = self._makeDataPoint({ data: _.findWhere(records, {id: id}), fieldsInfo: list.fieldsInfo, - fieldNames: list.fieldNames, fields: list.fields, modelName: list.model, parentID: list.id, + viewType: list.viewType, }); // add many2one records - self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); + self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data); list._cache[id] = dataPoint.id; } list.data.push(dataPoint.id); @@ -2272,10 +2306,11 @@ var BasicModel = AbstractModel.extend({ */ _searchReadUngroupedList: function (list) { var self = this; + var fieldNames = list.getFieldNames(); return this._rpc({ route: '/web/dataset/search_read', model: list.model, - fields: list.fieldNames, + fields: fieldNames, domain: list.domain || [], limit: list.limit, offset: list.offset, @@ -2287,14 +2322,14 @@ var BasicModel = AbstractModel.extend({ var dataPoint = self._makeDataPoint({ data: record, fields: list.fields, - fieldNames: list.fieldNames, fieldsInfo: list.fieldsInfo, modelName: list.model, parentID: list.id, + viewType: list.viewType, }); // add many2one records - self._parseServerData(list.fieldNames, dataPoint.fields, dataPoint.data); + self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data); return dataPoint.id; }); list.data = data; -- cgit v1.2.3 From 1fadf4a793920870e7dd8fb4c95f4b314fdd28fe Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 30 Mar 2017 13:45:27 +0200 Subject: [FIX] web: o2m: save updates of one2many records With this commit, updates of existing o2m records are now correctly saved in DB when clicking on 'Save' in the main form view. --- .../web/static/src/js/views/basic/basic_model.js | 49 ++++++++++++---------- 1 file changed, 26 insertions(+), 23 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 00e2fc43f6d..e105b78a836 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -201,7 +201,7 @@ var BasicModel = AbstractModel.extend({ }) .then(function () { _.each(records, function (record) { - record.res_ids.splice(record.offset, 1)[0]; + record.res_ids.splice(record.offset, 1); record.res_id = record.res_ids[record.offset]; record.count--; }); @@ -215,11 +215,15 @@ var BasicModel = AbstractModel.extend({ * everything that was stored in a _changes key. * * @param {string} id local resource id + * @param {Object} [options] + * @param {boolean} [options.rollback=false] if true, the changes will + * be reset to the last _savePoint, otherwise, they are reset to null */ - discardChanges: function (id) { + discardChanges: function (id, options) { + options = options || {}; var element = this.localData[id]; this._visitChildren(element, function (elem) { - elem._changes = null; + elem._changes = options.rollback && elem._savePoint || null; }); }, /** @@ -550,9 +554,9 @@ var BasicModel = AbstractModel.extend({ * @param {string} record_id local resource * @param {Object} [options] * @param {boolean} [options.reload=true] if true, data will be reloaded - * @param {boolean} [options.localSave=false] if true, the record will only - * be 'locally' saved: its changes will move from the _changes key to the - * data key + * @param {boolean} [options.savePoint=false] if true, the record will only + * be 'locally' saved: its changes written in a _savePoint key that can + * be restored later by call discardChanges with option rollback to true * @returns {Deferred} */ save: function (record_id, options) { @@ -560,10 +564,9 @@ var BasicModel = AbstractModel.extend({ return this.mutex.exec(function () { options = options || {}; var record = self.localData[record_id]; - if (options.localSave) { + if (options.savePoint) { if (record._changes) { - _.extend(record.data, record._changes); - record._changes = null; + record._savePoint = _.extend(record._savePoint || {}, record._changes); } return $.when(); } @@ -735,7 +738,7 @@ var BasicModel = AbstractModel.extend({ * notifyChanges, it is not protected by a mutex. Every changes from the * user to the model go through this method. * - * @param {string} recordID + * @param {string} recordID * @param {Object} changes * @returns {Deferred} */ @@ -1647,25 +1650,25 @@ var BasicModel = AbstractModel.extend({ addedIds = _.difference(relIds, list.res_ids); keptIds = _.intersection(list.res_ids, relIds); for (i = 0; i < keptIds.length; i++) { - commands[fieldName].push(x2ManyCommands.link_to(keptIds[i])); - } - for (i = 0; i < removedIds.length; i++) { - commands[fieldName].push(x2ManyCommands.delete(removedIds[i])); + relRecord = _.findWhere(relData, {res_id: keptIds[i]}); + var command; + if (!_.isEmpty(relRecord._changes)) { + var r = this.get(relRecord.id, {raw: true}); + var changes = _.pick(r.data, Object.keys(relRecord._changes)); + command = x2ManyCommands.update(relRecord.res_id, changes); + } else { + command = x2ManyCommands.link_to(keptIds[i]); + } + commands[fieldName].push(command); } for (i = 0; i < addedIds.length; i++) { relRecord = _.findWhere(relData, {res_id: addedIds[i]}); relRecord = this.get(relRecord.id, {raw: true}); commands[fieldName].push(x2ManyCommands.create(_.omit(relRecord.data, 'id'))); } - // FIXME: update of records temporarily disabled as it doesn't work if we updated - // the record in a form view, as the _changes contains all the fields of the - // form view instead of the one that have changed - // _.each(relData, function (relRecord) { - // var new_values = relRecord._changes; - // if (!_.isEmpty(new_values)) { - // commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, new_values)); - // } - // }); + for (i = 0; i < removedIds.length; i++) { + commands[fieldName].push(x2ManyCommands.delete(removedIds[i])); + } } } } -- cgit v1.2.3 From 28e1620731604410216239fee70a7b1cbfd3b7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 31 Mar 2017 16:03:46 +0200 Subject: [FIX] web: list view did not discard properly To add a record to a list, the list controller called the addDefaultRecord method. But that method did not made the changes to the _changes key, it edited in place the data key. So, this means that it was not possible to discard the add a line. --- addons/web/static/src/js/views/basic/basic_model.js | 5 +++-- 1 file changed, 3 insertions(+), 2 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 e105b78a836..f93635618e3 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -167,10 +167,11 @@ var BasicModel = AbstractModel.extend({ }; return this._makeDefaultRecord(list.model, params).then(function (id) { list.count++; + list._changes = list.data.slice(0); if (position === 'top') { - list.data.unshift(id); + list._changes.unshift(id); } else { - list.data.push(id); + list._changes.push(id); } return id; }); -- cgit v1.2.3 From 21324ded09883d08ec951b0e3cc80ff5ed270aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 31 Mar 2017 16:26:50 +0200 Subject: [FIX] web: do not create new line if onchange has warning in o2m In a one2many field, when the user clicks on the 'Add a line' button, we need to create a new record. This means that we call the default_get route, then call the onchanges. The onchange can return a 'warning' key, which is displayed to the user. But in the specific case of a one2many, we don't want to actually create the record. So, we reject the deferred to prevent that from happening. --- addons/web/static/src/js/views/basic/basic_model.js | 8 +++++++- 1 file changed, 7 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 f93635618e3..9139906f982 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -770,6 +770,7 @@ var BasicModel = AbstractModel.extend({ var onchangeDef; if (onChangeFields.length) { onchangeDef = self._applyOnChange(record, onChangeFields).then(function (result) { + delete record._warning; return _.keys(changes).concat(Object.keys(result && result.value || {})); }); } else { @@ -884,6 +885,7 @@ var BasicModel = AbstractModel.extend({ title: result.warning.title, type: 'dialog', }); + record._warning = true; } var defs = []; _.each(result.value, function (val, name) { @@ -2045,7 +2047,11 @@ var BasicModel = AbstractModel.extend({ } } if (shouldApplyOnchange) { - return self._applyOnChange(record, fields_key); + return self._applyOnChange(record, fields_key).then(function () { + if (record._warning) { + return $.Deferred().reject(); + } + }); } else { return $.when(); } -- cgit v1.2.3 From 3662c7246f90e8a7ea67aa52c71afd0cede330de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sat, 1 Apr 2017 09:36:23 +0200 Subject: [FIX] web: unselect current row on one2many before doing something else In a one2many, when we click on 'Add an item', we need to unselect the current row. Otherwise, its changes will not be committed and it will be in practice discarded. --- 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 9139906f982..8f20f46b1cf 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -167,7 +167,7 @@ var BasicModel = AbstractModel.extend({ }; return this._makeDefaultRecord(list.model, params).then(function (id) { list.count++; - list._changes = list.data.slice(0); + list._changes = list._changes || list.data.slice(0); if (position === 'top') { list._changes.unshift(id); } else { -- cgit v1.2.3 From cef1b273c06ba277c6d47a538c203872c4594898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sat, 1 Apr 2017 13:30:12 +0200 Subject: [FIX] web: prevent issue where changes where done to real data Before this commit, the basic model used the real data array as changes in the method _applyX2ManyChange, which means that each change was also done to the data array. --- 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 8f20f46b1cf..0b5de486bb9 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -989,7 +989,7 @@ var BasicModel = AbstractModel.extend({ var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; var rec; var defs = []; - list._changes = list._changes || list.data; + list._changes = list._changes || list.data.slice(0); switch (command.operation) { case 'ADD': -- cgit v1.2.3 From 9100e675dfe7ae60ea3c8f1a5ef1e9a78a4ab4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 2 Apr 2017 21:04:31 +0200 Subject: [FIX] web: add support for discarding lines in o2m This is not a complete work, but it introduces the basic infrastructure to properly edit various more sophisticated editing behaviours. The main focus of this commit is to handle discarding invalid lines in one2many, potentially asking the user for confirmation. So, the relevant methods have to be modified to return some deferred, because the operation is now asynchronous. --- .../web/static/src/js/views/basic/basic_model.js | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) (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 0b5de486bb9..1fe0e52130b 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -544,6 +544,22 @@ var BasicModel = AbstractModel.extend({ return result.id; }); }, + /** + * In some case, we may need to remove an element from a list, without going + * through the notifyChanges machinery. The motivation for this is when the + * user click on 'Add an item' in a field one2many with a required field, + * then clicks somewhere else. The new line need to be discarded, but we + * don't want to trigger a real notifyChanges (no need for that, and also, + * we don't want to rerender the UI). + * + * @param {string} elementID some valid element id. It is necessary that the + * corresponding element has a parent. + */ + removeLine: function (elementID) { + var record = this.localData[elementID]; + var parent = this.localData[record.parentID]; + parent._changes = _.without(parent._changes, elementID); + }, /** * Save a local resource, if needed. This is a complicated operation, * - it needs to check all changes, @@ -1652,6 +1668,11 @@ var BasicModel = AbstractModel.extend({ removedIds = _.difference(list.res_ids, relIds); addedIds = _.difference(relIds, list.res_ids); keptIds = _.intersection(list.res_ids, relIds); + + // the didChange variable keep track of the fact that at + // least one id was updated + var didChange = false; + for (i = 0; i < keptIds.length; i++) { relRecord = _.findWhere(relData, {res_id: keptIds[i]}); var command; @@ -1659,11 +1680,18 @@ var BasicModel = AbstractModel.extend({ var r = this.get(relRecord.id, {raw: true}); var changes = _.pick(r.data, Object.keys(relRecord._changes)); command = x2ManyCommands.update(relRecord.res_id, changes); + didChange = true; } else { command = x2ManyCommands.link_to(keptIds[i]); } commands[fieldName].push(command); } + if (!didChange && addedIds.length === 0 && removedIds.length === 0) { + // in this situation, we have no changed ids, no added + // ids and no removed ids, so we can safely ignore the + // last changes + commands[fieldName] = null; + } for (i = 0; i < addedIds.length; i++) { relRecord = _.findWhere(relData, {res_id: addedIds[i]}); relRecord = this.get(relRecord.id, {raw: true}); -- cgit v1.2.3 From dbc6f21321e11cdaac92bd12ce08479471b9d7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 3 Apr 2017 21:21:32 +0200 Subject: [FIX] web: properly evaluate domains in kanban views We still did not properly evaluate domains in kanban view. The proper eval context is quite tricky to get right, because we need to perform some computations. With this commit, we simply use the code that was already in the basic model. --- addons/web/static/src/js/views/basic/basic_model.js | 1 + 1 file changed, 1 insertion(+) (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 1fe0e52130b..5f934ea4e65 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1894,6 +1894,7 @@ var BasicModel = AbstractModel.extend({ }; dataPoint.getContext = this._getContext.bind(this, dataPoint); + dataPoint.getEvalContext = this._getEvalContext.bind(this, dataPoint); dataPoint.getDomain = this._getDomain.bind(this, dataPoint); dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint); -- cgit v1.2.3 From a3271fa6633b3ff771247fb0df8aa28401c92881 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Tue, 4 Apr 2017 11:59:54 +0200 Subject: [FIX] web: formview button context should be given to actions When clicking on a stat button in a form view, an action is executed. The button might also be given a context which have to be used when running the action. This context was ignored since the context refactoring made with commit 26a7fe7de1b6258989bc63335321f8f8e83435af. --- addons/web/static/src/js/views/basic/basic_model.js | 5 +++++ 1 file changed, 5 insertions(+) (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 5f934ea4e65..dc0beb96942 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1714,6 +1714,8 @@ var BasicModel = AbstractModel.extend({ * * @param {Object} element an element from the localData * @param {Object} [options] + * @param {string|Object} [options.additionalContext] + * another context to evaluate and merge to the returned context * @param {string} [options.fieldName] * the name of the field whose context needs to be added to the * result (and evaluated) @@ -1723,6 +1725,9 @@ var BasicModel = AbstractModel.extend({ var context = new Context(session.user_context, element.context); context.set_eval_context(this._getEvalContext(element)); + if (options && options.additionalContext) { + context.add(options.additionalContext); + } if (options && options.fieldName) { var viewType = options.viewType || element.viewType; var fieldInfo = element.fieldsInfo[viewType][options.fieldName]; -- cgit v1.2.3 From 2bca6535849295e76029b0c237b6367e1162d23a Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 3 Apr 2017 11:53:38 +0200 Subject: [FIX] web: BasicModel: correctly generate x2many commands This commit fixes two problems. First, the commands weren't correctly generated for relational fields inside relational fields. For example, if a form view contained a one2many containing itself a many2many, the commands weren't generated for the many2many inside the one2many. Thus, it's value couldn't be saved. Second, when doing an onchange RPC, all record data should be sent to the server, not only what have changed. Before this rev., for x2many fields, commands were sent only if the field had changed. --- .../web/static/src/js/views/basic/basic_model.js | 97 +++++++++++----------- 1 file changed, 48 insertions(+), 49 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 dc0beb96942..0642fd6842c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -589,28 +589,7 @@ var BasicModel = AbstractModel.extend({ } 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); - for (var fieldName in commands) { - if (commands[fieldName] === null) { - delete changes[fieldName]; - } else { - changes[fieldName] = commands[fieldName]; - } - } - - // replace local ids by actual ids in relational changes - for (fieldName in changes) { - var field = record.fields[fieldName]; - if (field.type === 'many2one') { - if (changes[fieldName]) { - var res_id = self.localData[changes[fieldName]].res_id; - changes[fieldName] = res_id; - } else { - changes[fieldName] = false; - } - } - } + var changes = self._generateChanges(record); if (method === 'create') { var fieldNames = record.getFieldNames(); @@ -864,7 +843,7 @@ var BasicModel = AbstractModel.extend({ var context = this._getContext(record); var rawData = this.get(record.id, {raw: true}).data; - var commands = this._generateX2ManyCommands(record); + var commands = this._generateX2ManyCommands(record, false); // compute field values var fieldNames = record.getFieldNames(); @@ -1622,41 +1601,65 @@ var BasicModel = AbstractModel.extend({ } return $.when.apply($, defs); }, + /** + * Generates an object mapping field names to their changed value in a given + * record (i.e. maps to the new value for basic fields, to the res_id for + * many2ones and to commands for x2manys). + * + * @private + * @param {Object} record + * @returns {Object} a map from changed fields to their new value + */ + _generateChanges: function (record) { + var changes = _.extend({}, record._changes); + var commands = this._generateX2ManyCommands(record, true); + for (var fieldName in record.fields) { + var type = record.fields[fieldName].type; + if (type === 'one2many' || type === 'many2many') { + if (commands[fieldName].length) { // replace localId by commands + changes[fieldName] = commands[fieldName]; + } else { // no command -> no change for that field + delete changes[fieldName]; + } + } else if (type === 'many2one' && fieldName in changes) { + var value = changes[fieldName]; + changes[fieldName] = value ? this.localData[value].res_id : false; + } + } + return changes; + }, /** * Read all x2many fields and generate the commands for the server to create * or write them... * * @param {Object} record + * @param {boolean} [changesOnly=false] if true, only generates commands for + * fields that have changed * @returns {Object} a map from some field names to commands */ - _generateX2ManyCommands: function (record) { + _generateX2ManyCommands: function (record, changesOnly) { var self = this; var commands = {}; - var type, list, relData, relIds, removedIds, addedIds, keptIds, relRecord, i; - var data = _.extend({}, record.data, record._changes); - + var type; for (var fieldName in record.fields) { type = record.fields[fieldName].type; if (type === 'many2many' || type === 'one2many') { - // skip if this field is empty + commands[fieldName] = []; if (!data[fieldName]) { - commands[fieldName] = null; + // skip if this field is empty continue; } - list = this.localData[data[fieldName]]; - if (!list._changes) { - commands[fieldName] = null; - // skip if this field hasn't changed + var list = this.localData[data[fieldName]]; + if (changesOnly && !list._changes) { + // if only changes are requested, skip if there is no change continue; } - relData = _.map(list._changes, function (localId) { + var relData = _.map(list._changes || list.data, function (localId) { return self.localData[localId]; }); - relIds = _.pluck(relData, 'res_id'); - commands[fieldName] = []; - + var relIds = _.pluck(relData, 'res_id'); if (type === 'many2many' || list._forceM2MLink) { // deliberately generate a single 'replace' command instead // of a 'delete' and a 'link' commands with the exact diff @@ -1665,20 +1668,17 @@ var BasicModel = AbstractModel.extend({ // an complete override of the actual value of the m2m) commands[fieldName].push(x2ManyCommands.replace_with(relIds)); } else if (type === 'one2many') { - removedIds = _.difference(list.res_ids, relIds); - addedIds = _.difference(relIds, list.res_ids); - keptIds = _.intersection(list.res_ids, relIds); - - // the didChange variable keep track of the fact that at + var removedIds = _.difference(list.res_ids, relIds); + var addedIds = _.difference(relIds, list.res_ids); + var keptIds = _.intersection(list.res_ids, relIds); + // the didChange variable keeps track of the fact that at // least one id was updated var didChange = false; - + var command, i, relRecord; for (i = 0; i < keptIds.length; i++) { relRecord = _.findWhere(relData, {res_id: keptIds[i]}); - var command; if (!_.isEmpty(relRecord._changes)) { - var r = this.get(relRecord.id, {raw: true}); - var changes = _.pick(r.data, Object.keys(relRecord._changes)); + var changes = this._generateChanges(relRecord); command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; } else { @@ -1686,11 +1686,11 @@ var BasicModel = AbstractModel.extend({ } commands[fieldName].push(command); } - if (!didChange && addedIds.length === 0 && removedIds.length === 0) { + if (changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) { // in this situation, we have no changed ids, no added // ids and no removed ids, so we can safely ignore the // last changes - commands[fieldName] = null; + commands[fieldName] = []; } for (i = 0; i < addedIds.length; i++) { relRecord = _.findWhere(relData, {res_id: addedIds[i]}); @@ -1703,7 +1703,6 @@ var BasicModel = AbstractModel.extend({ } } } - return commands; }, /** -- cgit v1.2.3 From 3ff7ff3e94e4fe34ec09a45fda04f0c8de33cd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 4 Apr 2017 15:08:00 +0200 Subject: [FIX] web: properly evaluate unset numeric values in domain Before this commit, when a numeric value was not set (for example, a new record with no default_get value), then it was not properly evaluated as 0 in a domain. We also slightly refactored the code so we always use the same method to compute the evaluation context. --- addons/web/static/src/js/views/basic/basic_model.js | 11 ++++++++++- 1 file changed, 10 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 0642fd6842c..f59cf96a881 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -268,6 +268,8 @@ var BasicModel = AbstractModel.extend({ * @param {boolean} [options.env=false] if true, will only return res_id * (if record) or res_ids (if list) * @param {boolean} [options.raw=false] if true, will not follow relations + * @param {boolean} [options.noUnsetNumeric=false] if true, will set numeric + * values to 0 if not set * @returns {Object} */ get: function (id, options) { @@ -311,6 +313,13 @@ var BasicModel = AbstractModel.extend({ if (!field) { continue; } + if (options.noUnsetNumeric) { + if (field.type === 'float' || + field.type === 'integer' || + field.type === 'monetary') { + element.data[fieldName] = element.data[fieldName] || 0; + } + } // get relational datapoint if (field.type === 'many2one') { @@ -1792,7 +1801,7 @@ var BasicModel = AbstractModel.extend({ * @returns {Object} */ _getEvalContext: function (element) { - var evalContext = this.get(element.id, {raw: true}).data; + var evalContext = this.get(element.id, {raw: true, noUnsetNumeric: true}).data; evalContext.active_model = element.model; if (evalContext.id) { evalContext.active_id = evalContext.id; -- cgit v1.2.3 From 920df597d3d7174723736a395e94dc8ff413c70b Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 29 Mar 2017 17:25:18 +0200 Subject: [REF] barcode, *: update code to new JS framework With the new views, the barcode mechanisms were completely broken, because the view and widget JS code was totally changed. This commit update and refactor all that code to make it work with the new design philosophy. Also, it update the coding style. --- addons/web/static/src/js/views/basic/basic_model.js | 8 +++++++- 1 file changed, 7 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 f59cf96a881..ced26fb7cb2 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1052,7 +1052,13 @@ var BasicModel = AbstractModel.extend({ defs.push(def); break; case 'CREATE': - defs.push(this.addDefaultRecord(list.id)); + if (command.data) { + defs.push(this.addDefaultRecord(list.id).then(function (id) { + return this.notifyChanges(id, command.data); + })); + } else { + defs.push(this.addDefaultRecord(list.id)); + } break; case 'UPDATE': defs.push(this._applyChange(command.id, command.data)); -- cgit v1.2.3 From 4872c8da31bc10738c0f195c8665b59ec61f0e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 5 Apr 2017 12:15:49 +0200 Subject: [FIX] web: don't save readonly fields Readonly fields can be modified with onchanges, but they should not be saved. --- addons/web/static/src/js/views/basic/basic_model.js | 9 +++++++++ 1 file changed, 9 insertions(+) (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 ced26fb7cb2..304f7e475f7 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -612,6 +612,15 @@ var BasicModel = AbstractModel.extend({ // make sure we don't write an undefined id delete changes.id; + // remove readonly fields from the list of changes + if (method === 'write') { + for (var fieldName in changes) { + if (record.fields[fieldName].readonly) { + delete changes[fieldName]; + } + } + } + // in the case of a write, only perform the RPC if there are changes to save if (method === 'create' || Object.keys(changes).length) { var args = method === 'write' ? [[record.data.id], changes] : [changes]; -- cgit v1.2.3 From bc89bb79d2eac7db2861127bc41dcaf131568d4b Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 5 Apr 2017 11:16:37 +0200 Subject: [FIX] web: BasicModel: ignore unknown fields after onchange It may happen that an onchange returns values for unknown fields in the view. In that case, the corresponding values need to be ignored as the postprocessing base on the field type can't be done, which may lead to errors later on (e.g. when saving). For example: one2many list in a form view which doesn't display a many2one field that exists on the related model. If an onchange on the main form view updates the value of the many2one inside the one2many, it will crash later on when the user will try to save (because the value [id, display_name] will be sent to the server). --- addons/web/static/src/js/views/basic/basic_model.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 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 304f7e475f7..47d5cee5c9c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -942,10 +942,16 @@ var BasicModel = AbstractModel.extend({ params.res_id = command[1]; } rec = self._makeDataPoint(params); - var data = command[2]; - _.each(Object.keys(data), function (name) { + var data = {}; + _.each(Object.keys(command[2]), function (name) { var field = rec.fields[name]; - if (field && field.type === 'many2one') { + if (!field) { + // ignore if this field is unknown as we don't + // know if it requires a special handling + return; + } + data[name] = command[2][name]; + if (field.type === 'many2one') { var r = self._makeDataPoint({ modelName: field.relation, data: { @@ -957,9 +963,6 @@ var BasicModel = AbstractModel.extend({ } }); rec._changes = data; - for (var f in rec._changes) { - rec.data[f] = null; - } list._changes.push(rec.id); } if (command[0] === 5) { -- cgit v1.2.3 From d0f321d6cc198045ff796865d94ddd15bd6d0b99 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 5 Apr 2017 11:32:56 +0200 Subject: [FIX] web: Form: don't discard onchange when editing a o2m Assume that there is an onchange on a form view that upates the records of a one2many field. Before this rev., if the user edited one of those one2many records (in a dialog) after the onchange, the onchange values were lost. --- addons/web/static/src/js/views/basic/basic_model.js | 6 +++++- 1 file changed, 5 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 47d5cee5c9c..c71ac81e0d9 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -504,6 +504,8 @@ var BasicModel = AbstractModel.extend({ * * @param {string} id local id for a resource * @param {Object} [options] + * @param {boolean} [options.keepChanges=false] if true, doesn't discard the + * changes on the record before reloading it * @returns {Deferred -> string} resolves to the id of the resource */ reload: function (id, options) { @@ -520,7 +522,9 @@ var BasicModel = AbstractModel.extend({ }; return this._makeDefaultRecord(element.model, params); } - this.discardChanges(id); + if (!options.keepChanges) { + this.discardChanges(id); + } } if (options.context !== undefined) { -- cgit v1.2.3 From 7e14e819038bb3eccaebb781790c0c58f07f1c64 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 5 Apr 2017 22:34:10 +0200 Subject: [FIX] web: one2many onchanges When there is an onchange on fields of a one2many, the data given to the onchange RPC should contain an additional key for the corresponding many2one, mapping to the current data of the parent (main) record. This wasn't working before this rev. for new o2m records created when the one2many wasn't editable inline (with a list view editable), because in that particular case, the reference to the parent record wasn't specified correctly. --- .../web/static/src/js/views/basic/basic_model.js | 48 ++++++++++++++-------- 1 file changed, 30 insertions(+), 18 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 c71ac81e0d9..1f8d5ce94c6 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -162,7 +162,6 @@ var BasicModel = AbstractModel.extend({ fields: list.fields, fieldsInfo: list.fieldsInfo, parentID: list.id, - relationField: list.relationField, viewType: list.viewType, }; return this._makeDefaultRecord(list.model, params).then(function (id) { @@ -863,22 +862,7 @@ var BasicModel = AbstractModel.extend({ var onchange_spec = this._buildOnchangeSpecs(record); var idList = record.data.id ? [record.data.id] : []; var context = this._getContext(record); - - var rawData = this.get(record.id, {raw: true}).data; - var commands = this._generateX2ManyCommands(record, false); - - // compute field values - var fieldNames = record.getFieldNames(); - var currentData = _.pick(_.extend(rawData, commands), fieldNames); - if (record.relationField) { - // if there is a relation field, this means that record is an elem - // in a one2many. The relation field is the corresponding many2one - - // parent is the list element containing all the records in the o2m - // and parent.parentID is the ID of the main record - var parent = this.localData[record.parentID]; - currentData[record.relationField] = this.get(parent.parentID, {raw:true}).data; - } + var currentData = this._generateOnChangeData(record); if (fields.length === 1) { fields = fields[0]; @@ -940,6 +924,7 @@ var BasicModel = AbstractModel.extend({ fields: list.fields, fieldsInfo: list.fieldsInfo, modelName: list.model, + parentID: list.id, viewType: list.viewType, }; if (command[0] === 1) { @@ -1659,6 +1644,34 @@ var BasicModel = AbstractModel.extend({ } return changes; }, + /** + * Generates an object mapping field names to their current value in a given + * record. If the record is inside a one2many, the returned object contains + * an additional key (the corresponding many2one field name) mapping to the + * current value of the parent record. + * + * @param {Object} record + * @returns {Object} the data + */ + _generateOnChangeData: function (record) { + var commands = this._generateX2ManyCommands(record, false); + var data = _.extend(this.get(record.id, {raw: true}).data, commands); + + // one2many records have a parentID + if (record.parentID) { + var parent = this.localData[record.parentID]; + // parent is the list element containing all the records in the + // one2many and parent.parentID is the ID of the main record + // if there is a relation field, this means that record is an elem + // in a one2many. The relation field is the corresponding many2one + if (parent.parentID && parent.relationField) { + var parentRecord = this.localData[parent.parentID]; + data[parent.relationField] = this._generateOnChangeData(parentRecord); + } + } + + return data; + }, /** * Read all x2many fields and generate the commands for the server to create * or write them... @@ -1992,7 +2005,6 @@ var BasicModel = AbstractModel.extend({ fieldsInfo: params.fieldsInfo, context: params.context, parentID: params.parentID, - relationField: params.relationField, res_ids: params.res_ids, viewType: params.viewType, }); -- cgit v1.2.3 From e7dfa4af775a93b291ccd570c05970f2ac2b9187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 6 Apr 2017 09:59:05 +0200 Subject: [FIX] web: add support for editable=top/bottom in o2m Before this commit, we lost the functionality of editing records top or bottom. However, it as almost ready, but we needed to change the way commands are generated, to respect the actual order in the UI. Also, the mock server needed to be updated, to make sure he also save the new ids in the correct order. --- .../web/static/src/js/views/basic/basic_model.js | 37 ++++++++++++---------- 1 file changed, 21 insertions(+), 16 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 1f8d5ce94c6..c95325dbb87 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1058,7 +1058,7 @@ var BasicModel = AbstractModel.extend({ return this.notifyChanges(id, command.data); })); } else { - defs.push(this.addDefaultRecord(list.id)); + defs.push(this.addDefaultRecord(list.id, {position: command.position})); } break; case 'UPDATE': @@ -1715,20 +1715,29 @@ var BasicModel = AbstractModel.extend({ var removedIds = _.difference(list.res_ids, relIds); var addedIds = _.difference(relIds, list.res_ids); var keptIds = _.intersection(list.res_ids, relIds); + // the didChange variable keeps track of the fact that at // least one id was updated var didChange = false; - var command, i, relRecord; - for (i = 0; i < keptIds.length; i++) { - relRecord = _.findWhere(relData, {res_id: keptIds[i]}); - if (!_.isEmpty(relRecord._changes)) { - var changes = this._generateChanges(relRecord); - command = x2ManyCommands.update(relRecord.res_id, changes); - didChange = true; - } else { - command = x2ManyCommands.link_to(keptIds[i]); + var command, relRecord; + for (var i = 0; i < relIds.length; i++) { + if (_.contains(keptIds, relIds[i])) { + // this is an id that already existed + relRecord = _.findWhere(relData, {res_id: relIds[i]}); + if (!_.isEmpty(relRecord._changes)) { + var changes = this._generateChanges(relRecord); + command = x2ManyCommands.update(relRecord.res_id, changes); + didChange = true; + } else { + command = x2ManyCommands.link_to(relIds[i]); + } + commands[fieldName].push(command); + } else if (_.contains(addedIds, relIds[i])) { + // this is a new id + relRecord = _.findWhere(relData, {res_id: relIds[i]}); + relRecord = this.get(relRecord.id, {raw: true}); + commands[fieldName].push(x2ManyCommands.create(_.omit(relRecord.data, 'id'))); } - commands[fieldName].push(command); } if (changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) { // in this situation, we have no changed ids, no added @@ -1736,11 +1745,7 @@ var BasicModel = AbstractModel.extend({ // last changes commands[fieldName] = []; } - for (i = 0; i < addedIds.length; i++) { - relRecord = _.findWhere(relData, {res_id: addedIds[i]}); - relRecord = this.get(relRecord.id, {raw: true}); - commands[fieldName].push(x2ManyCommands.create(_.omit(relRecord.data, 'id'))); - } + // add delete commands for (i = 0; i < removedIds.length; i++) { commands[fieldName].push(x2ManyCommands.delete(removedIds[i])); } -- cgit v1.2.3 From 318ff5f00d288f2e5db49548cd5f86646d22edc4 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 6 Apr 2017 13:50:05 +0200 Subject: [FIX] web: pass context of changed field to onchange RPC When a field with an onchange is changed, if there is a context defined on the field in the arch, this context must be added to the context sent with the onchange RPC. --- addons/web/static/src/js/views/basic/basic_model.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 c95325dbb87..47ce8ad3780 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -861,12 +861,15 @@ var BasicModel = AbstractModel.extend({ var self = this; var onchange_spec = this._buildOnchangeSpecs(record); var idList = record.data.id ? [record.data.id] : []; - var context = this._getContext(record); - var currentData = this._generateOnChangeData(record); - + var options = {}; if (fields.length === 1) { fields = fields[0]; + // if only one field changed, add its context to the RPC context + options.fieldName = fields; } + var context = this._getContext(record, options); + var currentData = this._generateOnChangeData(record); + return self._rpc({ model: record.model, method: 'onchange', -- cgit v1.2.3 From 60ae646c664bf3c78ce0ba22583c5af61d06b299 Mon Sep 17 00:00:00 2001 From: fwi-odoo Date: Thu, 6 Apr 2017 14:49:04 +0200 Subject: [FIX] web: properly initialize non empty x2many in default_get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the ´default_get´ returns default values for a x2many field, it needs to be set as changed fields in this x2many field. Previously, it was wrongly assumed that these changes were non existant. --- addons/web/static/src/js/views/basic/basic_model.js | 1 + 1 file changed, 1 insertion(+) (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 47ce8ad3780..82351645c26 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2065,6 +2065,7 @@ var BasicModel = AbstractModel.extend({ fields: fields, viewType: viewType, }); + x2manyList._changes = x2manyList._changes || []; x2manyList._changes.push(r.id); r._changes = value[2]; -- cgit v1.2.3 From 85790a298088e95f760ff9183a2ea9d81258a59e Mon Sep 17 00:00:00 2001 From: fwi-odoo Date: Thu, 6 Apr 2017 15:42:09 +0200 Subject: [FIX] web: don't try to create datapoint for relational field not in view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some case ´default_get´ returns values for relational fields that are not in the view so we cannot create a datapoint for these field. Furthermore, we cannot discard these fields in order to save their values correctly in the newly created record. --- addons/web/static/src/js/views/basic/basic_model.js | 3 ++- 1 file changed, 2 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 82351645c26..d904f8f1ec7 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2075,7 +2075,8 @@ var BasicModel = AbstractModel.extend({ } for (var name in r._changes) { - if (r.fields[name].type === 'many2one') { + var isFieldInView = name in r.fields; + if (isFieldInView && r.fields[name].type === 'many2one') { var rec = self._makeDataPoint({ context: r.context, modelName: r.fields[name].relation, -- cgit v1.2.3 From 64a9398fb385896cafe7725d7361e6131561f606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 6 Apr 2017 16:51:26 +0200 Subject: [FIX] web: fix corrupt datapoint in basic model When saving a new record, with the option reload=false, a datapoint in the basic model could be left in a wrong state: the x2many fields were deleted, with all the changes, but the values in the data key were still null, which means that the x2many field has no valid corresponding datapoint, as far as the basicmodel can see. So, with this commit, we make sure to push the changes into the data, so the datapoint is in a proper clean state. --- addons/web/static/src/js/views/basic/basic_model.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 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 d904f8f1ec7..1d511b1eb34 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -640,10 +640,15 @@ var BasicModel = AbstractModel.extend({ record.res_ids.push(id); record.count++; } - // erase changes as they have been applied - record._changes = {}; - - return shouldReload ? self._fetchRecord(record) : false; + if (shouldReload) { + // erase changes as they have been applied + record._changes = {}; + return self._fetchRecord(record); + } else { + _.extend(record.data, record._changes); + record._changes = {}; + return false; + } }); } else { return $.when(record_id); -- cgit v1.2.3 From 0cb500594627c4cbe94d4e8c86d69f74ca031f35 Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Wed, 5 Apr 2017 13:23:31 +0200 Subject: [FIX] web, *: basic model: makeRecord with X2manys `makeRecord` is now asynchronous as it will load data for relational fields (X2manys and many2ones if `display_name` is not fetched). This change has implied a refactoring in all `makeRecord` occurences to adapt with the returned deferred. --- .../web/static/src/js/views/basic/basic_model.js | 96 +++++++++++++++++++--- 1 file changed, 83 insertions(+), 13 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 1d511b1eb34..7badafa43c5 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -451,39 +451,92 @@ var BasicModel = AbstractModel.extend({ */ makeRecord: function (model, fields, fieldInfo) { var self = this; + var defs = []; var record_fields = {}; _.each(fields, function (field) { record_fields[field.name] = _.pick(field, 'type', 'relation', 'domain'); }); - var viewInfo = {}; - viewInfo.default = fieldInfo || _.mapObject(fields, function () { - return {}; + fieldInfo = fieldInfo || {}; + var fieldsInfo = {}; + fieldsInfo.default = {}; + _.each(fields, function (field) { + fieldsInfo.default[field.name] = fieldInfo[field.name] || {}; }); var record = this._makeDataPoint({ modelName: model, fields: record_fields, - fieldsInfo: viewInfo, + fieldsInfo: fieldsInfo, viewType: 'default', }); _.each(fields, function (field) { - if ('value' in field) { - if (field.type === 'many2one') { - var id = field.value[0]; - var display_name = field.value[1]; - var relatedRecord = self._makeDataPoint({ + var dataPoint; + if (field.type === 'many2one') { + if (field.value) { + var id = _.isArray(field.value) ? field.value[0] : field.value; + var display_name = _.isArray(field.value) ? field.value[1] : undefined; + dataPoint = self._makeDataPoint({ modelName: field.relation, data: { id: id, display_name: display_name, - } + }, }); - record.data[field.name] = relatedRecord.id; + record.data[field.name] = dataPoint.id; + if (display_name === undefined) { + defs.push(self._fetchNameGet(dataPoint)); + } + } + } else if (field.type === 'one2many' || field.type === 'many2many') { + var relatedFieldsInfo = {}; + relatedFieldsInfo.default = {}; + _.each(field.fields, function (field) { + relatedFieldsInfo.default[field.name] = {}; + }); + var dpParams = { + fieldsInfo: relatedFieldsInfo, + modelName: field.relation, + static: true, + type: 'list', + viewType: 'default', + }; + var needLoad = false; + // As value, you could either pass: + // - a list of ids related to the record + // - a list of object + // We only need to load the datapoint in the first case. + if (field.value && field.value.length) { + if (_.isObject(field.value[0])) { + dpParams.res_ids = _.pluck(field.value, 'id'); + dataPoint = self._makeDataPoint(dpParams); + _.each(field.value, function (data) { + var recordDP = self._makeDataPoint({ + data: data, + modelName: field.relation, + type: 'record', + }); + dataPoint.data.push(recordDP.id); + }); + } else { + dpParams.res_ids = field.value; + dataPoint = self._makeDataPoint(dpParams); + needLoad = true; + } } else { - record.data[field.name] = field.value; + dpParams.res_ids = []; + dataPoint = self._makeDataPoint(dpParams); } + + if (needLoad) { + defs.push(self._load(dataPoint)); + } + record.data[field.name] = dataPoint.id; + } else if (field.value) { + record.data[field.name] = field.value; } }); - return record.id; + return $.when.apply($, defs).then(function () { + return record.id; + }); }, /** * This is an extremely important method. All changes in any field go @@ -832,6 +885,7 @@ var BasicModel = AbstractModel.extend({ 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, method: 'name_get', @@ -1154,6 +1208,22 @@ var BasicModel = AbstractModel.extend({ }); }); }, + /** + * Fetch name_get for a record datapoint. + * + * @param {Object} dataPoint + * @returns {Deferred} + */ + _fetchNameGet: function (dataPoint) { + return this._rpc({ + model: dataPoint.model, + method: 'name_get', + args: [dataPoint.res_id], + context: dataPoint.getContext(), + }).then(function (result) { + dataPoint.data.display_name = result[0][1]; + }); + }, _fetchNameGets: function (list, fieldName) { var self = this; var model; -- cgit v1.2.3 From f0ee1e2be579904495b2a78d56c314960392e94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 7 Apr 2017 10:43:30 +0200 Subject: [FIX] web: add session user_context in the evalcontext When evaluating a domain, we need to add the user context, so domains can use a key such as uid. --- addons/web/static/src/js/views/basic/basic_model.js | 6 +++++- 1 file changed, 5 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 7badafa43c5..e5e7da7867b 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1915,6 +1915,10 @@ var BasicModel = AbstractModel.extend({ * Returns the evaluation context that should be used when evaluating the * context/domain associated to a given element from the localData. * + * It is actually quite subtle. We need to add some magic keys: active_id + * and active_ids. Also, the session user context is added in the mix to be + * sure. This allows some domains to use the uid key for example + * * @param {Object} element - an element from the localData * @returns {Object} */ @@ -1932,7 +1936,7 @@ var BasicModel = AbstractModel.extend({ } _.extend(evalContext, {parent: parent.data}); } - return evalContext; + return _.extend({}, session.user_context, evalContext); }, /** * Returns the list of field names of the given element according to its -- cgit v1.2.3