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: [PATCH] [REF] web,*: move fvg processing from DataManager to Views *: mail, web_diagram --- addons/mail/static/src/js/basic_view.js | 4 +- .../js/chrome/action_manager_act_window.js | 6 +- .../web/static/src/js/chrome/search_view.js | 24 +- .../static/src/js/services/data_manager.js | 285 +----------------- .../web/static/src/js/views/abstract_view.js | 43 ++- .../static/src/js/views/basic/basic_view.js | 246 ++++++++++++++- .../src/js/views/calendar/calendar_view.js | 4 +- .../web/static/src/js/views/form/form_view.js | 3 +- .../static/src/js/views/graph/graph_view.js | 14 +- .../static/src/js/views/kanban/kanban_view.js | 24 +- .../web/static/src/js/views/list/list_view.js | 9 +- .../static/src/js/views/pivot/pivot_view.js | 17 +- .../web/static/src/js/views/view_dialogs.js | 3 - .../web/static/tests/helpers/mock_server.js | 88 +++--- addons/web/static/tests/helpers/test_utils.js | 23 +- .../web/static/tests/views/calendar_tests.js | 36 +-- .../web_diagram/static/src/js/diagram_view.js | 2 +- 17 files changed, 431 insertions(+), 400 deletions(-) diff --git a/addons/mail/static/src/js/basic_view.js b/addons/mail/static/src/js/basic_view.js index a019709dad4..a7ce716b460 100644 --- a/addons/mail/static/src/js/basic_view.js +++ b/addons/mail/static/src/js/basic_view.js @@ -6,10 +6,10 @@ var BasicView = require('web.BasicView'); var mailWidgets = ['mail_followers', 'mail_thread', 'mail_activity', 'kanban_activity']; BasicView.include({ - init: function (viewInfo) { + init: function () { this._super.apply(this, arguments); this.mailFields = {}; - var fieldsInfo = viewInfo.fieldsInfo[this.viewType]; + var fieldsInfo = this.fieldsInfo[this.viewType]; for (var fieldName in fieldsInfo) { var fieldInfo = fieldsInfo[fieldName]; if (_.contains(mailWidgets, fieldInfo.widget)) { diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 1410063dcca..56c6e45a08d 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -347,11 +347,13 @@ ActionManager.include({ _.each(action.views, function (view) { var viewType = view[1]; var fieldsView = fieldsViews[viewType]; - var View = view_registry.get(fieldsView.arch.attrs.js_class || viewType); + var parsedXML = new DOMParser().parseFromString(fieldsView.arch, "text/xml"); + var key = parsedXML.documentElement.getAttribute('js_class'); + var View = view_registry.get(key || viewType); if (View) { views.push({ accessKey: View.prototype.accessKey, - fieldsView: fieldsViews[viewType], + fieldsView: fieldsView, icon: View.prototype.icon, isMobileFriendly: View.prototype.mobile_friendly, multiRecord: View.prototype.multi_record, diff --git a/addons/web/static/src/js/chrome/search_view.js b/addons/web/static/src/js/chrome/search_view.js index 6fbda215814..d1b732477e6 100644 --- a/addons/web/static/src/js/chrome/search_view.js +++ b/addons/web/static/src/js/chrome/search_view.js @@ -9,6 +9,7 @@ var FilterMenu = require('web.FilterMenu'); var GroupByMenu = require('web.GroupByMenu'); var pyeval = require('web.pyeval'); var search_inputs = require('web.search_inputs'); +var utils = require('web.utils'); var Widget = require('web.Widget'); var _t = core._t; @@ -274,7 +275,8 @@ var SearchView = Widget.extend({ this._super.apply(this, arguments); this.options = options; this.dataset = dataset; - this.fields_view = fvg; + this.fields_view = this._processFieldsView(_.clone(fvg)); + this.fields = this.fields_view.fields; this.query = undefined; this.title = this.options.action && this.options.action.name; @@ -603,6 +605,26 @@ var SearchView = Widget.extend({ current_category = filter.category; }); }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Processes a fieldsView in place. In particular, parses its arch. + * + * @todo: this function is also defined in AbstractView ; this code + * duplication could be removed once the SearchView will be rewritten. + * @private + * @param {Object} fv + * @param {string} fv.arch + * @returns {Object} the processed fieldsView + */ + _processFieldsView: function (fv) { + var doc = $.parseXML(fv.arch).documentElement; + fv.arch = utils.xml_to_json(doc, true); + return fv; + }, }); _.extend(SearchView, { diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js index 72ec816efdd..bb7f25d206b 100644 --- a/addons/web/static/src/js/services/data_manager.js +++ b/addons/web/static/src/js/services/data_manager.js @@ -1,10 +1,7 @@ odoo.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'); @@ -100,13 +97,18 @@ return core.Class.extend({ 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); + // Freeze the fields dict as it will be shared between views and + // no one should edit it + utils.deepFreeze(result.fields); + + // Insert views into the fields_views cache _.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]]); + var fvg = result.fields_views[view_descr[1]]; + fvg.viewFields = fvg.fields; + fvg.fields = result.fields; + self._cache.fields_views[fv_key] = $.when(fvg); }); // Insert filters, if any, into the filters cache @@ -184,57 +186,6 @@ return core.Class.extend({ }); }, - /** - * 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 */ @@ -253,224 +204,6 @@ return core.Class.extend({ _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"]); - }, }); }); diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 6dc7be88baa..a696f05bffe 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -28,6 +28,7 @@ var Class = require('web.Class'); var AbstractModel = require('web.AbstractModel'); var AbstractRenderer = require('web.AbstractRenderer'); var AbstractController = require('web.AbstractController'); +var utils = require('web.utils'); var AbstractView = Class.extend({ // name displayed in view switchers @@ -85,18 +86,32 @@ var AbstractView = Class.extend({ * @param {boolean} [params.action.views] */ init: function (viewInfo, params) { + // in general, the fieldsView has to be processed by the View (e.g. the + // arch is a string that needs to be parsed) ; the only exception is for + // inline form views inside form views, as they are processed alongside + // the main view, but they are opened in a FormViewDialog which + // instantiates another FormView (unlike kanban or list subviews for + // which only a Renderer is instantiated) + if (typeof viewInfo.arch === 'string') { + this.fieldsView = this._processFieldsView(viewInfo); + } else { + this.fieldsView = viewInfo; + } + this.fields = this.fieldsView.viewFields; + this.arch = this.fieldsView.arch; + this.rendererParams = { - arch: viewInfo.arch, + arch: this.arch, noContentHelp: params.action && params.action.help, }; this.controllerParams = { modelName: params.modelName, activeActions: { - edit: viewInfo.arch.attrs.edit ? JSON.parse(viewInfo.arch.attrs.edit) : true, - create: viewInfo.arch.attrs.create ? JSON.parse(viewInfo.arch.attrs.create) : true, - delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true, - duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true, + edit: this.arch.attrs.edit ? JSON.parse(this.arch.attrs.edit) : true, + create: this.arch.attrs.create ? JSON.parse(this.arch.attrs.create) : true, + delete: this.arch.attrs.delete ? JSON.parse(this.arch.attrs.delete) : true, + duplicate: this.arch.attrs.duplicate ? JSON.parse(this.arch.attrs.duplicate) : true, }, groupable: this.groupable, controllerID: params.controllerID, @@ -134,7 +149,7 @@ var AbstractView = Class.extend({ // 'name,id desc' // but we need it like: // [{name: 'id', asc: false}, {name: 'name', asc: true}] - var defaultOrder = viewInfo.arch.attrs.default_order; + var defaultOrder = this.arch.attrs.default_order; if (defaultOrder) { this.loadParams.orderedBy = _.map(defaultOrder.split(','), function (order) { order = order.trim().split(' '); @@ -235,6 +250,22 @@ var AbstractView = Class.extend({ var model = this.getModel(parent); return model.load(this.loadParams); }, + /** + * Processes a fieldsView. In particular, parses its arch. + * + * @private + * @param {Object} fieldsView + * @param {string} fieldsView.arch + * @returns {Object} the processed fieldsView + */ + _processFieldsView: function (fieldsView) { + var fv = _.extend({}, fieldsView); + var doc = $.parseXML(fv.arch).documentElement; + var stripWhitespaces = doc.nodeName.toLowerCase() !== 'kanban'; + fv.arch = utils.xml_to_json(doc, stripWhitespaces); + fv.viewFields = _.defaults({}, fv.viewFields, fv.fields); + return fv; + }, }); return AbstractView; 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; diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js index 6a8dc5e106e..217f6240711 100644 --- a/addons/web/static/src/js/views/calendar/calendar_view.js +++ b/addons/web/static/src/js/views/calendar/calendar_view.js @@ -32,8 +32,8 @@ var CalendarView = AbstractView.extend({ groupable: false, init: function (viewInfo, params) { this._super.apply(this, arguments); - var arch = viewInfo.arch; - var fields = viewInfo.fields; + var arch = this.arch; + var fields = this.fields; var attrs = arch.attrs; if (!attrs.date_start) { diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js index 548dfdc9624..10fc9a12b20 100644 --- a/addons/web/static/src/js/views/form/form_view.js +++ b/addons/web/static/src/js/views/form/form_view.js @@ -90,7 +90,8 @@ var FormView = BasicView.extend({ [[null, attrs.mode === 'tree' ? 'list' : attrs.mode]]) .then(function (views) { for (var viewName in views) { - attrs.views[viewName] = views[viewName]; + // clone to make runbot green? + attrs.views[viewName] = self._processFieldsView(views[viewName], viewName); } self._setSubViewLimit(attrs); })); diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js index 44e1f74e8da..feb151b02e7 100644 --- a/addons/web/static/src/js/views/graph/graph_view.js +++ b/addons/web/static/src/js/views/graph/graph_view.js @@ -36,13 +36,13 @@ var GraphView = AbstractView.extend({ /** * @override */ - init: function (viewInfo) { + init: function () { this._super.apply(this, arguments); var measure; var groupBys = []; - viewInfo.fields = _.defaults({__count__: {string: _t("Count"), type: "integer"}}, viewInfo.fields); - viewInfo.arch.children.forEach(function (field) { + this.fields.__count__ = {string: _t("Count"), type: "integer"}; + this.arch.children.forEach(function (field) { var name = field.attrs.name; if (field.attrs.interval) { name += ':' + field.attrs.interval; @@ -55,7 +55,7 @@ var GraphView = AbstractView.extend({ }); var measures = {__count__: {string: _t("Count"), type: "integer"}}; - _.each(viewInfo.fields, function (field, name) { + _.each(this.fields, function (field, name) { if (name !== 'id' && field.store === true) { if (field.type === 'integer' || field.type === 'float' || field.type === 'monetary') { measures[name] = field; @@ -64,12 +64,12 @@ var GraphView = AbstractView.extend({ }); this.controllerParams.measures = measures; - this.rendererParams.stacked = viewInfo.arch.attrs.stacked !== "False"; + this.rendererParams.stacked = this.arch.attrs.stacked !== "False"; - this.loadParams.mode = viewInfo.arch.attrs.type || 'bar'; + this.loadParams.mode = this.arch.attrs.type || 'bar'; this.loadParams.measure = measure || '__count__'; this.loadParams.groupBys = groupBys || []; - this.loadParams.fields = viewInfo.fields; + this.loadParams.fields = this.fields; }, }); diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js index 730b761b870..87b4a366502 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -30,16 +30,14 @@ var KanbanView = BasicView.extend({ init: function (viewInfo, params) { this._super.apply(this, arguments); - var arch = viewInfo.arch; - this.loadParams.limit = this.loadParams.limit || 40; // in mobile, columns are lazy-loaded, so set 'openGroupByDefault' to // false so that they will won't be loaded by the initial load this.loadParams.openGroupByDefault = config.device.isMobile ? false : true; this.loadParams.type = 'list'; - this.loadParams.groupBy = arch.attrs.default_group_by ? [arch.attrs.default_group_by] : (params.groupBy || []); + this.loadParams.groupBy = this.arch.attrs.default_group_by ? [this.arch.attrs.default_group_by] : (params.groupBy || []); var progressBar; - utils.traverse(arch, function (n) { + utils.traverse(this.arch, function (n) { var isProgressBar = (n.tag === 'progressbar'); if (isProgressBar) { progressBar = _.clone(n.attrs); @@ -54,16 +52,16 @@ var KanbanView = BasicView.extend({ var activeActions = this.controllerParams.activeActions; activeActions = _.extend(activeActions, { - group_create: arch.attrs.group_create ? JSON.parse(arch.attrs.group_create) : true, - group_edit: arch.attrs.group_edit ? JSON.parse(arch.attrs.group_edit) : true, - group_delete: arch.attrs.group_delete ? JSON.parse(arch.attrs.group_delete) : true, + group_create: this.arch.attrs.group_create ? JSON.parse(this.arch.attrs.group_create) : true, + group_edit: this.arch.attrs.group_edit ? JSON.parse(this.arch.attrs.group_edit) : true, + group_delete: this.arch.attrs.group_delete ? JSON.parse(this.arch.attrs.group_delete) : true, }); this.rendererParams.column_options = { editable: activeActions.group_edit, deletable: activeActions.group_delete, group_creatable: activeActions.group_create && !config.device.isMobile, - quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(viewInfo), + quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(), hasProgressBar: !!progressBar, }; this.rendererParams.record_options = { @@ -72,7 +70,7 @@ var KanbanView = BasicView.extend({ read_only_mode: params.readOnlyMode, }; - this.controllerParams.on_create = arch.attrs.on_create; + this.controllerParams.on_create = this.arch.attrs.on_create; this.controllerParams.readOnlyMode = false; this.controllerParams.hasButtons = true; @@ -90,17 +88,17 @@ var KanbanView = BasicView.extend({ * @private * @param {Object} viewInfo */ - _isQuickCreateEnabled: function (viewInfo) { + _isQuickCreateEnabled: function () { var groupBy = this.loadParams.groupBy[0]; groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined; - if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], viewInfo.fields[groupBy].type)) { + if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], this.fields[groupBy].type)) { return false; } if (!this.controllerParams.activeActions.create) { return false; } - if (viewInfo.arch.attrs.quick_create !== undefined) { - return JSON.parse(viewInfo.arch.attrs.quick_create); + if (this.arch.attrs.quick_create !== undefined) { + return JSON.parse(this.arch.attrs.quick_create); } return true; }, diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js index e970f825846..3082312e770 100644 --- a/addons/web/static/src/js/views/list/list_view.js +++ b/addons/web/static/src/js/views/list/list_view.js @@ -36,19 +36,18 @@ var ListView = BasicView.extend({ init: function (viewInfo, params) { this._super.apply(this, arguments); - var arch = viewInfo.arch; - var mode = arch.attrs.editable && !params.readonly ? "edit" : "readonly"; + var mode = this.arch.attrs.editable && !params.readonly ? "edit" : "readonly"; - this.controllerParams.editable = arch.attrs.editable; + this.controllerParams.editable = this.arch.attrs.editable; this.controllerParams.hasSidebar = params.hasSidebar; this.controllerParams.toolbarActions = viewInfo.toolbar; this.controllerParams.noLeaf = !!this.loadParams.context.group_by_no_leaf; this.controllerParams.mode = mode; - this.rendererParams.arch = arch; + this.rendererParams.arch = this.arch; this.rendererParams.hasSelectors = 'hasSelectors' in params ? params.hasSelectors : true; - this.rendererParams.editable = params.readonly ? false : arch.attrs.editable; + this.rendererParams.editable = params.readonly ? false : this.arch.attrs.editable; this.loadParams.limit = this.loadParams.limit || 80; this.loadParams.type = 'list'; diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index fbafd0e867c..0e908f29b8c 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -35,10 +35,6 @@ var PivotView = AbstractView.extend({ init: function (viewInfo, params) { this._super.apply(this, arguments); - var arch = viewInfo.arch; - var fields = _.extend({ - __count: {string: _t("Count"), type: "integer"} - }, viewInfo.fields); var activeMeasures = []; var colGroupBys = []; var rowGroupBys = []; @@ -46,7 +42,8 @@ var PivotView = AbstractView.extend({ var measures = {}; var groupableFields = {}; - _.each(fields, function (field, name) { + this.fields.__count = {string: _t("Count"), type: "integer"}; + _.each(this.fields, function (field, name) { if ((name !== 'id') && (field.store === true)) { if (_.contains(['integer', 'float', 'monetary'], field.type)) { measures[name] = field; @@ -58,7 +55,7 @@ var PivotView = AbstractView.extend({ }); measures.__count = {string: _t("Count"), type: "integer"}; - arch.children.forEach(function (field) { + this.arch.children.forEach(function (field) { var name = field.attrs.name; if (field.attrs.interval) { name += ':' + field.attrs.interval; @@ -84,17 +81,17 @@ var PivotView = AbstractView.extend({ rowGroupBys.push(name); } }); - if ((!activeMeasures.length) || arch.attrs.display_quantity) { + if ((!activeMeasures.length) || this.arch.attrs.display_quantity) { activeMeasures.push('__count'); } this.loadParams.measures = activeMeasures; this.loadParams.colGroupBys = colGroupBys; this.loadParams.rowGroupBys = rowGroupBys; - this.loadParams.fields = fields; + this.loadParams.fields = this.fields; - this.controllerParams.title = params.title || arch.attrs.string || _t("Untitled"); - this.controllerParams.enableLinking = !arch.attrs.disable_linking; + this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled"); + this.controllerParams.enableLinking = !this.arch.attrs.disable_linking; this.controllerParams.measures = measures; this.controllerParams.groupableFields = groupableFields; // retrieve form and list view ids from the action to open those views diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 72e215f9c24..e45516acb37 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -165,9 +165,6 @@ var FormViewDialog = ViewDialog.extend({ } fields_view_def.then(function (viewInfo) { - if (self.recordID) { - self.model.addFieldsInfo(self.recordID, viewInfo); - } var formview = new FormView(viewInfo, { modelName: self.res_model, context: self.context, diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index dbe1a2c49e0..8a44e1eb1e7 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -2,10 +2,8 @@ odoo.define('web.MockServer', function (require) { "use strict"; var Class = require('web.Class'); -var data_manager = require('web.data_manager'); var Domain = require('web.Domain'); var pyeval = require('web.pyeval'); -var utils = require('web.utils'); var MockServer = Class.extend({ /** @@ -56,6 +54,15 @@ var MockServer = Class.extend({ // Public //-------------------------------------------------------------------------- + /** + * Mocks a fields_get RPC for a given model. + * + * @param {string} model + * @returns {Object} + */ + fieldsGet: function (model) { + return this.data[model].fields; + }, /** * helper: read a string describing an arch, and returns a simulated * 'field_view_get' call to the server. Calls processViews() of data_manager @@ -77,13 +84,10 @@ var MockServer = Class.extend({ } var fields = $.extend(true, {}, this.data[model].fields); var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context); - var fields_views = {}; - fields_views[fvg.type] = fvg; - data_manager.processViews(fields_views, fields); if (toolbar) { fvg.toolbar = toolbar; } - return fields_views[fvg.type]; + return fvg; }, /** * Simulate a complete RPC call. This is the main method for this class. @@ -166,7 +170,7 @@ var MockServer = Class.extend({ * 'fields_view_get' call to the server. * * @private - * @param {string|Object} arch a string OR a parsed xml document + * @param {string} arch a string OR a parsed xml document * @param {string} model a model name (that should be in this.data) * @param {Object} fields * @param {Object} context @@ -179,29 +183,32 @@ var MockServer = Class.extend({ var onchanges = this.data[model].onchanges || {}; var fieldNodes = {}; + var doc; if (typeof arch === 'string') { - var doc = $.parseXML(arch).documentElement; - arch = utils.xml_to_json(doc, true); + doc = $.parseXML(arch).documentElement; + } else { + doc = arch; } - var inTreeView = (arch.tag === 'tree'); + var inTreeView = (doc.tagName === 'tree'); - this._traverse(arch, function (node) { - if (typeof node === "string") { + this._traverse(doc, function (node) { + if (node.nodeType === Node.TEXT_NODE) { return false; } var modifiers = {}; - var isField = (node.tag === 'field'); + var isField = (node.tagName === 'field'); if (isField) { - fieldNodes[node.attrs.name] = node; + var fieldName = node.getAttribute('name'); + fieldNodes[fieldName] = node; // 'transfer_field_to_modifiers' simulation - var field = fields[node.attrs.name]; + var field = fields[fieldName]; if (!field) { - throw new Error("Field " + node.attrs.name + " does not exist"); + throw new Error("Field " + fieldName + " does not exist"); } var defaultValues = {}; var stateExceptions = {}; @@ -209,7 +216,7 @@ var MockServer = Class.extend({ stateExceptions[attr] = []; defaultValues[attr] = !!field[attr]; }); - _.each(field['states'] || {}, function (modifs, state) { + _.each(field.states || {}, function (modifs, state) { _.each(modifs, function (modif) { if (defaultValues[modif[0]] !== modif[1]) { stateExceptions[modif[0]].append(state); @@ -226,36 +233,42 @@ var MockServer = Class.extend({ } // 'transfer_node_to_modifiers' simulation - if (node.attrs.attrs) { - var attrs = pyeval.py_eval(node.attrs.attrs); + var attrs = node.getAttribute('attrs'); + if (attrs) { + attrs = pyeval.py_eval(attrs); _.extend(modifiers, attrs); - delete node.attrs.attrs; + node.removeAttribute('attrs'); } - if (node.attrs.states) { + + var states = node.getAttribute('states'); + if (states) { if (!modifiers.invisible) { modifiers.invisible = []; } - modifiers.invisible.push(["state", "not in", node.attrs.states.split(",")]); + modifiers.invisible.push(["state", "not in", states.split(",")]); } _.each(modifiersNames, function (a) { - if (node.attrs[a]) { + var mod = node.getAttribute(a); + if (mod) { var pyevalContext = window.py.dict.fromJSON(context || {}); - var v = pyeval.py_eval(node.attrs[a], {context: pyevalContext}) ? true: false; + var v = pyeval.py_eval(mod, {context: pyevalContext}) ? true: false; if (inTreeView && a === 'invisible') { - modifiers['column_invisible'] = v; + modifiers.column_invisible = v; } else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) { modifiers[a] = v; } } }); - // 'transfer_modifiers_to_node' simulation _.each(modifiersNames, function (a) { if (a in modifiers && (!!modifiers[a] === false || (_.isArray(modifiers[a]) && !modifiers[a].length))) { delete modifiers[a]; } }); - node.attrs.modifiers = JSON.stringify(modifiers); + + if (Object.keys(modifiers).length) { + node.setAttribute('modifiers', JSON.stringify(modifiers)); + } return !isField; }); @@ -264,29 +277,34 @@ var MockServer = Class.extend({ _.each(fieldNodes, function (node, name) { var field = fields[name]; if (field.type === "many2one" || field.type === "many2many") { - node.attrs.can_create = node.attrs.can_create || "true"; - node.attrs.can_write = node.attrs.can_write || "true"; + var canCreate = node.getAttribute('can_create'); + node.setAttribute('can_create', canCreate || "true"); + var canWrite = node.getAttribute('can_write'); + node.setAttribute('can_write', canWrite || "true"); } if (field.type === "one2many" || field.type === "many2many") { field.views = {}; - _.each(node.children, function (children) { + _.each(node.childNodes, function (children) { relModel = field.relation; relFields = $.extend(true, {}, self.data[relModel].fields); - field.views[children.tag] = self._fieldsViewGet(children, relModel, + field.views[children.tagName] = self._fieldsViewGet(children, relModel, relFields, context); }); } // add onchanges if (name in onchanges) { - node.attrs.on_change="1"; + node.setAttribute('on_change', "1"); } }); + + var xmlSerializer = new XMLSerializer(); + var processedArch = xmlSerializer.serializeToString(doc); return { - arch: arch, + arch: processedArch, fields: _.pick(fields, _.keys(fieldNodes)), model: model, - type: arch.tag === 'tree' ? 'list' : arch.tag, + type: doc.tagName === 'tree' ? 'list' : doc.tagName, }; }, /** @@ -1040,7 +1058,7 @@ var MockServer = Class.extend({ _traverse: function (tree, f) { var self = this; if (f(tree)) { - _.each(tree.children, function (c) { self._traverse(c, f); }); + _.each(tree.childNodes, function (c) { self._traverse(c, f); }); } }, /** diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 850301fb981..260436ff152 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -126,6 +126,23 @@ var createActionManager = function (params) { return actionManager; }; +/** + * performs a fields_view_get, and mocks the postprocessing done by the + * data_manager to return an equivalent structure. + * + * @param {MockServer} server + * @param {Object} params + * @param {string} params.model + * @returns {Object} an object with 3 keys: arch, fields and viewFields + */ +function fieldsViewGet(server, params) { + var fieldsView = server.fieldsViewGet(params); + // mock the structure produced by the DataManager + fieldsView.viewFields = fieldsView.fields; + fieldsView.fields = server.fieldsGet(params.model); + return fieldsView; +} + /** * create a view synchronously. This method uses the createAsyncView method. * Most views are synchronous, so the deferred can be resolved immediately and @@ -189,8 +206,7 @@ function createAsyncView(params) { // add mock environment: mock server, session, fieldviewget, ... var mockServer = addMockEnvironment(widget, params); - var viewInfo = mockServer.fieldsViewGet(params); - + var viewInfo = fieldsViewGet(mockServer, params); // create the view var viewOptions = { modelName: params.model || 'foo', @@ -419,7 +435,7 @@ function addMockEnvironment(widget, params) { model: event.data.modelName, }).then(function (views) { views = _.mapObject(views, function (viewParams) { - return mockServer.fieldsViewGet(viewParams); + return fieldsViewGet(mockServer, viewParams); }); event.data.on_success(views); }); @@ -694,6 +710,7 @@ return $.when( createParent: createParent, createView: createView, dragAndDrop: dragAndDrop, + fieldsViewGet: fieldsViewGet, intercept: intercept, observe: observe, patch: patch, diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 48a8faf4f3e..835aa628ced 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -114,36 +114,12 @@ QUnit.module('Views', { ''+ ''+ '', - "event,1,form": { - attrs: {}, - children: [ - { - attrs: { - modifiers: '{"invisible": true}', - name: "allday" - }, - children: [], - tag: 'field' - }, - { - attrs: { - modifiers: '{"invisible": [["allday","=",false]]}', - name: "start" - }, - children: [], - tag: 'field' - }, - { - attrs: { - modifiers: '{"invisible": [["allday","=",true]]}', - name: "stop" - }, - children: [], - tag: 'field' - } - ], - tag: "form" - } + "event,1,form": + '
' + + '' + + '