488 lines
19 KiB
JavaScript
488 lines
19 KiB
JavaScript
flectra.define('web.DataManager', function (require) {
|
|
"use strict";
|
|
|
|
var config = require('web.config');
|
|
var core = require('web.core');
|
|
var fieldRegistry = require('web.field_registry');
|
|
var pyeval = require('web.pyeval');
|
|
var rpc = require('web.rpc');
|
|
var utils = require('web.utils');
|
|
|
|
return core.Class.extend({
|
|
init: function () {
|
|
this._init_cache();
|
|
core.bus.on('clear_cache', this, this.invalidate.bind(this));
|
|
},
|
|
|
|
_init_cache: function () {
|
|
this._cache = {
|
|
actions: {},
|
|
fields_views: {},
|
|
filters: {},
|
|
views: {},
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Invalidates the whole cache
|
|
* Suggestion: could be refined to invalidate some part of the cache
|
|
*/
|
|
invalidate: function () {
|
|
this._init_cache();
|
|
},
|
|
|
|
/**
|
|
* Loads an action from its id or xmlid.
|
|
*
|
|
* @param {int|string} [action_id] the action id or xmlid
|
|
* @param {Object} [additional_context] used to load the action
|
|
* @return {Deferred} resolved with the action whose id or xmlid is action_id
|
|
*/
|
|
load_action: function (action_id, additional_context) {
|
|
var self = this;
|
|
var key = this._gen_key(action_id, additional_context || {});
|
|
|
|
if (!this._cache.actions[key]) {
|
|
this._cache.actions[key] = rpc.query({
|
|
route: "/web/action/load",
|
|
params: {
|
|
action_id: action_id,
|
|
additional_context : additional_context,
|
|
},
|
|
}).then(function (action) {
|
|
self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key];
|
|
return action;
|
|
}, this._invalidate.bind(this, this._cache.actions, key));
|
|
}
|
|
|
|
return this._cache.actions[key].then(function (action) {
|
|
return $.extend(true, {}, action);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Loads various information concerning views: fields_view for each view,
|
|
* the fields of the corresponding model, and optionally the filters.
|
|
*
|
|
* @param {Object} params
|
|
* @param {String} params.model
|
|
* @param {Object} params.context
|
|
* @param {Array} params.views_descr array of [view_id, view_type]
|
|
* @param {Object} [options] dictionary of various options:
|
|
* - options.load_filters: whether or not to load the filters,
|
|
* - options.action_id: the action_id (required to load filters),
|
|
* - options.toolbar: whether or not a toolbar will be displayed,
|
|
* @return {Deferred} resolved with the requested views information
|
|
*/
|
|
load_views: function (params, options) {
|
|
var self = this;
|
|
|
|
var model = params.model;
|
|
var context = params.context;
|
|
var views_descr = params.views_descr;
|
|
var key = this._gen_key(model, views_descr, options || {}, context);
|
|
|
|
if (!this._cache.views[key]) {
|
|
// Don't load filters if already in cache
|
|
var filters_key;
|
|
if (options.load_filters) {
|
|
filters_key = this._gen_key(model, options.action_id);
|
|
options.load_filters = !this._cache.filters[filters_key];
|
|
}
|
|
|
|
this._cache.views[key] = rpc.query({
|
|
args: [],
|
|
kwargs: {
|
|
views: views_descr,
|
|
options: options,
|
|
context: context.eval(),
|
|
},
|
|
model: model,
|
|
method: 'load_views',
|
|
}).then(function (result) {
|
|
// Postprocess fields_views and insert them into the fields_views cache
|
|
result.fields_views = _.mapObject(result.fields_views, self._postprocess_fvg.bind(self));
|
|
self.processViews(result.fields_views, result.fields);
|
|
_.each(views_descr, function (view_descr) {
|
|
var toolbar = options.toolbar && view_descr[1] !== 'search';
|
|
var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context);
|
|
self._cache.fields_views[fv_key] = $.when(result.fields_views[view_descr[1]]);
|
|
});
|
|
|
|
// Insert filters, if any, into the filters cache
|
|
if (result.filters) {
|
|
self._cache.filters[filters_key] = $.when(result.filters);
|
|
}
|
|
|
|
return result.fields_views;
|
|
}, this._invalidate.bind(this, this._cache.views, key));
|
|
}
|
|
|
|
return this._cache.views[key];
|
|
},
|
|
|
|
/**
|
|
* Loads the filters of a given model and optional action id.
|
|
*
|
|
* @param {Object} [dataset] the dataset for which the filters are loaded
|
|
* @param {int} [action_id] the id of the action (optional)
|
|
* @return {Deferred} resolved with the requested filters
|
|
*/
|
|
load_filters: function (dataset, action_id) {
|
|
var key = this._gen_key(dataset.model, action_id);
|
|
if (!this._cache.filters[key]) {
|
|
this._cache.filters[key] = rpc.query({
|
|
args: [dataset.model, action_id],
|
|
kwargs: {
|
|
context: dataset.get_context(),
|
|
},
|
|
model: 'ir.filters',
|
|
method: 'get_filters',
|
|
}).fail(this._invalidate.bind(this, this._cache.filters, key));
|
|
}
|
|
return this._cache.filters[key];
|
|
},
|
|
|
|
/**
|
|
* Calls 'create_or_replace' on 'ir_filters'.
|
|
*
|
|
* @param {Object} [filter] the filter description
|
|
* @return {Deferred} resolved with the id of the created or replaced filter
|
|
*/
|
|
create_filter: function (filter) {
|
|
var self = this;
|
|
return rpc.query({
|
|
args: [filter],
|
|
model: 'ir.filters',
|
|
method: 'create_or_replace',
|
|
})
|
|
.then(function (filter_id) {
|
|
var key = [
|
|
filter.model_id,
|
|
filter.action_id || false,
|
|
].join(',');
|
|
self._invalidate(self._cache.filters, key);
|
|
return filter_id;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Calls 'unlink' on 'ir_filters'.
|
|
*
|
|
* @param {Object} [filter] the description of the filter to remove
|
|
* @return {Deferred}
|
|
*/
|
|
delete_filter: function (filter) {
|
|
var self = this;
|
|
return rpc.query({
|
|
args: [filter.id],
|
|
model: 'ir.filters',
|
|
method: 'unlink',
|
|
})
|
|
.then(function () {
|
|
self._cache.filters = {}; // invalidate cache
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Processes fields and fields_views. For each field, writes its name inside
|
|
* the field description to make it self-contained. For each fields_view,
|
|
* completes its fields with the missing ones.
|
|
*
|
|
* @param {Object} fieldsViews object of fields_views (keys are view types)
|
|
* @param {Object} fields all the fields of the model
|
|
*/
|
|
processViews: function (fieldsViews, fields) {
|
|
var fieldName, fieldsView, viewType;
|
|
// write the field name inside the description for all fields
|
|
for (fieldName in fields) {
|
|
fields[fieldName].name = fieldName;
|
|
}
|
|
for (viewType in fieldsViews) {
|
|
fieldsView = fieldsViews[viewType];
|
|
// write the field name inside the description for fields in view
|
|
for (fieldName in fieldsView.fields) {
|
|
fieldsView.fields[fieldName].name = fieldName;
|
|
}
|
|
// complete fields (in view) with missing ones
|
|
_.defaults(fieldsView.fields, fields);
|
|
// process the fields_view
|
|
_.extend(fieldsView, this._processFieldsView({
|
|
type: viewType,
|
|
arch: fieldsView.arch,
|
|
fields: fieldsView.fields,
|
|
}));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Private function that postprocesses fields_view (mainly parses the arch attribute)
|
|
*/
|
|
_postprocess_fvg: function (fields_view) {
|
|
var self = this;
|
|
|
|
// Parse arch
|
|
var doc = $.parseXML(fields_view.arch).documentElement;
|
|
fields_view.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
|
|
|
|
// Process inner views (x2manys)
|
|
_.each(fields_view.fields, function(field) {
|
|
_.each(field.views || {}, function(view) {
|
|
self._postprocess_fvg(view);
|
|
});
|
|
});
|
|
|
|
return fields_view;
|
|
},
|
|
|
|
/**
|
|
* Private function that generates a cache key from its arguments
|
|
*/
|
|
_gen_key: function () {
|
|
return _.map(Array.prototype.slice.call(arguments), function (arg) {
|
|
if (!arg) {
|
|
return false;
|
|
}
|
|
return _.isObject(arg) ? JSON.stringify(arg) : arg;
|
|
}).join(',');
|
|
},
|
|
|
|
/**
|
|
* Private function that invalidates a cache entry
|
|
*/
|
|
_invalidate: function (cache, key) {
|
|
delete cache[key];
|
|
},
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Process a field node, in particular, put a flag on the field to give
|
|
* special directives to the BasicModel.
|
|
*
|
|
* @param {string} viewType
|
|
* @param {Object} field - the field properties
|
|
* @param {Object} attrs - the field attributes (from the xml)
|
|
* @returns {Object} attrs
|
|
*/
|
|
_processField: function (viewType, field, attrs) {
|
|
var self = this;
|
|
attrs.Widget = this._getFieldWidgetClass(viewType, field, attrs);
|
|
|
|
if (!_.isObject(attrs.options)) { // parent arch could have already been processed (TODO this should not happen)
|
|
attrs.options = attrs.options ? pyeval.py_eval(attrs.options) : {};
|
|
}
|
|
|
|
if (attrs.on_change && !field.onChange) {
|
|
field.onChange = "1";
|
|
}
|
|
|
|
// the relational data of invisible relational fields should not be
|
|
// fetched (e.g. name_gets of invisible many2ones), at least those that
|
|
// are always invisible.
|
|
// the invisible attribute of a field is supposed to be static ("1" in
|
|
// general), but not totally as it may use keys of the context
|
|
// ("context.get('some_key')"). It is evaluated server-side, and the
|
|
// result is put inside the modifiers as a value of the '(column_)invisible'
|
|
// key, and the raw value is left in the invisible attribute (it is used
|
|
// in debug mode for informational purposes).
|
|
// this should change, for instance the server might set the evaluated
|
|
// value in invisible, which could then be seen as static by the client,
|
|
// and add another key in debug mode containing the raw value.
|
|
// for now, we look inside the modifiers and consider the value only if
|
|
// it is static (=== true),
|
|
if (attrs.modifiers.invisible === true || attrs.modifiers.column_invisible === true) {
|
|
attrs.__no_fetch = true;
|
|
}
|
|
|
|
if (!_.isEmpty(field.views)) {
|
|
// process the inner fields_view as well to find the fields they use.
|
|
// register those fields' description directly on the view.
|
|
// for those inner views, the list of all fields isn't necessary, so
|
|
// basically the field_names will be the keys of the fields obj.
|
|
// don't use _ to iterate on fields in case there is a 'length' field,
|
|
// as _ doesn't behave correctly when there is a length key in the object
|
|
attrs.views = {};
|
|
_.each(field.views, function (innerFieldsView, viewType) {
|
|
viewType = viewType === 'tree' ? 'list' : viewType;
|
|
innerFieldsView.type = viewType;
|
|
attrs.views[viewType] = self._processFieldsView(_.extend({}, innerFieldsView));
|
|
});
|
|
delete field.views;
|
|
}
|
|
|
|
if (field.type === 'one2many' || field.type === 'many2many') {
|
|
if (attrs.Widget.prototype.useSubview) {
|
|
if (!attrs.views) {
|
|
attrs.views = {};
|
|
}
|
|
var mode = attrs.mode;
|
|
if (!mode) {
|
|
if (attrs.views.tree && attrs.views.kanban) {
|
|
mode = 'tree';
|
|
} else if (!attrs.views.tree && attrs.views.kanban) {
|
|
mode = 'kanban';
|
|
} else {
|
|
mode = 'tree,kanban';
|
|
}
|
|
}
|
|
if (mode.indexOf(',') !== -1) {
|
|
mode = config.device.size_class !== config.device.SIZES.XS ? 'tree' : 'kanban';
|
|
}
|
|
if (mode === 'tree') {
|
|
mode = 'list';
|
|
if (!attrs.views.list && attrs.views.tree) {
|
|
attrs.views.list = attrs.views.tree;
|
|
}
|
|
}
|
|
attrs.mode = mode;
|
|
if (mode in attrs.views) {
|
|
var view = attrs.views[mode];
|
|
var defaultOrder = view.arch.attrs.default_order;
|
|
if (defaultOrder) {
|
|
// process the default_order, which is like 'name,id desc'
|
|
// but we need it like [{name: 'name', asc: true}, {name: 'id', asc: false}]
|
|
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
|
|
order = order.trim().split(' ');
|
|
return {name: order[0], asc: order[1] !== 'desc'};
|
|
});
|
|
} else {
|
|
// if there is a field with widget `handle`, the x2many
|
|
// needs to be ordered by this field to correctly display
|
|
// the records
|
|
var handleField = _.find(view.arch.children, function (child) {
|
|
return child.attrs && child.attrs.widget === 'handle';
|
|
});
|
|
if (handleField) {
|
|
attrs.orderedBy = [{name: handleField.attrs.name, asc: true}];
|
|
}
|
|
}
|
|
|
|
attrs.columnInvisibleFields = {};
|
|
_.each(view.arch.children, function (child) {
|
|
if (child.attrs && child.attrs.modifiers) {
|
|
attrs.columnInvisibleFields[child.attrs.name] =
|
|
child.attrs.modifiers.column_invisible || false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (attrs.Widget.prototype.fieldsToFetch) {
|
|
attrs.viewType = 'default';
|
|
attrs.relatedFields = _.extend({}, attrs.Widget.prototype.fieldsToFetch);
|
|
attrs.fieldsInfo = {
|
|
default: _.mapObject(attrs.Widget.prototype.fieldsToFetch, function () {
|
|
return {};
|
|
}),
|
|
};
|
|
if (attrs.options.color_field) {
|
|
// used by m2m tags
|
|
attrs.relatedFields[attrs.options.color_field] = { type: 'integer' };
|
|
attrs.fieldsInfo.default[attrs.options.color_field] = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attrs.Widget.prototype.fieldDependencies) {
|
|
attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies;
|
|
}
|
|
|
|
return attrs;
|
|
},
|
|
/**
|
|
* Visit all nodes in the arch field and process each fields
|
|
*
|
|
* @param {string} viewType
|
|
* @param {Object} arch
|
|
* @param {Object} fields
|
|
* @returns {Object} fieldsInfo
|
|
*/
|
|
_processFields: function (viewType, arch, fields) {
|
|
var self = this;
|
|
var fieldsInfo = Object.create(null);
|
|
utils.traverse(arch, function (node) {
|
|
if (typeof node === 'string') {
|
|
return false;
|
|
}
|
|
if (!_.isObject(node.attrs.modifiers)) {
|
|
node.attrs.modifiers = node.attrs.modifiers ? JSON.parse(node.attrs.modifiers) : {};
|
|
}
|
|
if (!_.isObject(node.attrs.options) && node.tag === 'button') {
|
|
node.attrs.options = node.attrs.options ? JSON.parse(node.attrs.options) : {};
|
|
}
|
|
if (node.tag === 'field') {
|
|
fieldsInfo[node.attrs.name] = self._processField(viewType,
|
|
fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {});
|
|
|
|
if (fieldsInfo[node.attrs.name].fieldDependencies) {
|
|
var deps = fieldsInfo[node.attrs.name].fieldDependencies;
|
|
for (var dependency_name in deps) {
|
|
var dependency_dict = {name: dependency_name, type: deps[dependency_name].type};
|
|
if (!(dependency_name in fieldsInfo)) {
|
|
fieldsInfo[dependency_name] = _.extend({}, dependency_dict, {options: deps[dependency_name].options || {}});
|
|
}
|
|
if (!(dependency_name in fields)) {
|
|
fields[dependency_name] = dependency_dict;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return node.tag !== 'arch';
|
|
});
|
|
return fieldsInfo;
|
|
},
|
|
/**
|
|
* Visit all nodes in the arch field and process each fields and inner views
|
|
*
|
|
* @param {Object} viewInfo
|
|
* @param {Object} viewInfo.arch
|
|
* @param {Object} viewInfo.fields
|
|
* @returns {Object} viewInfo
|
|
*/
|
|
_processFieldsView: function (viewInfo) {
|
|
var viewFields = this._processFields(viewInfo.type, viewInfo.arch, viewInfo.fields);
|
|
viewInfo.fieldsInfo = {};
|
|
viewInfo.fieldsInfo[viewInfo.type] = viewFields;
|
|
utils.deepFreeze(viewInfo.fields);
|
|
return viewInfo;
|
|
},
|
|
/**
|
|
* Returns the AbstractField specialization that should be used for the
|
|
* given field informations. If there is no mentioned specific widget to
|
|
* use, determine one according the field type.
|
|
*
|
|
* @param {string} viewType
|
|
* @param {Object} field
|
|
* @param {Object} attrs
|
|
* @returns {function|null} AbstractField specialization Class
|
|
*/
|
|
_getFieldWidgetClass: function (viewType, field, attrs) {
|
|
var Widget;
|
|
if (attrs.widget) {
|
|
Widget = fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
|
|
if (!Widget) {
|
|
console.warn("Missing widget: ", attrs.widget, " for field", attrs.name, "of type", field.type);
|
|
}
|
|
} else if (viewType === 'kanban' && field.type === 'many2many') {
|
|
// we want to display the widget many2manytags in kanban even if it
|
|
// is not specified in the view
|
|
Widget = fieldRegistry.get('kanban.many2many_tags');
|
|
}
|
|
return Widget || fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
|
|
},
|
|
});
|
|
|
|
});
|
|
|
|
flectra.define('web.data_manager', function (require) {
|
|
"use strict";
|
|
|
|
var DataManager = require('web.DataManager');
|
|
|
|
var data_manager = new DataManager();
|
|
|
|
return data_manager;
|
|
|
|
});
|