From cb339842d4d8b19f3870127c00a6868af2e95004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 5 Jan 2018 13:09:43 +0100 Subject: [REF] web,*: move fvg processing from DataManager to Views *: mail, web_diagram --- addons/web/static/src/js/views/basic/basic_view.js | 246 ++++++++++++++++++++- 1 file changed, 243 insertions(+), 3 deletions(-) (limited to 'addons/web/static/src/js/views/basic') diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 8a91e2b90c7..f428a83cd41 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -13,6 +13,10 @@ odoo.define('web.BasicView', function (require) { var AbstractView = require('web.AbstractView'); var BasicController = require('web.BasicController'); var BasicModel = require('web.BasicModel'); +var config = require('web.config'); +var fieldRegistry = require('web.field_registry'); +var pyeval = require('web.pyeval'); +var utils = require('web.utils'); var BasicView = AbstractView.extend({ config: _.extend({}, AbstractView.prototype.config, { @@ -35,6 +39,9 @@ var BasicView = AbstractView.extend({ init: function (viewInfo, params) { this._super.apply(this, arguments); + this.fieldsInfo = {}; + this.fieldsInfo[this.viewType] = this.fieldsView.fieldsInfo[this.viewType]; + this.rendererParams.viewType = this.viewType; this.controllerParams.confirmOnDelete = true; @@ -42,10 +49,10 @@ var BasicView = AbstractView.extend({ this.controllerParams.hasButtons = 'action_buttons' in params ? params.action_buttons : true; - this.loadParams.fieldsInfo = viewInfo.fieldsInfo; - this.loadParams.fields = viewInfo.fields; + this.loadParams.fieldsInfo = this.fieldsInfo; + this.loadParams.fields = this.fields; this.loadParams.context = params.context || {}; - this.loadParams.limit = parseInt(viewInfo.arch.attrs.limit, 10) || params.limit; + this.loadParams.limit = parseInt(this.arch.attrs.limit, 10) || params.limit; this.loadParams.viewType = this.viewType; this.loadParams.parentID = params.parentID; this.recordID = params.recordID; @@ -57,6 +64,31 @@ var BasicView = AbstractView.extend({ // Private //-------------------------------------------------------------------------- + /** + * Returns the AbstractField specialization that should be used for the + * given field informations. If there is no mentioned specific widget to + * use, determines one according the field type. + * + * @private + * @param {string} viewType + * @param {Object} field + * @param {Object} attrs + * @returns {function|null} AbstractField specialization Class + */ + _getFieldWidgetClass: function (viewType, field, attrs) { + var FieldWidget; + if (attrs.widget) { + FieldWidget = fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]); + if (!FieldWidget) { + 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 + FieldWidget = fieldRegistry.get('kanban.many2many_tags'); + } + return FieldWidget || fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]); + }, /** * In some cases, we already have a preloaded record * @@ -67,6 +99,15 @@ var BasicView = AbstractView.extend({ _loadData: function () { if (this.recordID) { var self = this; + + // Add the fieldsInfo of the current view to the given recordID, + // as it will be shared between two views, and it must be able to + // handle changes on fields that are only on this view. + this.model.addFieldsInfo(this.recordID, { + fields: this.fields, + fieldsInfo: this.fieldsInfo, + }); + var record = this.model.get(this.recordID); var viewType = this.viewType; var viewFields = Object.keys(record.fieldsInfo[viewType]); @@ -135,6 +176,205 @@ var BasicView = AbstractView.extend({ } return this._super.apply(this, arguments); }, + /** + * Processes a field node, in particular, put a flag on the field to give + * special directives to the BasicModel. + * + * @private + * @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; + attrs.views[viewType] = self._processFieldsView(innerFieldsView, viewType); + }); + } + + 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; + }, + /** + * Visits all nodes in the arch and processes each fields. + * + * @private + * @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; + }, + /** + * Overrides to process the fields, and generate fieldsInfo which contains + * the description of the fields in view, with their attrs in the arch. + * + * @override + * @private + * @param {Object} fieldsView + * @param {string} fieldsView.arch + * @param {Object} fieldsView.fields + * @param {string} [viewType] by default, this.viewType + * @returns {Object} the processed fieldsView with extra key 'fieldsInfo' + */ + _processFieldsView: function (fieldsView, viewType) { + var fv = this._super.apply(this, arguments); + + viewType = viewType || this.viewType; + var viewFields = this._processFields(viewType, fv.arch, fv.viewFields); + fv.fieldsInfo = {}; + fv.fieldsInfo[viewType] = viewFields; + fv.type = viewType; + + return fv; + }, }); return BasicView; -- cgit v1.2.3