diff options
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/boot.js | 10 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 92 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 292 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 65 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 18 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 36 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 136 | ||||
| -rw-r--r-- | addons/web/static/src/js/testing.js | 393 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 248 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 38 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 100 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 70 |
12 files changed, 1095 insertions, 403 deletions
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 05ce709e80a..631a7f8c6f5 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,12 +19,14 @@ /** * OpenERP instance constructor * - * @param {Array} modules list of modules to initialize + * @param {Array|String} modules list of modules to initialize */ init: function(modules) { - // By default only web will be loaded, the rest will be by loaded - // by openerp.web.Session on the first session_authenticate - modules = _.union(['web'], modules || []); + if (modules === "fuck your shit, don't load anything you cunt") { + modules = []; + } else { + modules = _.union(['web'], modules || []); + } var new_instance = { // links to the global openerp _openerp: openerp, diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 9c249a5fb0c..b95550c74a0 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -48,7 +48,7 @@ instance.web.Notification = instance.web.Widget.extend({ */ instance.web.dialog = function(element) { var result = element.dialog.apply(element, _.rest(_.toArray(arguments))); - result.dialog("widget").addClass("openerp"); + result.dialog("widget").openerpClass(); return result; }; @@ -189,8 +189,15 @@ instance.web.Dialog = instance.web.Widget.extend({ } }); -instance.web.CrashManager = instance.web.CallbackEnabled.extend({ +instance.web.CrashManager = instance.web.Class.extend({ + init: function() { + this.active = true; + }, + rpc_error: function(error) { + if (!this.active) { + return; + } if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); if (split.length > 1) { @@ -205,6 +212,9 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ } }, show_warning: function(error) { + if (!this.active) { + return; + } instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ @@ -213,7 +223,9 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ }); }, show_error: function(error) { - var self = this; + if (!this.active) { + return; + } var buttons = {}; buttons[_t("Ok")] = function() { $(this).dialog("close"); @@ -303,14 +315,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty(); var fetch_db = this.rpc("/web/database/get_list", {}).then( function(result) { - self.db_list = result.db_list; + self.db_list = result; }, function (_, ev) { ev.preventDefault(); self.db_list = null; }); var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) { - self.lang_list = result.lang_list; + self.lang_list = result; }); return $.when(fetch_db, fetch_langs).done(self.do_render); }, @@ -333,6 +345,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label"); self.$el.find("label").addClass("oe_form_label"); self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); + self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate }); self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); @@ -418,6 +431,18 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.do_action(client_action); }); }, + do_duplicate: function(form) { + var self = this; + var fields = $(form).serializeArray(); + self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) { + if (result.error) { + self.display_error(result); + return; + } + self.do_notify("Duplicating database", "The database has been duplicated."); + self.start(); + }); + }, do_drop: function(form) { var self = this; var $form = $(form), @@ -518,6 +543,9 @@ instance.web.Login = instance.web.Widget.extend({ this.selected_db = null; this.selected_login = null; this.params = action.params || {}; + if (_.isEmpty(this.params)) { + this.params = $.bbq.getState(true); + } if (this.params.login_successful) { this.on('login_successful', this, this.params.login_successful); @@ -548,7 +576,7 @@ instance.web.Login = instance.web.Widget.extend({ return d; }, on_db_loaded: function (result) { - this.db_list = result.db_list; + this.db_list = result; this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db})); if(this.db_list.length === 0) { this.do_action("database_manager"); @@ -620,8 +648,34 @@ instance.web.Login = instance.web.Widget.extend({ instance.web.client_actions.add("login", "instance.web.Login"); /** + * Redirect to url by replacing window.location + * If wait is true, sleep 1s and wait for the server i.e. after a restart. + */ +instance.web.redirect = function(url, wait) { + // Dont display a dialog if some xmlhttprequest are in progress + if (instance.client && instance.client.crashmanager) { + instance.client.crashmanager.active = false; + } + + var wait_server = function() { + instance.session.rpc("/web/webclient/version_info", {}).done(function() { + window.location = url; + }).fail(function() { + setTimeout(wait_server, 250); + }); + }; + + if (wait) { + setTimeout(wait_server, 1000); + } else { + window.location = url; + } +}; + +/** * Client action to reload the whole interface. - * If params has an entry 'menu_id', it opens the given menu entry. + * If params.menu_id, it opens the given menu entry. + * If params.wait, reload will wait the openerp server to be reachable before reloading */ instance.web.Reload = function(parent, action) { var params = action.params || {}; @@ -637,8 +691,8 @@ instance.web.Reload = function(parent, action) { hash = "#menu_id=" + menu_id; } var url = l.protocol + "//" + l.host + l.pathname + search + hash; - window.onerror = function() {}; - window.location = url; + + instance.web.redirect(url, params.wait); }; instance.web.client_actions.add("reload", "instance.web.Reload"); @@ -648,7 +702,7 @@ instance.web.client_actions.add("reload", "instance.web.Reload"); */ instance.web.HistoryBack = function(parent) { if (!parent.history_back()) { - window.location = '/' + (window.location.search || ''); + instance.web.Home(parent); } }; instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); @@ -656,11 +710,10 @@ instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); /** * Client action to go back home. */ -instance.web.Home = instance.web.Widget.extend({ - init: function(parent) { - window.location = '/' + (window.location.search || ''); - } -}); +instance.web.Home = function(parent, action) { + var url = '/' + (window.location.search || ''); + instance.web.redirect(url, action.params && action.params.wait); +}; instance.web.client_actions.add("home", "instance.web.Home"); instance.web.ChangePassword = instance.web.Widget.extend({ @@ -887,7 +940,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ if(res.company_id[0] > 1) topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]); self.$el.find('.oe_topbar_name').text(topbar_name); - var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=image_small&id=%s', self.session.prefix, self.session.session_id, self.session.uid); + var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid}); $avatar.attr('src', avatar_src); }); }; @@ -899,7 +952,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ on_menu_settings: function() { var self = this; if (!this.getParent().has_uncommitted_changes()) { - self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) { result.res_id = instance.session.uid; self.getParent().action_manager.do_action(result); }); @@ -930,6 +983,7 @@ instance.web.Client = instance.web.Widget.extend({ return instance.session.session_bind(this.origin).then(function() { var $e = $(QWeb.render(self._template, {})); self.replaceElement($e); + $e.openerpClass(); self.bind_events(); return self.show_common(); }); @@ -1106,10 +1160,10 @@ instance.web.WebClient = instance.web.Client.extend({ var self = this; var state = event.getState(true); if (!_.isEqual(this._current_state, state)) { - if(state.action_id === undefined && state.menu_id) { + if(!state.action && state.menu_id) { self.menu.has_been_loaded.done(function() { self.menu.do_reload().done(function() { - self.menu.menu_click(state.menu_id) + self.menu.menu_click(state.menu_id); }); }); } else { diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index d2e94d37939..06b3940f1e1 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -243,15 +243,17 @@ instance.web.ParentedMixin = { /** * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead. + * + * This class just handle the dispatching of events, it is not meant to be extended, + * nor used directly. All integration with parenting and automatic unregistration of + * events is done in EventDispatcherMixin. + * + * Copyright notice for the following Class: * * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. * Backbone may be freely distributed under the MIT license. * For all details and documentation: * http://backbonejs.org - * - * This class just handle the dispatching of events, it is not meant to be extended, - * nor used directly. All integration with parenting and automatic unregistration of - * events is done in EventDispatcherMixin. * */ var Events = instance.web.Class.extend({ @@ -334,7 +336,6 @@ var Events = instance.web.Class.extend({ return this; } }); -// end of Jeremy Ashkenas' code instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { __eventDispatcherMixin: true, @@ -426,66 +427,94 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { } }); -instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { - init: function() { - instance.web.PropertiesMixin.init.call(this); - var self = this; - // Transform on_/do_* methods into callbacks - var callback_maker = function(fn) { - return function() { - return fn.apply(self, arguments); - } - }; - for (var name in this) { - if(typeof(this[name]) == "function") { - if((/^on_|^do_/).test(name)) { - this[name] = callback_maker(this[name]); - } - } - } - }, +// Classes + +/** + * Base class for all visual components. Provides a lot of functionalities helpful + * for the management of a part of the DOM. + * + * Widget handles: + * - Rendering with QWeb. + * - Life-cycle management and parenting (when a parent is destroyed, all its children are + * destroyed too). + * - Insertion in DOM. + * + * Guide to create implementations of the Widget class: + * ============================================== + * + * Here is a sample child class: + * + * MyWidget = instance.base.Widget.extend({ + * // the name of the QWeb template to use for rendering + * template: "MyQWebTemplate", + * + * init: function(parent) { + * this._super(parent); + * // stuff that you want to init before the rendering + * }, + * start: function() { + * // stuff you want to make after the rendering, `this.$el` holds a correct value + * this.$el.find(".my_button").click(/* an example of event binding * /); + * + * // if you have some asynchronous operations, it's a good idea to return + * // a promise in start() + * var promise = this.rpc(...); + * return promise; + * } + * }); + * + * Now this class can simply be used with the following syntax: + * + * var my_widget = new MyWidget(this); + * my_widget.appendTo($(".some-div")); + * + * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the + * DOM inside the ".some-div" div and its events were binded. + * + * And of course, when you don't need that widget anymore, just do: + * + * my_widget.destroy(); + * + * That will kill the widget in a clean way and erase its content from the dom. + */ +instance.web.Widget = instance.web.Class.extend(instance.web.PropertiesMixin, { + // Backbone-ish API + tagName: 'div', + id: null, + className: null, + attributes: {}, + events: {}, /** - * Proxies a method of the object, in order to keep the right ``this`` on - * method invocations. - * - * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and - * even more so to ``jQuery.proxy`` with a fundamental difference: its - * resolution of the method being called is lazy, meaning it will use the - * method as it is when the proxy is called, not when the proxy is created. - * - * Other methods will fix the bound method to what it is when creating the - * binding/proxy, which is fine in most javascript code but problematic in - * OpenERP Web where developers may want to replace existing callbacks with - * theirs. - * - * The semantics of this precisely replace closing over the method call. + * The name of the QWeb template that will be used for rendering. Must be + * redefined in subclasses or the default render() method can not be used. * - * @param {String|Function} method function or name of the method to invoke - * @returns {Function} proxied method + * @type string */ - proxy: function (method) { - var self = this; - return function () { - var fn = (typeof method === 'string') ? self[method] : method; - return fn.apply(self, arguments); - } - } -}); - -instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { + template: null, /** * Constructs the widget and sets its parent if a parent is given. * * @constructs instance.web.Widget - * @extends instance.web.CallbackEnabled * * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. */ init: function(parent) { - instance.web.CallbackEnabledMixin.init.call(this); + instance.web.PropertiesMixin.init.call(this); this.setParent(parent); + // Bind on_/do_* methods to this + // We might remove this automatic binding in the future + for (var name in this) { + if(typeof(this[name]) == "function") { + if((/^on_|^do_/).test(name)) { + this[name] = this[name].bind(this); + } + } + } + // FIXME: this should not be + this.setElement(this._make_descriptive()); + this.session = instance.session; }, /** * Destroys the current widget, also destroys all its children before destroying itself. @@ -574,94 +603,32 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { */ start: function() { return $.when(); - } -}); - -// Classes - -instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEnabledMixin, { - init: function() { - instance.web.CallbackEnabledMixin.init.call(this); - } -}); - -/** - * Base class for all visual components. Provides a lot of functionalities helpful - * for the management of a part of the DOM. - * - * Widget handles: - * - Rendering with QWeb. - * - Life-cycle management and parenting (when a parent is destroyed, all its children are - * destroyed too). - * - Insertion in DOM. - * - * Guide to create implementations of the Widget class: - * ============================================== - * - * Here is a sample child class: - * - * MyWidget = instance.base.Widget.extend({ - * // the name of the QWeb template to use for rendering - * template: "MyQWebTemplate", - * - * init: function(parent) { - * this._super(parent); - * // stuff that you want to init before the rendering - * }, - * start: function() { - * // stuff you want to make after the rendering, `this.$el` holds a correct value - * this.$el.find(".my_button").click(/* an example of event binding * /); - * - * // if you have some asynchronous operations, it's a good idea to return - * // a promise in start() - * var promise = this.rpc(...); - * return promise; - * } - * }); - * - * Now this class can simply be used with the following syntax: - * - * var my_widget = new MyWidget(this); - * my_widget.appendTo($(".some-div")); - * - * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the - * DOM inside the ".some-div" div and its events were binded. - * - * And of course, when you don't need that widget anymore, just do: - * - * my_widget.destroy(); - * - * That will kill the widget in a clean way and erase its content from the dom. - */ -instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { - // Backbone-ish API - tagName: 'div', - id: null, - className: null, - attributes: {}, - events: {}, + }, /** - * The name of the QWeb template that will be used for rendering. Must be - * redefined in subclasses or the default render() method can not be used. + * Proxies a method of the object, in order to keep the right ``this`` on + * method invocations. * - * @type string - */ - template: null, - /** - * Constructs the widget and sets its parent if a parent is given. + * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and + * even more so to ``jQuery.proxy`` with a fundamental difference: its + * resolution of the method being called is lazy, meaning it will use the + * method as it is when the proxy is called, not when the proxy is created. * - * @constructs instance.web.Widget - * @extends instance.web.CallbackEnabled + * Other methods will fix the bound method to what it is when creating the + * binding/proxy, which is fine in most javascript code but problematic in + * OpenERP Web where developers may want to replace existing callbacks with + * theirs. * - * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. - * When that widget is destroyed by calling destroy(), the current instance will be - * destroyed too. Can be null. + * The semantics of this precisely replace closing over the method call. + * + * @param {String|Function} method function or name of the method to invoke + * @returns {Function} proxied method */ - init: function(parent) { - instance.web.WidgetMixin.init.call(this,parent); - // FIXME: this should not be - this.setElement(this._make_descriptive()); - this.session = instance.session; + proxy: function (method) { + var self = this; + return function () { + var fn = (typeof method === 'string') ? self[method] : method; + return fn.apply(self, arguments); + } }, /** * Renders the element. The default implementation renders the widget using QWeb, @@ -678,7 +645,6 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { } this.replaceElement($el); }, - /** * Re-sets the widget's root element and replaces the old root element * (if any) by the new one in the DOM. @@ -808,10 +774,10 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { } return false; }, - rpc: function(url, data, success, error) { - var def = $.Deferred().done(success).fail(error); + rpc: function(url, data, options) { + var def = $.Deferred(); var self = this; - instance.session.rpc(url, data).done(function() { + instance.session.rpc(url, data, options).done(function() { if (!self.isDestroyed()) def.resolve.apply(def, arguments); }).fail(function() { @@ -941,7 +907,7 @@ instance.web.Registry = instance.web.Class.extend({ } }); -instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ +instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { triggers: { 'request': 'Request sent', 'response': 'Response received', @@ -950,13 +916,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }, /** * @constructs instance.web.JsonRPC - * @extends instance.web.CallbackEnabled * * @param {String} [server] JSON-RPC endpoint hostname * @param {String} [port] JSON-RPC endpoint port */ init: function() { - this._super(); + instance.web.PropertiesMixin.init.call(this); this.server = null; this.debug = ($.deparam($.param.querystring()).debug != undefined); }, @@ -1266,12 +1231,14 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ * * @param {String} url RPC endpoint * @param {Object} params call parameters + * @param {Object} options additional options for rpc call * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ - rpc: function(url, params) { + rpc: function(url, params, options) { var self = this; + options = options || {}; // url can be an $.ajax option object if (_.isString(url)) { url = { url: url }; @@ -1286,10 +1253,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.trigger('request', url, payload); + if (! options.shadow) + this.trigger('request', url, payload); var request = this.rpc_function(url, payload).done( function (response, textStatus, jqXHR) { - self.trigger('response', response); + if (! options.shadow) + self.trigger('response', response); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); @@ -1303,7 +1272,8 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ } ).fail( function(jqXHR, textStatus, errorThrown) { - self.trigger('response_failed', jqXHR); + if (! options.shadow) + self.trigger('response_failed', jqXHR); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, @@ -1311,7 +1281,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }; deferred.reject(error, $.Event()); }); - // Allow deferred user to disable on_rpc_error in fail + // Allow deferred user to disable rpc_error call in fail deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { @@ -1344,9 +1314,18 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ // extracted from payload to set on the url var data = { session_id: this.session_id, - id: payload.id + id: payload.id, + sid: this.httpsessionid, }; - url.url = this.get_url(url.url); + + var set_sid = function (response, textStatus, jqXHR) { + // If response give us the http session id, we store it for next requests... + if (response.httpsessionid) { + self.httpsessionid = response.httpsessionid; + } + }; + + url.url = this.url(url.url, null); var ajax = _.extend({ type: "GET", dataType: 'jsonp', @@ -1361,7 +1340,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ if(payload_url.length < 2000) { // Direct jsonp request ajax.data.r = payload_str; - return $.ajax(ajax); + return $.ajax(ajax).done(set_sid); } else { // Indirect jsonp request var ifid = _.uniqueId('oe_rpc_iframe'); @@ -1399,11 +1378,20 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }); // append the iframe to the DOM (will trigger the first load) $form.after($iframe); - return deferred; + return deferred.done(set_sid); } }, - get_url: function (file) { - return this.prefix + file; + + url: function(path, params) { + var qs = ''; + if (!_.isNull(params)) { + params = _.extend(params || {}, {session_id: this.session_id}); + if (this.httpsessionid) { + params.sid = this.httpsessionid; + } + qs = '?' + $.param(params); + } + return this.prefix + path + qs; }, }); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index b7dac303520..7ddefa301f2 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -3,10 +3,13 @@ *--------------------------------------------------------*/ var console; if (!console) { - console = {log: function () {}}; -} -if (!console.debug) { - console.debug = console.log; + // Even IE9 only exposes console object if debug window opened + console = {}; + ('log error debug info warn assert clear dir dirxml trace group' + + ' groupCollapsed groupEnd time timeEnd profile profileEnd count' + + ' exception').split(/\s+/).forEach(function(property) { + console[property] = _.identity; + }); } openerp.web.coresetup = function(instance) { @@ -19,9 +22,9 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.name = instance._session_id; this.qweb_mutex = new $.Mutex(); }, - rpc: function(url, params) { + rpc: function(url, params, options) { params.session_id = this.session_id; - return this._super(url, params); + return this._super(url, params, options); }, /** * Setup a sessionm @@ -53,7 +56,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.session_id = this.get_cookie('session_id'); return this.session_reload().then(function(result) { var modules = instance._modules.join(','); - var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).then(self.do_load_qweb); + var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).then(self.load_qweb.bind(self)); if(self.session_is_valid()) { return deferred.then(function() { return self.load_modules(); }); } @@ -155,28 +158,26 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess load_modules: function() { var self = this; return this.rpc('/web/session/modules', {}).then(function(result) { - var lang = self.user_context.lang, - all_modules = _.uniq(self.module_list.concat(result)); - var params = { mods: all_modules, lang: lang}; + var all_modules = _.uniq(self.module_list.concat(result)); var to_load = _.difference(result, self.module_list).join(','); self.module_list = all_modules; - var loaded = self.rpc('/web/webclient/translations', params).done(function(trans) { - instance.web._t.database.set_bundle(trans); - }); - var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"]; + var loaded = self.load_translations(); + var datejs_locale = "/web/static/lib/datejs/globalization/" + self.user_context.lang.replace("_", "-") + ".js"; + + var file_list = [ datejs_locale ]; if(to_load.length) { loaded = $.when( loaded, - self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.do_load_css), - self.rpc('/web/webclient/qweblist', {mods: to_load}).then(self.do_load_qweb), + self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)), + self.rpc('/web/webclient/qweblist', {mods: to_load}).then(self.load_qweb.bind(self)), self.rpc('/web/webclient/jslist', {mods: to_load}).done(function(files) { file_list = file_list.concat(files); }) ); } return loaded.then(function () { - return self.do_load_js(file_list); + return self.load_js(file_list); }).done(function() { self.on_modules_loaded(); self.trigger('module_loaded'); @@ -190,29 +191,35 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess }); }); }, - do_load_css: function (files) { + load_translations: function() { + var params = { mods: this.module_list, lang: this.user_context.lang }; + return this.rpc('/web/webclient/translations', params).done(function(trans) { + instance.web._t.database.set_bundle(trans); + }); + }, + load_css: function (files) { var self = this; _.each(files, function (file) { $('head').append($('<link>', { - 'href': self.get_url(file), + 'href': self.url(file, null), 'rel': 'stylesheet', 'type': 'text/css' })); }); }, - do_load_js: function(files) { + load_js: function(files) { var self = this; var d = $.Deferred(); - if(files.length != 0) { + if(files.length !== 0) { var file = files.shift(); var tag = document.createElement('script'); tag.type = 'text/javascript'; - tag.src = self.get_url(file); + tag.src = self.url(file, null); tag.onload = tag.onreadystatechange = function() { if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done ) return; tag.onload_done = true; - self.do_load_js(files).done(function () { + self.load_js(files).done(function () { d.resolve(); }); }; @@ -223,7 +230,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess } return d; }, - do_load_qweb: function(files) { + load_qweb: function(files) { var self = this; _.each(files, function(file) { self.qweb_mutex.exec(function() { @@ -453,6 +460,16 @@ $.fn.getAttributes = function() { } return o; } +$.fn.openerpClass = function(additionalClass) { + // This plugin should be applied on top level elements + additionalClass = additionalClass || ''; + if (!!$.browser.msie) { + additionalClass += ' openerp_ie'; + } + return this.each(function() { + $(this).addClass('openerp ' + additionalClass); + }); +}; /** Jquery extentions */ $.Mutex = (function() { diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 2f8679794b0..524a88bdc48 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -278,9 +278,10 @@ instance.web.Model = instance.web.Class.extend({ * @param {String} method name of the method to call * @param {Array} [args] positional arguments * @param {Object} [kwargs] keyword arguments + * @param {Object} [options] additional options for the rpc() method * @returns {jQuery.Deferred<>} call result */ - call: function (method, args, kwargs) { + call: function (method, args, kwargs, options) { args = args || []; kwargs = kwargs || {}; if (!_.isArray(args)) { @@ -294,7 +295,7 @@ instance.web.Model = instance.web.Class.extend({ method: method, args: args, kwargs: kwargs - }); + }, options); }, /** * Fetches a Query instance bound to this model, for searching @@ -358,17 +359,16 @@ instance.web.Model = instance.web.Class.extend({ }, }); -instance.web.DataSet = instance.web.CallbackEnabled.extend({ +instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, { /** * Collection of OpenERP records, used to share records and the current selection between views. * * @constructs instance.web.DataSet - * @extends instance.web.CallbackEnabled * * @param {String} model the OpenERP model this dataset will manage */ init: function(parent, model, context) { - this._super(parent); + instance.web.PropertiesMixin.init.call(this); this.model = model; this.context = context || {}; this.index = null; @@ -722,10 +722,10 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.last_default_get = {}; }, default_get: function(fields, options) { - return this._super(fields, options).done(this.on_default_get); - }, - on_default_get: function(res) { - this.last_default_get = res; + var self = this; + return this._super(fields, options).done(function(res) { + self.last_default_get = res; + }); }, create: function(data) { var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 03b2c5b775d..e586b94db6b 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -83,6 +83,42 @@ instance.web.strip_raw_chars = function (value) { var normalize_format = function (format) { return Date.normalizeFormat(instance.web.strip_raw_chars(format)); }; + +/** + * Check with a scary heuristic if the value is a bin_size or not. + * If not, compute an approximate size out of the base64 encoded string. + * + * @param {String} value original format + */ +instance.web.binary_to_binsize = function (value) { + if (!value) { + return instance.web.human_size(0); + } + if (value.substr(0, 10).indexOf(' ') == -1) { + // Computing approximate size out of base64 encoded string + // http://en.wikipedia.org/wiki/Base64#MIME + return instance.web.human_size(value.length / 1.37); + } else { + // already bin_size + return value; + } +}; + +/** + * Returns a human readable size + * + * @param {Number} numner of bytes + */ +instance.web.human_size = function(size) { + var units = _t("Bytes,Kb,Mb,Gb,Tb,Pb,Eb,Zb,Yb").split(','); + var i = 0; + while (size >= 1024) { + size /= 1024; + ++i; + } + return size.toFixed(2) + ' ' + units[i]; +}; + /** * Formats a single atomic value based on a field descriptor * diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index e84ea66ff9a..0f25b2484c3 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -349,7 +349,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea $.when(load_view) .then(function(r) { self.search_view_loaded(r) - }).fail(function () { + }, function () { self.ready.reject.apply(null, arguments); }); } @@ -459,7 +459,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea complete_global_search: function (req, resp) { $.when.apply(null, _(this.inputs).chain() .invoke('complete', req.term) - .value()).done(function () { + .value()).then(function () { resp(_(_(arguments).compact()).flatten(true)); }); }, @@ -527,7 +527,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea childView.on('blurred', self, self.proxy('childBlurred')); }); - $.when.apply(null, started).done(function () { + $.when.apply(null, started).then(function () { var input_to_focus; // options.at: facet inserted at given index, focus next input // otherwise just focus last input @@ -608,7 +608,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // add Filters to this.inputs, need view.controls filled (new instance.web.search.Filters(this)); // add custom filters to this.inputs - (new instance.web.search.CustomFilters(this)); + this.custom_filters = new instance.web.search.CustomFilters(this); // add Advanced to this.inputs (new instance.web.search.Advanced(this)); }, @@ -635,16 +635,41 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // load defaults var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( - 'facet_for_defaults', this.defaults)).done(function () { - self.query.reset(_(arguments).compact(), {preventSearch: true}); - }); - + 'facet_for_defaults', this.defaults)) + .then(this.proxy('setup_default_query')); + return $.when(drawer_started, defaults_fetched) .done(function () { self.trigger("search_view_loaded", data); self.ready.resolve(); }); }, + setup_default_query: function () { + // Hacky implementation of CustomFilters#facet_for_defaults ensure + // CustomFilters will be ready (and CustomFilters#filters will be + // correctly filled) by the time this method executes. + var custom_filters = this.custom_filters.filters; + if (!_(custom_filters).isEmpty()) { + // Check for any is_default custom filter + var personal_filter = _(custom_filters).find(function (filter) { + return filter.user_id && filter.is_default; + }); + if (personal_filter) { + this.custom_filters.enable_filter(personal_filter, true); + return; + } + + var global_filter = _(custom_filters).find(function (filter) { + return !filter.user_id && filter.is_default; + }); + if (global_filter) { + this.custom_filters.enable_filter(global_filter, true); + return; + } + } + // No custom filter, or no is_default custom filter, apply view defaults + this.query.reset(_(arguments).compact(), {preventSearch: true}); + }, /** * Extract search data from the view's facets. * @@ -1460,10 +1485,15 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ instance.web.search.CustomFilters = instance.web.search.Input.extend({ template: 'SearchView.CustomFilters', _in_drawer: true, + init: function () { + this.is_ready = $.Deferred(); + this._super.apply(this, arguments); + }, start: function () { var self = this; this.model = new instance.web.Model('ir.filters'); this.filters = {}; + this.$filters = {}; this.view.query .on('remove', function (facet) { if (!facet.get('is_custom_filter')) { @@ -1479,24 +1509,78 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ // FIXME: local eval of domain and context to get rid of special endpoint return this.rpc('/web/searchview/get_filters', { model: this.view.model - }).then(this.proxy('set_filters')); + }) + .then(this.proxy('set_filters')) + .then(function () { + self.is_ready.resolve(null); + }, function () { + self.is_ready.reject(); + }); + }, + /** + * Special implementation delaying defaults until CustomFilters is loaded + */ + facet_for_defaults: function () { + return this.is_ready; + }, + /** + * Generates a mapping key (in the filters and $filter mappings) for the + * filter descriptor object provided (as returned by ``get_filters``). + * + * The mapping key is guaranteed to be unique for a given (user_id, name) + * pair. + * + * @param {Object} filter + * @param {String} filter.name + * @param {Number|Pair<Number, String>} [filter.user_id] + * @return {String} mapping key corresponding to the filter + */ + key_for: function (filter) { + var user_id = filter.user_id; + var uid = (user_id instanceof Array) ? user_id[0] : user_id; + return _.str.sprintf('(%s)%s', uid, filter.name); + }, + /** + * Generates a :js:class:`~instance.web.search.Facet` descriptor from a + * filter descriptor + * + * @param {Object} filter + * @param {String} filter.name + * @param {Object} [filter.context] + * @param {Array} [filter.domain] + * @return {Object} + */ + facet_for: function (filter) { + return { + category: _t("Custom Filter"), + icon: 'M', + field: { + get_context: function () { return filter.context; }, + get_groupby: function () { return [filter.context]; }, + get_domain: function () { return filter.domain; } + }, + is_custom_filter: true, + values: [{label: filter.name, value: null}] + }; }, clear_selection: function () { this.$('li.oe_selected').removeClass('oe_selected'); }, append_filter: function (filter) { var self = this; - var key = _.str.sprintf('(%s)%s', filter.user_id, filter.name); + var key = this.key_for(filter); var $filter; - if (key in this.filters) { - $filter = this.filters[key]; + if (key in this.$filters) { + $filter = this.$filters[key]; } else { var id = filter.id; - $filter = this.filters[key] = $('<li></li>') + this.filters[key] = filter; + $filter = this.$filters[key] = $('<li></li>') .appendTo(this.$('.oe_searchview_custom_list')) .addClass(filter.user_id ? 'oe_searchview_custom_private' : 'oe_searchview_custom_public') + .toggleClass('oe_searchview_custom_default', filter.is_default) .text(filter.name); $('<a class="oe_searchview_custom_delete">x</a>') @@ -1504,33 +1588,30 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ e.stopPropagation(); self.model.call('unlink', [id]).done(function () { $filter.remove(); + delete self.$filters[key]; + delete self.filters[key]; }); }) .appendTo($filter); } $filter.unbind('click').click(function () { - self.view.query.reset([{ - category: _t("Custom Filter"), - icon: 'M', - field: { - get_context: function () { return filter.context; }, - get_groupby: function () { return [filter.context]; }, - get_domain: function () { return filter.domain; } - }, - is_custom_filter: true, - values: [{label: filter.name, value: null}] - }]); - $filter.addClass('oe_selected'); + self.enable_filter(filter); }); }, + enable_filter: function (filter, preventSearch) { + this.view.query.reset([this.facet_for(filter)], { + preventSearch: preventSearch || false}); + this.$filters[this.key_for(filter)].addClass('oe_selected'); + }, set_filters: function (filters) { _(filters).map(_.bind(this.append_filter, this)); }, save_current: function () { var self = this; var $name = this.$('input:first'); - var private_filter = !this.$('input:last').prop('checked'); + var private_filter = !this.$('#oe_searchview_custom_public').prop('checked'); + var set_as_default = this.$('#oe_searchview_custom_default').prop('checked'); var search = this.view.build_search_data(); this.rpc('/web/session/eval_domain_and_context', { @@ -1546,7 +1627,8 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ user_id: private_filter ? instance.session.uid : false, model_id: self.view.model, context: results.context, - domain: results.domain + domain: results.domain, + is_default: set_as_default }; // FIXME: current context? return self.model.call('create_or_replace', [filter]).done(function (id) { diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js new file mode 100644 index 00000000000..bf39f543734 --- /dev/null +++ b/addons/web/static/src/js/testing.js @@ -0,0 +1,393 @@ +// Test support structures and methods for OpenERP +openerp.testing = {}; +(function (testing) { + var dependencies = { + corelib: [], + coresetup: ['corelib'], + data: ['corelib', 'coresetup'], + dates: [], + formats: ['coresetup', 'dates'], + chrome: ['corelib', 'coresetup'], + views: ['corelib', 'coresetup', 'data', 'chrome'], + search: ['data', 'coresetup', 'formats'], + list: ['views', 'data'], + form: ['data', 'views', 'list', 'formats'], + list_editable: ['list', 'form', 'data'], + }; + + testing.dependencies = window['oe_all_dependencies'] || []; + testing.current_module = null; + testing.templates = { }; + testing.add_template = function (name) { + var xhr = QWeb2.Engine.prototype.get_xhr(); + xhr.open('GET', name, false); + xhr.send(null); + (testing.templates[testing.current_module] = + testing.templates[testing.current_module] || []) + .push(xhr.responseXML); + }; + /** + * Function which does not do anything + */ + testing.noop = function () { }; + /** + * Alter provided instance's ``session`` attribute to make response + * mockable: + * + * * The ``responses`` parameter can be used to provide a map of (RPC) + * paths (e.g. ``/web/view/load``) to a function returning a response + * to the query. + * * ``instance.session`` grows a ``responses`` attribute which is + * a map of the same (and is in fact initialized to the ``responses`` + * parameter if one is provided) + * + * Note that RPC requests to un-mocked URLs will be rejected with an + * error message: only explicitly specified urls will get a response. + * + * Mocked sessions will *never* perform an actual RPC connection. + * + * @param instance openerp instance being initialized + * @param {Object} [responses] + */ + testing.mockifyRPC = function (instance, responses) { + var session = instance.session; + session.responses = responses || {}; + session.rpc_function = function (url, payload) { + var fn, params; + var needle = payload.params.model + ':' + payload.params.method; + if (url.url === '/web/dataset/call_kw' + && needle in this.responses) { + fn = this.responses[needle]; + params = [ + payload.params.args || [], + payload.params.kwargs || {} + ]; + } else { + fn = this.responses[url.url]; + params = [payload]; + } + + if (!fn) { + return $.Deferred().reject({}, 'failed', + _.str.sprintf("Url %s not found in mock responses, with arguments %s", + url.url, JSON.stringify(payload.params)) + ).promise(); + } + try { + return $.when(fn.apply(null, params)).then(function (result) { + // Wrap for RPC layer unwrapper thingy + return {result: result}; + }); + } catch (e) { + // not sure why this looks like that + return $.Deferred().reject({}, 'failed', String(e)); + } + }; + }; + + var StackProto = { + execute: function (fn) { + var args = [].slice.call(arguments, 1); + // Warning: here be dragons + var i = 0, setups = this.setups, teardowns = this.teardowns; + var d = $.Deferred(); + + var succeeded, failed; + var success = function () { + succeeded = _.toArray(arguments); + return teardown(); + }; + var failure = function () { + // save first failure + if (!failed) { + failed = _.toArray(arguments); + } + // chain onto next teardown + return teardown(); + }; + + var setup = function () { + // if setup to execute + if (i < setups.length) { + var f = setups[i] || testing.noop; + $.when(f.apply(null, args)).then(function () { + ++i; + setup(); + }, failure); + } else { + $.when(fn.apply(null, args)).then(success, failure); + } + }; + var teardown = function () { + // if teardown to execute + if (i > 0) { + var f = teardowns[--i] || testing.noop; + $.when(f.apply(null, args)).then(teardown, failure); + } else { + if (failed) { + d.reject.apply(d, failed); + } else if (succeeded) { + d.resolve.apply(d, succeeded); + } else { + throw new Error("Didn't succeed or fail?"); + } + } + }; + setup(); + + return d; + }, + push: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: this.setups.concat([setup]), + teardowns: this.teardowns.concat([teardown]) + }); + }, + unshift: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: [setup].concat(this.setups), + teardowns: [teardown].concat(this.teardowns) + }); + } + }; + /** + * + * @param {Function} [setup] + * @param {Function} [teardown] + * @return {*} + */ + testing.Stack = function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: setup ? [setup] : [], + teardowns: teardown ? [teardown] : [] + }); + }; + + var db = window['oe_db_info']; + testing.section = function (name, options, body) { + if (_.isFunction(options)) { + body = options; + options = {}; + } + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); + + QUnit.module(testing.current_module + '.' + name, {_oe: options}); + body(testing.case); + }; + testing.case = function (name, options, callback) { + if (_.isFunction(options)) { + callback = options; + options = {}; + } + + var module = testing.current_module; + var module_index = _.indexOf(testing.dependencies, module); + var module_deps = testing.dependencies.slice( + // If module not in deps (because only tests, no JS) -> indexOf + // returns -1 -> index becomes 0 -> replace with ``undefined`` so + // Array#slice returns a full copy + 0, module_index + 1 || undefined); + + // Serialize options for this precise test case + // WARNING: typo is from jquery, do not fix! + var env = QUnit.config.currentModuleTestEnviroment; + // section setup + // case setup + // test + // case teardown + // section teardown + var case_stack = testing.Stack() + .push(env._oe.setup, env._oe.teardown) + .push(options.setup, options.teardown); + var opts = _.defaults({}, options, env._oe); + // FIXME: if this test is ignored, will still query + if (opts.rpc === 'rpc' && !db) { + QUnit.config.autostart = false; + db = { + source: null, + supadmin: null, + password: null + }; + var $msg = $('<form style="margin: 0 1em 1em;">') + .append('<h3>A test needs to clone a database</h3>') + .append('<h4>Please provide the source clone information</h4>') + .append(' Source DB: ').append('<input name="source">').append('<br>') + .append(' DB Password: ').append('<input name="supadmin">').append('<br>') + .append('Admin Password: ').append('<input name="password">').append('<br>') + .append('<input type="submit" value="OK"/>') + .submit(function (e) { + e.preventDefault(); + e.stopPropagation(); + db.source = $msg.find('input[name=source]').val(); + db.supadmin = $msg.find('input[name=supadmin]').val(); + db.password = $msg.find('input[name=password]').val(); + QUnit.start(); + $.unblockUI(); + }); + $.blockUI({ + message: $msg, + css: { + fontFamily: 'monospace', + textAlign: 'left', + whiteSpace: 'pre-wrap', + cursor: 'default' + } + }); + } + + QUnit.test(name, function () { + var instance; + if (!opts.dependencies) { + instance = openerp.init(module_deps); + } else { + // empty-but-specified dependencies actually allow running + // without loading any module into the instance + + // TODO: clean up this mess + var d = opts.dependencies.slice(); + // dependencies list should be in deps order, reverse to make + // loading order from last + d.reverse(); + var di = 0; + while (di < d.length) { + var m = /^web\.(\w+)$/.exec(d[di]); + if (m) { + d[di] = m[1]; + } + d.splice.apply(d, [di+1, 0].concat( + _(dependencies[d[di]]).reverse())); + ++di; + } + + instance = openerp.init("fuck your shit, don't load anything you cunt"); + _(d).chain() + .reverse() + .uniq() + .each(function (module) { + openerp.web[module](instance); + }); + } + if (_.isNumber(opts.asserts)) { + expect(opts.asserts); + } + + if (opts.templates) { + for(var i=0; i<module_deps.length; ++i) { + var dep = module_deps[i]; + var templates = testing.templates[dep]; + if (_.isEmpty(templates)) { continue; } + + for (var j=0; j < templates.length; ++j) { + instance.web.qweb.add_template(templates[j]); + } + } + } + + var $fixture = $('#qunit-fixture'); + + var mock, async = false; + switch (opts.rpc) { + case 'mock': + async = true; + testing.mockifyRPC(instance); + mock = function (spec, handler) { + instance.session.responses[spec] = handler; + }; + break; + case 'rpc': + async = true; + (function () { + // Bunch of random base36 characters + var dbname = 'test_' + Math.random().toString(36).slice(2); + // Add db setup/teardown at the start of the stack + case_stack = case_stack.unshift(function (instance) { + // FIXME hack: don't want the session to go through shitty loading process of everything + instance.session.session_init = testing.noop; + instance.session.load_modules = testing.noop; + instance.session.session_bind(); + return instance.session.rpc('/web/database/duplicate', { + fields: [ + {name: 'super_admin_pwd', value: db.supadmin}, + {name: 'db_original_name', value: db.source}, + {name: 'db_name', value: dbname} + ] + }).then(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return instance.session.session_authenticate( + dbname, 'admin', db.password, true); + }); + }, function (instance) { + return instance.session.rpc('/web/database/drop', { + fields: [ + {name: 'drop_pwd', value: db.supadmin}, + {name: 'drop_db', value: dbname} + ] + }).then(function (result) { + if (result.error) { + return $.Deferred().reject(result.error).promise(); + } + return result; + }); + }); + })(); + } + + // Always execute tests asynchronously + stop(); + var timeout; + case_stack.execute(function () { + var result = callback.apply(null, arguments); + if (!(result && _.isFunction(result.then))) { + if (async) { + ok(false, "asynchronous test cases must return a promise"); + } + } else { + if (!_.isNumber(opts.asserts)) { + ok(false, "asynchronous test cases must specify the " + + "number of assertions they expect"); + } + } + + return $.Deferred(function (d) { + $.when(result).then(function () { + d.resolve.apply(d, arguments) + }, function () { + d.reject.apply(d, arguments); + }); + if (async || (result && result.then)) { + // async test can be either implicit async (rpc) or + // promise-returning + timeout = setTimeout(function () { + QUnit.config.semaphore = 1; + d.reject({message: "Test timed out"}); + }, 2000); + } + }); + }, instance, $fixture, mock).always(function () { + if (timeout) { clearTimeout(timeout); } + start(); + }).fail(function (error) { + if (options.fail_on_rejection === false) { + return; + } + var message; + if (typeof error !== 'object' + || typeof error.message !== 'string') { + message = JSON.stringify([].slice.apply(arguments)); + } else { + message = error.message; + if (error.data && error.data.debug) { + message += '\n\n' + error.data.debug; + } + } + + ok(false, message); + }); + }); + }; +})(openerp.testing); diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 906f1d8fe36..dad622fb7f9 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -173,10 +173,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } else { this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); } - this.$buttons.on('click', '.oe_form_button_create', this.on_button_create); - this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit); - this.$buttons.on('click', '.oe_form_button_save', this.on_button_save); - this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel); + this.$buttons.on('click', '.oe_form_button_create', + this.guard_active(this.on_button_create)); + this.$buttons.on('click', '.oe_form_button_edit', + this.guard_active(this.on_button_edit)); + this.$buttons.on('click', '.oe_form_button_save', + this.guard_active(this.on_button_save)); + this.$buttons.on('click', '.oe_form_button_cancel', + this.guard_active(this.on_button_cancel)); if (this.options.footer_to_buttons) { this.$el.find('footer').appendTo(this.$buttons); } @@ -618,6 +622,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return self._process_save(save_obj).then(function() { save_obj.ret = _.toArray(arguments); return iterate(); + }, function() { + save_obj.error = true; }); } return $.when(); @@ -801,6 +807,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var save_obj = {prepend_on_create: prepend_on_create, ret: null}; this.save_list.push(save_obj); return this._process_operations().then(function() { + if (save_obj.error) + return $.Deferred().reject(); return $.when.apply($, save_obj.ret); }); }, @@ -1187,14 +1195,36 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }); } }, + view_arch_to_dom_node: function(arch) { + // Historic mess for views arch + // + // server: + // -> got xml as string + // -> parse to xml and manipulate domains and contexts + // -> convert to json + // client: + // -> got view as json + // -> convert back to xml as string + // -> parse it as xml doc (manipulate button@type for IE) + // -> convert back to string + // -> parse it as dom element with jquery + // -> for each widget, convert node to json + // + // Wow !!! + var xml = instance.web.json_node_to_xml(arch); + + var doc = $.parseXML('<div class="oe_form">' + xml + '</div>'); + $('button', doc).each(function() { + $(this).attr('data-button-type', $(this).attr('type')); + }); + xml = instance.web.xml_to_str(doc); + return $(xml); + }, render_to: function($target) { var self = this; this.$target = $target; - // TODO: I know this will save the world and all the kitten for a moment, - // but one day, we will have to get rid of xml2json - var xml = instance.web.json_node_to_xml(this.fvg.arch); - this.$form = $('<div class="oe_form">' + xml + '</div>'); + this.$form = this.view_arch_to_dom_node(this.fvg.arch); this.process_version(); @@ -1863,6 +1893,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ template: 'WidgetButton', init: function(field_manager, node) { + node.attrs.type = node.attrs['data-button-type']; this._super(field_manager, node); this.force_disabled = false; this.string = (this.node.attrs.string || '').replace(/_/g, ''); @@ -2152,17 +2183,10 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w }, set_dimensions: function (height, width) { - // remove width css property - this.$el.css('width', ''); - // extract style (without width) - var old_style = this.$el.attr('style'); - // jQuery doesn't understand/use !important - var style = 'width:' + width + 'px !important;'; - if (old_style) { - style += old_style - } - this.$el.attr('style', style); - this.$el.css('minHeight', height); + this.$el.css({ + width: width, + minHeight: height + }); }, commit_value: function() { return $.when(); @@ -2386,7 +2410,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ self.$input.focus(); return; } - self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); + self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); self.$input_picker.show(); self.picker('show'); self.$input_picker.hide(); @@ -2639,22 +2663,18 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ } }); +/** + The progressbar field expect a float from 0 to 100. +*/ instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ template: 'FieldProgressBar', - start: function() { - this._super.apply(this, arguments); + render_value: function() { this.$el.progressbar({ - value: this.get('value'), + value: this.get('value') || 0, disabled: this.get("effective_readonly") }); - }, - render_value: function() { - var show_value = Number(this.get('value')); - if (isNaN(show_value)) { - show_value = 0; - } - var formatted_value = instance.web.format_value(show_value, { type : 'float' }, '0'); - this.$el.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); + var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' }); + this.$('span').html(formatted_value + '%'); } }); @@ -3106,7 +3126,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc minLength: 0, delay: 0 }); - this.$input.autocomplete("widget").addClass("openerp"); + this.$input.autocomplete("widget").openerpClass(); // used to correct a bug when selecting an element by pushing 'enter' in an editable list this.$input.keyup(function(e) { if (e.which === 13) { // ENTER @@ -3817,26 +3837,13 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this._super.apply(this, arguments); }, do_delete: function (ids) { - var self = this; - var next = $.when(); - var _super = this._super; - // handle deletion of an item which does not exist - // TODO: better handle that in the editable list? - var false_id_index = _(ids).indexOf(false); - if (false_id_index !== -1) { - ids.splice(false_id_index, 1); - next = this.cancel_edition(true); - } - return next.then(function () { - // wheeee - var confirm = window.confirm; - window.confirm = function () { return true; }; - try { - return _super.call(self, ids); - } finally { - window.confirm = confirm; - } - }); + var confirm = window.confirm; + window.confirm = function () { return true; }; + try { + return this._super(ids); + } finally { + window.confirm = confirm; + } } }); instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({ @@ -3999,13 +4006,18 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in self._drop_shown = true; }); self.tags = self.$text.textext()[0].tags(); - self.$text.focusout(function() { - self.$text.trigger("setInputData", ""); - }).keydown(function(e) { - if (e.which === $.ui.keyCode.TAB && self._drop_shown) { - self.$text.textext()[0].autocomplete().selectFromDropdown(); - } - }); + self.$text + .focusin(function () { + self.trigger('focused'); + }) + .focusout(function() { + self.$text.trigger("setInputData", ""); + self.trigger('blurred'); + }).keydown(function(e) { + if (e.which === $.ui.keyCode.TAB && self._drop_shown) { + self.$text.textext()[0].autocomplete().selectFromDropdown(); + } + }); }, set_value: function(value_) { value_ = value_ || []; @@ -4794,15 +4806,6 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); }, - human_filesize : function(size) { - var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - var i = 0; - while (size >= 1024) { - size /= 1024; - ++i; - } - return size.toFixed(2) + ' ' + units[i]; - }, on_file_change: function(e) { var self = this; var file_node = e.target; @@ -4850,6 +4853,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. link.href = "data:application/octet-stream;base64," + value; } else { instance.web.blockUI(); + var c = instance.webclient.crashmanager; this.session.get_file({ url: '/web/binary/saveas_ajax', data: {data: JSON.stringify({ @@ -4860,7 +4864,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. context: this.view.dataset.get_context() })}, complete: instance.web.unblockUI, - error: instance.webclient.crashmanager.on_rpc_error + error: c.rpc_error.bind(c) }); ev.stopPropagation(); return false; @@ -4919,7 +4923,7 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { this.binary_value = true; this.internal_set_value(file_base64); - var show_value = name + " (" + this.human_filesize(size) + ")"; + var show_value = name + " (" + instance.web.human_size(size) + ")"; this.$el.find('input').eq(0).val(show_value); this.set_filename(name); }, @@ -4939,12 +4943,16 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) { url = 'data:image/png;base64,' + this.get('value'); } else if (this.get('value')) { - var id = escape(JSON.stringify(this.view.datarecord.id || null)); + var id = JSON.stringify(this.view.datarecord.id || null); var field = this.name; if (this.options.preview_image) field = this.options.preview_image; - url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + - this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime()); + url = this.session.url('/web/binary/image', { + model: this.view.dataset.model, + id: id, + field: field, + t: (new Date().getTime()), + }); } else { url = this.placeholder; } @@ -4983,14 +4991,14 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ * Options on attribute ; "blockui" {Boolean} block the UI or not * during the file is uploading */ -instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ +instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ template: "FieldBinaryFileUploader", init: function(field_manager, node) { this._super(field_manager, node); this.field_manager = field_manager; this.node = node; - if(this.field.type != "one2many" || this.field.relation != 'ir.attachment') { - throw "The type of the field '"+this.field.string+"' must be a one2many field with a relation to 'ir.attachment' model."; + if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') { + throw "The type of the field '"+this.field.string+"' must be a many2many field with a relation to 'ir.attachment' model."; } this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); this.fileupload_id = _.uniqueId('oe_fileupload_temp'); @@ -5000,21 +5008,85 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel this._super(this); this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); }, + set_value: function(value_) { + var value_ = value_ || []; + var self = this; + var ids = []; + _.each(value_, function(command) { + if (isNaN(command) && command.id == undefined) { + switch (command[0]) { + case commands.CREATE: + ids = ids.concat(command[2]); + return; + case commands.REPLACE_WITH: + ids = ids.concat(command[2]); + return; + case commands.UPDATE: + ids = ids.concat(command[2]); + return; + case commands.LINK_TO: + ids = ids.concat(command[1]); + return; + case commands.DELETE: + ids = _.filter(ids, function (id) { return id != command[1];}); + return; + case commands.DELETE_ALL: + ids = []; + return; + } + } else { + ids.push(command); + } + }); + this._super( ids ); + }, get_value: function() { return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); }); }, get_file_url: function (attachment) { - return instance.origin + '/web/binary/saveas?session_id=' + this.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id']; + return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']}); + }, + read_name_values : function () { + var self = this; + // select the list of id for a get_name + var values = []; + _.each(this.get('value'), function (val) { + if (typeof val != 'object') { + values.push(val); + } + }); + // send request for get_name + if (values.length) { + return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) { + _.each(datas, function (data) { + data.no_unlink = true; + data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id}); + + _.each(self.get('value'), function (val, key) { + if(val == data.id) { + self.get('value')[key] = data; + } + }); + }); + }); + } else { + return $.when(this.get('value')); + } }, render_value: function () { - var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this})); - render.on('click', '.oe_delete', _.bind(this.on_file_delete, this)); - this.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + var self = this; + this.read_name_values().then(function (datas) { - // reinit input type file - var $input = this.$('input.oe_form_binary_file'); - $input.after($input.clone(true)).remove(); - this.$(".oe_fileupload").show(); + var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self})); + render.on('click', '.oe_delete', _.bind(self.on_file_delete, self)); + self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + + // reinit input type file + var $input = self.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + self.$(".oe_fileupload").show(); + + }); }, on_file_change: function (event) { event.stopPropagation(); @@ -5030,7 +5102,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel } // block UI or not - if(this.node.attrs.blockui) { + if(this.node.attrs.blockui>0) { instance.web.blockUI(); } @@ -5064,7 +5136,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel }, on_file_loaded: function (event, result) { // unblock UI - if(this.node.attrs.blockui) { + if(this.node.attrs.blockui>0) { instance.web.unblockUI(); } @@ -5094,7 +5166,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel if(file_id != this.get('value')[i].id){ files.push(this.get('value')[i]); } - else { + else if(!this.get('value')[i].no_unlink) { this.ds_file.unlink([file_id]); } } @@ -5256,7 +5328,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'progressbar': 'instance.web.form.FieldProgressBar', 'image': 'instance.web.form.FieldBinaryImage', 'binary': 'instance.web.form.FieldBinaryFile', - 'one2many_binary': 'instance.web.form.FieldOne2ManyBinaryMultiFiles', + 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles', 'statusbar': 'instance.web.form.FieldStatus', 'monetary': 'instance.web.form.FieldMonetary', }); diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 03a48297eca..df74674a343 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -1,6 +1,6 @@ openerp.web.list = function (instance) { var _t = instance.web._t, - _lt = instance.web._lt; + _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.web.views.add('list', 'instance.web.ListView'); instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListView# */ { @@ -503,12 +503,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi return this.reload_content(); }, reload_record: function (record) { + var self = this; return this.dataset.read_ids( [record.get('id')], _.pluck(_(this.columns).filter(function (r) { return r.tag === 'field'; }), 'name') ).done(function (records) { + if (!records[0]) { + self.records.remove(record); + return; + } _(records[0]).each(function (value, key) { record.set(key, value, {silent: true}); }); @@ -1557,9 +1562,8 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } }); -var DataGroup = instance.web.CallbackEnabled.extend({ +var DataGroup = instance.web.Class.extend({ init: function(parent, model, domain, context, group_by, level) { - this._super(parent, null); this.model = new instance.web.Model(model, context, domain); this.group_by = group_by; this.context = context; @@ -2152,7 +2156,7 @@ instance.web.list.Boolean = instance.web.list.Column.extend({ * @private */ _format: function (row_data, options) { - return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', + return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>', row_data[this.id].value ? 'checked="checked"' : ''); } }); @@ -2164,20 +2168,24 @@ instance.web.list.Binary = instance.web.list.Column.extend({ */ _format: function (row_data, options) { var text = _t("Download"); - var download_url = _.str.sprintf( - '/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', - instance.session.session_id, options.model, this.id, options.id); - if (this.filename) { - download_url += '&filename_field=' + this.filename; - if (row_data[this.filename]) { - text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( - row_data[this.filename].value, {type: 'char'})); + var value = row_data[this.id].value; + var download_url; + if (value && value.substr(0, 10).indexOf(' ') == -1) { + download_url = "data:application/octet-stream;base64," + value; + } else { + download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id}); + if (this.filename) { + download_url += '&filename_field=' + this.filename; } } - return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { + if (this.filename && row_data[this.filename]) { + text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( + row_data[this.filename].value, {type: 'char'})); + } + return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', { text: text, href: download_url, - size: row_data[this.id].value + size: instance.web.binary_to_binsize(value), }); } }); @@ -2217,7 +2225,7 @@ instance.web.list.Many2OneButton = instance.web.list.Column.extend({ }); instance.web.list.Many2Many = instance.web.list.Column.extend({ _format: function (row_data, options) { - if (row_data[this.id].value) { + if (!_.isEmpty(row_data[this.id].value)) { // If value, use __display version for printing row_data[this.id] = row_data[this.id + '__display']; } diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index 37ce8c4f8b4..1abe3caab39 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -77,6 +77,15 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, + do_delete: function (ids) { + var _super = this._super.bind(this); + var next = this.editor.is_editing() + ? this.cancel_edition(true) + : $.when(); + return next.then(function () { + return _super(ids); + }); + }, editable: function () { return this.fields_view.arch.attrs.editable || this._context_editable @@ -117,13 +126,6 @@ openerp.web.list_editable = function (instance) { e.preventDefault(); self.cancel_edition(); }); - this.$el - .off('click', 'tbody td:not(.oe_list_field_cell)') - .on('click', 'tbody td:not(.oe_list_field_cell)', function () { - if (!self.editor.is_editing()) { - self.start_edition(); - } - }); this.editor.destroy(); // Editor is not restartable due to formview not being // restartable @@ -164,13 +166,23 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - var self = this; - return this.saving_mutex.exec(function() { - if (!self.editor.is_editing()) { - return $.when(); - } - return self.save_edition(); - }); + return this.save_edition(); + }, + /** + * Builds a record with the provided id (``false`` for a creation), + * setting all columns with ``false`` value so code which relies on + * having an actual value behaves correctly + * + * @param {*} id + * @return {instance.web.list.Record} + */ + make_empty_record: function (id) { + var attrs = {id: id}; + _(this.columns).chain() + .filter(function (x) { return x.tag === 'field'}) + .pluck('name') + .each(function (field) { attrs[field] = false; }); + return new instance.web.list.Record(attrs); }, /** * Set up the edition of a record of the list view "inline" @@ -186,12 +198,7 @@ openerp.web.list_editable = function (instance) { if (record) { item = record.attributes; } else { - var attrs = {id: false}; - _(this.columns).chain() - .filter(function (x) { return x.tag === 'field'}) - .pluck('name') - .each(function (field) { attrs[field] = false; }); - record = new instance.web.list.Record(attrs); + record = this.make_empty_record(false); this.records.add(record, { at: this.prepends_on_create() ? 0 : null}); } @@ -269,29 +276,34 @@ openerp.web.list_editable = function (instance) { */ save_edition: function () { var self = this; - return this.with_event('save', { - editor: this.editor, - form: this.editor.form, - cancel: false - }, function () { - return this.editor.save().then(function (attrs) { - var created = false; - var record = self.records.get(attrs.id); - if (!record) { - // new record - created = true; - record = self.records.find(function (r) { - return !r.get('id'); - }).set('id', attrs.id); - } - // onwrite callback could be altering & reloading the - // record which has *just* been saved, so first perform all - // onwrites then do a final reload of the record - return self.handle_onwrite(record) - .then(function () { - return self.reload_record(record); }) - .then(function () { - return { created: created, record: record }; }); + return self.saving_mutex.exec(function() { + if (!self.editor.is_editing()) { + return $.when(); + } + return self.with_event('save', { + editor: self.editor, + form: self.editor.form, + cancel: false + }, function () { + return self.editor.save().then(function (attrs) { + var created = false; + var record = self.records.get(attrs.id); + if (!record) { + // new record + created = true; + record = self.records.find(function (r) { + return !r.get('id'); + }).set('id', attrs.id); + } + // onwrite callback could be altering & reloading the + // record which has *just* been saved, so first perform all + // onwrites then do a final reload of the record + return self.handle_onwrite(record) + .then(function () { + return self.reload_record(record); }) + .then(function () { + return { created: created, record: record }; }); + }); }); }); }, @@ -391,7 +403,7 @@ openerp.web.list_editable = function (instance) { if (!record) { // insert after the source record var index = this.records.indexOf(source_record) + 1; - record = new instance.web.list.Record({id: id}); + record = this.make_empty_record(id); this.records.add(record, {at: index}); this.dataset.ids.splice(index, 0, id); } diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 53132e454d1..641821152b2 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -213,8 +213,12 @@ instance.web.ActionManager = instance.web.Widget.extend({ if (run_action) { this.null_action(); action_loaded = this.do_action(state.action); - instance.webclient.menu.has_been_loaded.done(function() { - instance.webclient.menu.open_action(state.action); + $.when(action_loaded || null).done(function() { + instance.webclient.menu.has_been_loaded.done(function() { + if (self.inner_action && self.inner_action.id) { + instance.webclient.menu.open_action(self.inner_action.id); + } + }); }); } } @@ -306,15 +310,15 @@ instance.web.ActionManager = instance.web.Widget.extend({ } var widget = executor.widget(); if (executor.action.target === 'new') { - if (this.dialog_widget && ! this.dialog_widget.isDestroyed()) + if (this.dialog_widget && !this.dialog_widget.isDestroyed()) { this.dialog_widget.destroy(); - if (this.dialog === null || this.dialog.isDestroyed()) { - this.dialog = new instance.web.Dialog(this, { - dialogClass: executor.klass, - }); - this.dialog.on("closing", null, options.on_close); - this.dialog.init_dialog(); } + this.dialog_stop(); + this.dialog = new instance.web.Dialog(this, { + dialogClass: executor.klass, + }); + this.dialog.on("closing", null, options.on_close); + this.dialog.init_dialog(); this.dialog.dialog_title = executor.action.name; if (widget instanceof instance.web.ViewManager) { _.extend(widget.flags, { @@ -397,6 +401,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ }).done(function(res) { action = _.clone(action); action.context = res.context; + var c = instance.webclient.crashmanager; self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, @@ -407,7 +412,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ } self.dialog_stop(); }, - error: instance.webclient.crashmanager.on_rpc_error + error: c.rpc_error.bind(c) }) }); }, @@ -504,7 +509,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') .parent().addClass('active'); - r = $.when(view_promise).done(function () { + return $.when(view_promise).done(function () { _.each(_.keys(self.views), function(view_name) { var controller = self.views[view_name].controller; if (controller) { @@ -520,7 +525,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ }); self.trigger('switch_mode', view_type, no_store, view_options); }); - return r; }, do_create_view: function(view_type) { // Lazy loading of views @@ -785,8 +789,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ name: "JS Tests", target: 'new', type : 'ir.actions.act_url', - url: '/web/static/test/test.html' - }) + url: '/web/tests?mod=*' + }); break; case 'perm_read': var ids = current_view.get_selected_ids(); @@ -1098,11 +1102,11 @@ instance.web.Sidebar = instance.web.Widget.extend({ on_attachments_loaded: function(attachments) { var self = this; var items = []; - var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; + var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'}); _.each(attachments,function(a) { a.label = a.name; if(a.type === "binary") { - a.url = prefix + a.id + '&t=' + (new Date().getTime()); + a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime()); } }); self.items['files'] = attachments; @@ -1235,11 +1239,12 @@ instance.web.View = instance.web.Widget.extend({ }); }, null); } else { + self.do_action({"type":"ir.actions.act_window_close"}); return result_handler(); } }; - if (action_data.special) { + if (action_data.special === 'cancel') { return handler({"type":"ir.actions.act_window_close"}); } else if (action_data.type=="object") { var args = [[record_id]], additional_args = []; @@ -1277,6 +1282,27 @@ instance.web.View = instance.web.Widget.extend({ do_hide: function () { this.$el.hide(); }, + is_active: function () { + var manager = this.getParent(); + return !manager || !manager.active_view + || manager.views[manager.active_view].controller === this; + }, /** + * Wraps fn to only call it if the current view is the active one. If the + * current view is not active, doesn't call fn. + * + * fn can not return anything, as a non-call to fn can't return anything + * either + * + * @param {Function} fn function to wrap in the active guard + */ + guard_active: function (fn) { + var self = this; + return function () { + if (self.is_active()) { + fn.apply(self, arguments); + } + } + }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { this.getParent().do_push_state(state); @@ -1384,14 +1410,16 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { } else { return r + '/>'; } -} +}; instance.web.xml_to_str = function(node) { - if (window.ActiveXObject) { + if (window.XMLSerializer) { + return (new XMLSerializer()).serializeToString(node); + } else if (window.ActiveXObject) { return node.xml; } else { - return (new XMLSerializer()).serializeToString(node); + throw new Error("Could not serialize XML"); } -} +}; instance.web.str_to_xml = function(s) { if (window.DOMParser) { var dp = new DOMParser(); |
