diff options
| author | Christophe Simonis <chs@openerp.com> | 2012-11-14 23:35:38 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@openerp.com> | 2012-11-14 23:35:38 +0100 |
| commit | a161d749f8bf3fe86f9553d249a34fe05acd44e0 (patch) | |
| tree | 63d06db3751b3571a1c644a13303b890594d627d /addons/web/static/src | |
| parent | 28f7f46a5ced9d6a0ac7e68a64f1f9849422c465 (diff) | |
| parent | eb5a5bf838fdeac8564802f3b08fbfad6066e8e7 (diff) | |
merge upstream
bzr revid: chs@openerp.com-20121114223538-qp233873r33uh3e3
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/css/base.css | 39 | ||||
| -rw-r--r-- | addons/web/static/src/css/base.sass | 28 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 26 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 273 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 51 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 13 | ||||
| -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/view_form.js | 125 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 36 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 91 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 49 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 89 |
13 files changed, 609 insertions, 383 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index b34e699b541..a421336c43f 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -2316,16 +2316,16 @@ text-align: justify; } .openerp .oe_form_editable .oe_form .oe_form_field_integer input { - width: 6em !important; + width: 6em; } .openerp .oe_form_editable .oe_form .oe_form_field_float input { - width: 7em !important; + width: 7em; } .openerp .oe_form_editable .oe_form .oe_form_field_date input { - width: 7.5em !important; + width: 7.5em; } .openerp .oe_form_editable .oe_form .oe_form_field_datetime input { - width: 11.5em !important; + width: 11.5em; } .openerp .oe_hidden_input_file { position: relative; @@ -2384,6 +2384,9 @@ float: right; padding-left: 2px; } +.openerp .oe_form_field_many2one input { + padding-right: 13px; +} .openerp.ui-autocomplete li.oe_m2o_dropdown_option a { font-style: italic; padding-left: 2em; @@ -2588,32 +2591,33 @@ top: 5px; } .openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_cm_button { - display: none; + line-height: 19px; } -.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { - height: 27px; +.openerp .oe_list.oe_list_editable.oe_editing .oe_input_icon { + margin-top: 5px; +} +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field { + min-width: 0; + max-width: none; } .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea { + height: 27px; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; border: 1px solid #aaaaff; margin: 0; } +.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field select { + min-width: 0; +} .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input { text-align: right; width: 100% !important; } -.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime > span, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date > span { - width: 100% !important; -} .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master { width: 100% !important; } -.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_field_float, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_view_integer, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date { - min-width: 0 !important; - max-width: none !important; -} .openerp .oe_list_group_name { white-space: nowrap; } @@ -2678,6 +2682,9 @@ text-align: right !important; max-width: 100px; } +.openerp .oe_list_content td.oe_list_field_date, .openerp .oe_list_content th.oe_list_header_date { + min-width: 6em; +} .openerp .oe_list_content > thead { border-bottom: 2px solid #cacaca; background: #eeeeee; @@ -2745,6 +2752,10 @@ .openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child:after { border-width: 0; } +.openerp .oe_list_content > tbody > tr > td.oe_list_field_boolean input { + filter: alpha(opacity=50); + opacity: 0.5; +} .openerp .oe_list_content > tbody > tr:nth-child(odd) { background-color: #f0f0fa; background-color: #f0f0fa; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 368df7f047c..596426917b5 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1846,13 +1846,13 @@ $sheet-padding: 16px .oe_form_editable .oe_form .oe_form_field_integer input - width: 6em !important + width: 6em .oe_form_field_float input - width: 7em !important + width: 7em .oe_form_field_date input - width: 7.5em !important + width: 7.5em .oe_form_field_datetime input - width: 11.5em !important + width: 11.5em // }}} // FormView.fields_binary {{{ /* http://www.quirksmode.org/dom/inputfile.html @@ -1908,6 +1908,8 @@ $sheet-padding: 16px line-height: 14px float: right padding-left: 2px + input + padding-right: 13px &.ui-autocomplete li.oe_m2o_dropdown_option a font-style: italic @@ -2050,26 +2052,26 @@ $sheet-padding: 16px .oe_m2o_drop_down_button top: 5px .oe_m2o_cm_button - display: none + line-height: 19px + .oe_input_icon + margin-top: 5px .oe_form_field + min-width: 0 + max-width: none input, textarea height: $row-height - input, textarea @include radius(0) border: 1px solid #aaf margin: 0 + input, textarea, select + min-width: 0 &.oe_form_field_float,&.oe_form_view_integer input text-align: right width: 100% !important &.oe_form_field_datetime,&.oe_form_field_date - > span - width: 100% !important input.oe_datepicker_master width: 100% !important - .oe_form_field_float,.oe_form_view_integer,&.oe_form_field_datetime,&.oe_form_field_date - min-width: 0 !important - max-width: none !important .oe_list_group_name white-space: nowrap // }}} @@ -2128,6 +2130,8 @@ $sheet-padding: 16px td.oe_number text-align: right !important max-width: 100px + td.oe_list_field_date, th.oe_list_header_date + min-width: 6em > thead border-bottom: 2px solid #cacaca background: #eee @@ -2177,6 +2181,8 @@ $sheet-padding: 16px width: 17px &:after border-width: 0 + > td.oe_list_field_boolean input + @include opacity() > tr:nth-child(odd) background-color: #f0f0fa @include vertical-gradient(#f0f0fa, #eeeef6) diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index f103828251a..c48c231bb99 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -189,7 +189,7 @@ instance.web.Dialog = instance.web.Widget.extend({ } }); -instance.web.CrashManager = instance.web.CallbackEnabled.extend({ +instance.web.CrashManager = instance.web.Class.extend({ rpc_error: function(error) { if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); @@ -303,7 +303,7 @@ 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(); @@ -333,6 +333,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 +419,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 +531,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 +564,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"); @@ -911,7 +927,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); }); }; @@ -1130,7 +1146,7 @@ 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 === undefined && state.menu_id) { self.menu.has_been_loaded.done(function() { self.menu.do_reload().done(function() { self.menu.menu_click(state.menu_id) diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index d2e94d37939..6ea0fd53372 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. @@ -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); }, @@ -1311,7 +1276,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 +1309,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 +1335,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 +1373,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 76e2bf8002c..65b1874e093 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) { @@ -59,7 +62,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(); }); } @@ -161,28 +164,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'); @@ -196,29 +197,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(); }); }; @@ -229,7 +236,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() { diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 2f8679794b0..9162d0066c2 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -358,17 +358,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 +721,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/view_form.js b/addons/web/static/src/js/view_form.js index 906f1d8fe36..867569e2ea5 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); } @@ -1187,14 +1191,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 +1889,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 +2179,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 +2406,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 +2659,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 + '%'); } }); @@ -3999,13 +4015,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 +4815,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 +4862,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 +4873,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 +4932,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); }, @@ -4943,8 +4956,12 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ 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; } @@ -5004,7 +5021,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel 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']}); }, render_value: function () { var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this})); @@ -5030,7 +5047,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 +5081,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(); } diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 03a48297eca..db8c962efc7 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -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..8bcf8b5d036 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -117,13 +117,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 +157,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 +189,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 +267,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 +394,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 c779ac115c3..c9c1f918114 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); + } + }); }); } } @@ -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 @@ -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; @@ -1277,6 +1281,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 +1409,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(); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 553543f9d0a..a2e7a59a989 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -152,6 +152,32 @@ </tr> </table> </form> + <form id="db_duplicate" name="duplicate_db_form" style="display: none;"> + <div class="oe_view_manager oe_view_manager_current"> + <div class="oe_view_manager_header" style="padding: 8px;"> + <div class="oe_header_row"> + <h2 class="oe_view_title"> + <span class="oe_view_title_text oe_breadcrumb_title">Duplicate Database</span> + </h2> + <button type="submit" class="oe_button oe_highlight db_duplicate">Duplicate</button> + </div> + </div> + </div> + <table align="center" class="db_option_table"> + <tr> + <td><label for="super_admin_pwd">Master password:</label></td> + <td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td> + </tr> + <tr> + <td><label for="db_original_name">Original database name:</label></td> + <td><input type="text" name="db_original_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" autofocus="true"/></td> + </tr> + <tr> + <td><label for="db_name">New database name:</label></td> + <td><input type="text" name="db_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" /></td> + </tr> + </table> + </form> <form id="db_drop" name="drop_db_form" style="display: none; "> <div class="oe_view_manager oe_view_manager_current"> <div class="oe_view_manager_header" style="padding: 8px;"> @@ -279,6 +305,7 @@ <div class="oe_secondary_menu_section">Database Management</div> <ul class="oe_secondary_submenu"> <li><a href="#db_create">Create</a></li> + <li><a href="#db_duplicate">Duplicate</a></li> <li><a href="#db_drop">Drop</a></li> <li><a href="#db_backup">Backup</a></li> <li><a href="#db_restore">Restore</a></li> @@ -1182,26 +1209,28 @@ </t> <t t-name="FieldBinaryFileUploader.files"> <div class="oe_attachments"> - <t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> - <div class="oe_attachment"> - <span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}"> - <span class="oe_fileuploader_in_process">...Upload in progress...</span> - <t t-raw="file.name || file.filename"/> - </span> - <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> - <t t-raw="file.name || file.filename"/> - </a> - <t t-if="(!file.upload or file.percent_loaded>=100)"> - <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a> - </t> - </div> - </t> - <t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> - <div> - <a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> - <t t-raw="file.name || file.filename"/> - </a> - </div> + <t t-if="widget.get('value')"> + <t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> + <div class="oe_attachment"> + <span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}"> + <span class="oe_fileuploader_in_process">...Upload in progress...</span> + <t t-raw="file.name || file.filename"/> + </span> + <a t-if="(!file.upload or file.percent_loaded>=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> + <t t-raw="file.name || file.filename"/> + </a> + <t t-if="(!file.upload or file.percent_loaded>=100)"> + <a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a> + </t> + </div> + </t> + <t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file"> + <div> + <a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> + <t t-raw="file.name || file.filename"/> + </a> + </div> + </t> </t> </div> </t> @@ -1512,17 +1541,19 @@ <h3><span class="oe_i">M</span> Custom Filters</h3> <ul class="oe_searchview_custom_list"/> <div class="oe_searchview_custom"> - <h4>Save current filter</h4> - <form> - <p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p> - <p><input id="oe_searchview_custom_public" type="checkbox"/> - <label for="oe_searchview_custom_public">Share with all users</label></p> - <button>Save</button> - </form> + <h4>Save current filter</h4> + <form> + <p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p> + <p> + <input id="oe_searchview_custom_public" type="checkbox"/> + <label for="oe_searchview_custom_public">Share with all users</label> + <input id="oe_searchview_custom_default" type="checkbox"/> + <label for="oe_searchview_custom_default">Use by default</label> + </p> + <button>Save</button> + </form> </div> </div> - <div> - </div> </div> <div t-name="SearchView.advanced" class="oe_searchview_advanced"> |
