diff options
| author | Vishmita Jadeja (openerp) <vja@tinyerp.com> | 2013-05-01 11:11:29 +0530 |
|---|---|---|
| committer | Vishmita Jadeja (openerp) <vja@tinyerp.com> | 2013-05-01 11:11:29 +0530 |
| commit | 3b7863b479593fd6f2ccba8996f227f8d2e7df25 (patch) | |
| tree | 0ce0101d6938cc5217c2ab69b52126a369766b3e /addons/web/static/src/js | |
| parent | ae6d15de59251618a21d4520e50456fd9777de33 (diff) | |
| parent | 511efd43a90b6bbc30bac9764358fc81b42fb13a (diff) | |
[Merge]Merge with trunk
bzr revid: vja@tinyerp.com-20130501054129-2ie8kj73yosxbfo8
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 104 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 24 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 105 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 30 | ||||
| -rw-r--r-- | addons/web/static/src/js/pyeval.js | 22 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 226 | ||||
| -rw-r--r-- | addons/web/static/src/js/testing.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 509 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 9 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 3 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 54 |
12 files changed, 725 insertions, 369 deletions
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index eaa4bcbfe1c..4a89eee8984 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -251,9 +251,9 @@ instance.web.CrashManager = instance.web.Class.extend({ if (handler) { new (handler)(this, error).display(); return; - }; + } if (error.data.name === "openerp.addons.web.session SessionExpiredException") { - this.show_warning({type: "Session Expired", data: { message: "Your OpenERP session expired. Please refresh the current web page." }}); + this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }}); return; } if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" @@ -530,7 +530,11 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ 'login': 'admin', 'password': form_obj['create_admin_pwd'], 'login_successful': function() { - self.do_action("reload"); + var url = '/?db=' + form_obj['db_name']; + if (self.session.debug) { + url += '&debug'; + } + instance.web.redirect(url); }, }, _push_me: false, @@ -641,6 +645,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage instance.web.Login = instance.web.Widget.extend({ template: "Login", remember_credentials: true, + events: { + 'change input[name=db],select[name=db]': function(ev) { + this.set('database_selector', $(ev.currentTarget).val()); + }, + }, init: function(parent, action) { this._super(parent); @@ -652,18 +661,18 @@ instance.web.Login = instance.web.Widget.extend({ if (_.isEmpty(this.params)) { this.params = $.bbq.getState(true); } + if (action && action.params && action.params.db) { + this.params.db = action.params.db; + } else if ($.deparam.querystring().db) { + this.params.db = $.deparam.querystring().db; + } + if (this.params.db) { + this.selected_db = this.params.db; + } if (this.params.login_successful) { this.on('login_successful', this, this.params.login_successful); } - - if (this.has_local_storage && this.remember_credentials) { - this.selected_db = localStorage.getItem('last_db_login_success'); - this.selected_login = localStorage.getItem('last_login_login_success'); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - this.selected_password = localStorage.getItem('last_password_login_success'); - } - } }, start: function() { var self = this; @@ -671,10 +680,10 @@ instance.web.Login = instance.web.Widget.extend({ self.$el.find('.oe_login_manage_db').click(function() { self.do_action("database_manager"); }); + self.on('change:database_selector', this, function() { + this.database_selected(this.get('database_selector')); + }); var d = $.when(); - if ($.deparam.querystring().db) { - self.params.db = $.deparam.querystring().db; - } if ($.param.fragment().token) { self.params.token = $.param.fragment().token; } @@ -682,16 +691,44 @@ instance.web.Login = instance.web.Widget.extend({ if (self.params.db && self.params.login && self.params.password) { d = self.do_login(self.params.db, self.params.login, self.params.password); } else { - if (self.params.db) { - self.on_db_loaded([self.params.db]) - } else { - d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed); - } + d = self.rpc("/web/database/get_list", {}) + .done(self.on_db_loaded) + .fail(self.on_db_failed) + .always(function() { + if (self.selected_db && self.has_local_storage && self.remember_credentials) { + self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || ''); + if (self.session.debug) { + self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || ''); + } + } + }); } return d; }, + remember_last_used_database: function(db) { + // This cookie will be used server side in order to avoid db reloading on first visit + var ttl = 24 * 60 * 60 * 365; + document.cookie = [ + 'last_used_database=' + db, + 'path=/', + 'max-age=' + ttl, + 'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString() + ].join(';'); + }, + database_selected: function(db) { + var params = $.deparam.querystring(); + params.db = db; + this.remember_last_used_database(db); + this.$('.oe_login_dbpane').empty().text(_t('Loading...')); + this.$('[name=login], [name=password]').prop('readonly', true); + instance.web.redirect('/?' + $.param(params)); + }, on_db_loaded: function (result) { + var self = this; this.db_list = result; + if (!this.selected_db) { + this.selected_db = result[0]; + } 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"); @@ -732,17 +769,11 @@ instance.web.Login = instance.web.Widget.extend({ self.hide_error(); self.$(".oe_login_pane").fadeOut("slow"); return this.session.session_authenticate(db, login, password).then(function() { - if (self.has_local_storage) { - if(self.remember_credentials) { - localStorage.setItem('last_db_login_success', db); - localStorage.setItem('last_login_login_success', login); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - localStorage.setItem('last_password_login_success', password); - } - } else { - localStorage.setItem('last_db_login_success', ''); - localStorage.setItem('last_login_login_success', ''); - localStorage.setItem('last_password_login_success', ''); + self.remember_last_used_database(db); + if (self.has_local_storage && self.remember_credentials) { + localStorage.setItem(db + '|last_login', login); + if (self.session.debug) { + localStorage.setItem(db + '|last_password', password); } } self.trigger('login_successful'); @@ -800,6 +831,9 @@ instance.web.Reload = function(parent, action) { var sobj = $.deparam(l.search.substr(1)); sobj.ts = new Date().getTime(); + if (params.url_search) { + sobj = _.extend(sobj, params.url_search); + } var search = '?' + $.param(sobj); var hash = l.hash; @@ -1218,7 +1252,8 @@ instance.web.WebClient = instance.web.Client.extend({ $("body").addClass("kitten-mode-activated"); $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")"); if ($.blockUI) { - $.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">'; + var imgkit = Math.floor(Math.random() * 2 + 1); + $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">'; } } if (!self.session.session_is_valid()) { @@ -1303,7 +1338,7 @@ instance.web.WebClient = instance.web.Client.extend({ }, logo_edit: function(ev) { var self = this; - new self.alive(instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) { + self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) { self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) { result.res_id = res['company_id'][0]; result.target = "new"; @@ -1500,8 +1535,9 @@ instance.web.EmbeddedClient = instance.web.Client.extend({ }); }, - do_action: function(action) { - return this.action_manager.do_action(action); + do_action: function(/*...*/) { + var am = this.action_manager; + return am.do_action.apply(am, arguments); }, authenticate: function() { diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 63e22672bd7..c4bb81117f8 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -232,13 +232,17 @@ instance.web.ParentedMixin = { Utility method to only execute asynchronous actions if the current object has not been destroyed. - @param {Promise} promise The promise representing the asynchronous action. - @param {bool} reject Defaults to false. If true, the returned promise will be - rejected with no arguments if the current object is destroyed. If false, - the returned promise will never be resolved nor rejected. - @returns {Promise} A promise that will mirror the given promise if everything goes - fine but will either be rejected with no arguments or never resolved if the - current object is destroyed. + @param {$.Deferred} promise The promise representing the asynchronous + action. + @param {bool} [reject=false] If true, the returned promise will be + rejected with no arguments if the current + object is destroyed. If false, the + returned promise will never be resolved + or rejected. + @returns {$.Deferred} A promise that will mirror the given promise if + everything goes fine but will either be rejected + with no arguments or never resolved if the + current object is destroyed. */ alive: function(promise, reject) { var def = $.Deferred(); @@ -697,10 +701,10 @@ instance.web.Widget = instance.web.Controller.extend({ * Method called after rendering. Mostly used to bind actions, perform asynchronous * calls, etc... * - * By convention, the method should return a promise to inform the caller when - * this widget has been initialized. + * By convention, this method should return an object that can be passed to $.when() + * to inform the caller when this widget has been initialized. * - * @returns {jQuery.Deferred} + * @returns {jQuery.Deferred or any} */ start: function() { return $.when(); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 43d292b66db..dedb4a49710 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess }); }, session_is_valid: function() { + var db = $.deparam.querystring().db; + if (db && this.db !== db) { + return false; + } return !!this.uid; }, /** diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index bbff633c687..d60f72c6577 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -112,24 +112,27 @@ instance.web.Query = instance.web.Class.extend({ * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null} */ group_by: function (grouping) { - if (grouping === undefined) { - return null; + var ctx = instance.web.pyeval.eval( + 'context', this._model.context(this._context)); + + // undefined passed in explicitly (!) + if (_.isUndefined(grouping)) { + grouping = []; } if (!(grouping instanceof Array)) { grouping = _.toArray(arguments); } - if (_.isEmpty(grouping)) { return null; } + if (_.isEmpty(grouping) && !ctx['group_by_no_leaf']) { + return null; + } var self = this; - - var ctx = instance.web.pyeval.eval( - 'context', this._model.context(this._context)); return this._model.call('read_group', { groupby: grouping, fields: _.uniq(grouping.concat(this._fields || [])), domain: this._model.domain(this._filter), - context: this._model.context(this._context), + context: ctx, offset: this._offset, limit: this._limit, orderby: instance.web.serialize_sort(this._order_by) || false @@ -325,7 +328,7 @@ instance.web.Model = instance.web.Class.extend({ * Fetches the model's domain, combined with the provided domain if any * * @param {Array} [domain] to combine with the model's internal domain - * @returns The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain + * @returns {instance.web.CompoundDomain} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain */ domain: function (domain) { if (!domain) { return this._domain; } @@ -337,7 +340,7 @@ instance.web.Model = instance.web.Class.extend({ * combined with the provided context if any * * @param {Object} [context] to combine with the model's internal context - * @returns The union of the user's context and the model's internal context, as well as the provided context if any. In that order. + * @returns {instance.web.CompoundContext} The union of the user's context and the model's internal context, as well as the provided context if any. In that order. */ context: function (context) { return new instance.web.CompoundContext( @@ -481,9 +484,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, * Creates a new record in db * * @param {Object} data field values to set on the new record + * @param {Object} options Dictionary that can contain the following keys: + * - readonly_fields: Values from readonly fields that were updated by + * on_changes. Only used by the BufferedDataSet to make the o2m work correctly. * @returns {$.Deferred} */ - create: function(data) { + create: function(data, options) { return this._model.call('create', [data], {context: this.get_context()}); }, /** @@ -491,8 +497,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, * * @param {Number|String} id identifier for the record to alter * @param {Object} data field values to write into the record - * @param {Function} callback function called with operation result - * @param {Function} error_callback function called in case of write error + * @param {Object} options Dictionary that can contain the following keys: + * - context: The context to use in the server-side call. + * - readonly_fields: Values from readonly fields that were updated by + * on_changes. Only used by the BufferedDataSet to make the o2m work correctly. * @returns {$.Deferred} */ write: function (id, data, options) { @@ -599,6 +607,9 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, alter_ids: function(n_ids) { this.ids = n_ids; }, + remove_ids: function (ids) { + this.alter_ids(_(this.ids).difference(ids)); + }, /** * Resequence records. * @@ -696,22 +707,28 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({ get_domain: function (other_domain) { this._model.domain(other_domain); }, + alter_ids: function (ids) { + this._super(ids); + if (this.index !== null && this.index >= this.ids.length) { + this.index = this.ids.length > 0 ? this.ids.length - 1 : 0; + } + }, + remove_ids: function (ids) { + var before = this.ids.length; + this._super(ids); + if (this._length) { + this._length -= (before - this.ids.length); + } + }, unlink: function(ids, callback, error_callback) { var self = this; return this._super(ids).done(function(result) { - self.ids = _(self.ids).difference(ids); - if (self._length) { - self._length -= 1; - } - if (self.index !== null) { - self.index = self.index <= self.ids.length - 1 ? - self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); - } + self.remove_ids( ids); self.trigger("dataset_changed", ids, callback, error_callback); }); }, size: function () { - if (this._length !== undefined) { + if (this._length != null) { return this._length; } return this._super(); @@ -732,10 +749,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ self.last_default_get = res; }); }, - create: function(data) { - var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, - defaults: this.last_default_get}; - this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)})); + create: function(data, options) { + var cached = { + id:_.uniqueId(this.virtual_id_prefix), + values: _.extend({}, data, (options || {}).readonly_fields || {}), + defaults: this.last_default_get + }; + this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)})); this.cache.push(cached); return $.Deferred().resolve(cached.id).promise(); }, @@ -762,7 +782,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ cached = {id: id, values: {}}; this.cache.push(cached); } - $.extend(cached.values, record.values); + $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {})); if (dirty) this.trigger("dataset_changed", id, data, options); return $.Deferred().resolve(true).promise(); @@ -860,8 +880,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ } return completion.promise(); }, - call_button: function (method, args) { - var id = args[0][0], index; + /** + * Invalidates caching of a record in the dataset to ensure the next read + * of that record will hit the server. + * + * Of use when an action is going to remote-alter a record which will then + * need to be reloaded, e.g. action button. + * + * @param {Object} id record to remove from the BDS's cache + */ + evict_record: function (id) { for(var i=0, len=this.cache.length; i<len; ++i) { var record = this.cache[i]; // if record we call the button upon is in the cache @@ -871,8 +899,15 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ break; } } + }, + call_button: function (method, args) { + this.evict_record(args[0][0]); return this._super(method, args); }, + exec_workflow: function (id, signal) { + this.evict_record(id); + return this._super(id, signal); + }, alter_ids: function(n_ids) { this._super(n_ids); this.trigger("dataset_changed", n_ids); @@ -903,9 +938,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({ return this._super.apply(this, arguments); } }, - create: function(data) { + create: function(data, options) { if (this.create_function) { - return this.create_function(data, this._super); + return this.create_function(data, options, this._super); } else { return this._super.apply(this, arguments); } @@ -946,7 +981,10 @@ instance.web.CompoundContext = instance.web.Class.extend({ }, get_eval_context: function () { return this.__eval_context; - } + }, + eval: function() { + return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true}); + }, }); instance.web.CompoundDomain = instance.web.Class.extend({ @@ -969,7 +1007,10 @@ instance.web.CompoundDomain = instance.web.Class.extend({ }, get_eval_context: function() { return this.__eval_context; - } + }, + eval: function() { + return instance.web.pyeval.eval('domain', this); + }, }); instance.web.DropMisordered = instance.web.Class.extend({ diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 851b60af229..854c5f76218 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -314,4 +314,34 @@ instance.web.auto_date_to_str = function(value, type) { } }; +/** + * performs a half up rounding with arbitrary precision, correcting for float loss of precision + * See the corresponding float_round() in server/tools/float_utils.py for more info + * @param {Number} the value to be rounded + * @param {Number} a non zero precision parameter. eg: 0.01 rounds to two digits. + */ +instance.web.round_precision = function(value, precision){ + if(!value){ + return 0; + }else if(!precision){ + throw new Error('round_precision(...): Cannot round value: '+value+' with a precision of zero (or undefined)'); + } + var normalized_value = value / precision; + var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2); + var epsilon = Math.pow(2, epsilon_magnitude - 53); + normalized_value += normalized_value >= 0 ? epsilon : -epsilon; + var rounded_value = Math.round(normalized_value); + return rounded_value * precision; +}; + +/** + * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision + * See the corresponding float_round() in server/tools/float_utils.py for more info + * @param {Number} the value to be rounded + * @param {Number} the number of decimals. eg: round_decimals(3.141592,2) -> 3.14 + */ +instance.web.round_decimals = function(value, decimals){ + return instance.web.round_precision(value, Math.pow(10,-decimals)); +}; + }; diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 554c4aa7db0..c8207066ca9 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -502,12 +502,15 @@ openerp.web.pyeval = function (instance) { return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]); }); + var args = _.map(('year month day hour minute second microsecond ' + + 'years months weeks days hours minutes secondes microseconds ' + + 'weekday leakdays yearday nlyearday').split(' '), function (arg) { + return [arg, null] + }); + args.unshift('*'); var relativedelta = py.type('relativedelta', null, { __init__: function () { - this.ops = py.PY_parseArgs(arguments, - '* year month day hour minute second microsecond ' - + 'years months weeks days hours minutes secondes microseconds ' - + 'weekday leakdays yearday nlyearday'); + this.ops = py.PY_parseArgs(arguments, args); }, __add__: function (other) { if (!py.PY_isInstance(other, datetime.date)) { @@ -600,7 +603,7 @@ openerp.web.pyeval = function (instance) { }); var eval_contexts = function (contexts, evaluation_context) { - evaluation_context = evaluation_context || {}; + evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {}); return _(contexts).reduce(function (result_context, ctx) { // __eval_context evaluations can lead to some of `contexts`'s // values being null, skip them as well as empty contexts @@ -625,9 +628,10 @@ openerp.web.pyeval = function (instance) { // siblings _.extend(evaluation_context, evaluated); return _.extend(result_context, evaluated); - }, _.extend({}, instance.session.user_context)); + }, {}); }; var eval_domains = function (domains, evaluation_context) { + evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {}); var result_domain = []; _(domains).each(function (domain) { if (_.isString(domain)) { @@ -654,6 +658,7 @@ openerp.web.pyeval = function (instance) { return result_domain; }; var eval_groupbys = function (contexts, evaluation_context) { + evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {}); var result_group = []; _(contexts).each(function (ctx) { if (_.isString(ctx)) { @@ -704,14 +709,15 @@ openerp.web.pyeval = function (instance) { * @param {Array} object domains or contexts to evaluate * @param {Object} [context] evaluation context */ - instance.web.pyeval.eval = function (type, object, context) { + instance.web.pyeval.eval = function (type, object, context, options) { + options = options || {}; context = _.extend(instance.web.pyeval.context(), context || {}); context['context'] = py.dict.fromJSON(context); //noinspection FallthroughInSwitchStatementJS switch(type) { case 'context': object = [object]; - case 'contexts': return eval_contexts(object, context); + case 'contexts': return eval_contexts((options.no_user_context ? [] : [instance.session.user_context]).concat(object), context); case 'domain': object = [object]; case 'domains': return eval_domains(object, context); case 'groupbys': return eval_groupbys(object, context); diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index edbec1626b0..0051598e48a 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -324,7 +324,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea e.preventDefault(); break; } - } + }, + 'autocompleteopen': function () { + this.$el.autocomplete('widget').css('z-index', 1004); + }, }, /** * @constructs instance.web.SearchView @@ -352,7 +355,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.has_defaults = !_.isEmpty(this.defaults); this.inputs = []; - this.controls = {}; + this.controls = []; this.headless = this.options.hidden && !this.has_defaults; @@ -379,6 +382,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea model: this.dataset._model, view_id: this.view_id, view_type: 'search', + context: this.dataset.get_context(), }); $.when(load_view).then(function (r) { @@ -492,6 +496,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea */ complete_global_search: function (req, resp) { $.when.apply(null, _(this.inputs).chain() + .filter(function (input) { return input.visible(); }) .invoke('complete', req.term) .value()).then(function () { resp(_(_(arguments).compact()).flatten(true)); @@ -580,18 +585,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * * @param {Array} items a list of nodes to convert to widgets * @param {Object} fields a mapping of field names to (ORM) field attributes - * @param {String} [group_name] name of the group to put the new controls in + * @param {Object} [group] group to put the new controls in */ - make_widgets: function (items, fields, group_name) { - group_name = group_name || null; - if (!(group_name in this.controls)) { - this.controls[group_name] = []; + make_widgets: function (items, fields, group) { + if (!group) { + group = new instance.web.search.Group( + this, 'q', {attrs: {string: _t("Filters")}}); } - var self = this, group = this.controls[group_name]; + var self = this; var filters = []; _.each(items, function (item) { if (filters.length && item.tag !== 'filter') { - group.push(new instance.web.search.FilterGroup(filters, this)); + group.push(new instance.web.search.FilterGroup(filters, group)); filters = []; } @@ -599,15 +604,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea case 'separator': case 'newline': break; case 'filter': - filters.push(new instance.web.search.Filter(item, this)); + filters.push(new instance.web.search.Filter(item, group)); break; case 'group': - self.make_widgets(item.children, fields, item.attrs.string); + self.make_widgets(item.children, fields, + new instance.web.search.Group(group, 'w', item)); break; case 'field': - group.push(this.make_field(item, fields[item['attrs'].name])); + var field = this.make_field( + item, fields[item['attrs'].name], group); + group.push(field); // filters - self.make_widgets(item.children, fields, group_name); + self.make_widgets(item.children, fields, group); break; } }, this); @@ -622,12 +630,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * * @param {Object} item fields_view_get node for the field * @param {Object} field fields_get result for the field + * @param {Object} [parent] * @returns instance.web.search.Field */ - make_field: function (item, field) { + make_field: function (item, field, parent) { var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]); if(obj) { - return new (obj) (item, field, this); + return new (obj) (item, field, parent || this); } else { console.group('Unknown field type ' + field.type); console.error('View node', item); @@ -862,13 +871,18 @@ instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web * @constructs instance.web.search.Widget * @extends instance.web.Widget * - * @param view the ancestor view of this widget + * @param parent parent of this widget */ - init: function (view) { - this._super(view); - this.view = view; + init: function (parent) { + this._super(parent); + var ancestor = parent; + do { + this.view = ancestor; + } while (!(ancestor instanceof instance.web.SearchView) + && (ancestor = (ancestor.getParent && ancestor.getParent()))); } }); + instance.web.search.add_expand_listener = function($root) { $root.find('a.searchview_group_string').click(function (e) { $root.toggleClass('folded expanded'); @@ -877,13 +891,24 @@ instance.web.search.add_expand_listener = function($root) { }); }; instance.web.search.Group = instance.web.search.Widget.extend({ - template: 'SearchView.group', - init: function (view_section, view, fields) { - this._super(view); - this.attrs = view_section.attrs; - this.lines = view.make_widgets( - view_section.children, fields); - } + init: function (parent, icon, node) { + this._super(parent); + var attrs = node.attrs; + this.modifiers = attrs.modifiers = + attrs.modifiers ? JSON.parse(attrs.modifiers) : {}; + this.attrs = attrs; + this.icon = icon; + this.name = attrs.string; + this.children = []; + + this.view.controls.push(this); + }, + push: function (input) { + this.children.push(input); + }, + visible: function () { + return !this.modifiers.invisible; + }, }); instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{ @@ -892,12 +917,12 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan * @constructs instance.web.search.Input * @extends instance.web.search.Widget * - * @param view + * @param parent */ - init: function (view) { - this._super(view); + init: function (parent) { + this._super(parent); + this.load_attrs({}); this.view.inputs.push(this); - this.style = undefined; }, /** * Fetch auto-completion values for the widget. @@ -945,15 +970,30 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan "get_domain not implemented for widget " + this.attrs.type); }, load_attrs: function (attrs) { - if (attrs.modifiers) { - attrs.modifiers = JSON.parse(attrs.modifiers); - attrs.invisible = attrs.modifiers.invisible || false; - if (attrs.invisible) { - this.style = 'display: none;' + attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {}; + this.attrs = attrs; + }, + /** + * Returns whether the input is "visible". The default behavior is to + * query the ``modifiers.invisible`` flag on the input's description or + * view node. + * + * @returns {Boolean} + */ + visible: function () { + if (this.attrs.modifiers.invisible) { + return false; + } + var parent = this; + while ((parent = parent.getParent()) && + ( (parent instanceof instance.web.search.Group) + || (parent instanceof instance.web.search.Input))) { + if (!parent.visible()) { + return false; } } - this.attrs = attrs; - } + return true; + }, }); instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{ template: 'SearchView.filters', @@ -967,17 +1007,19 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in * @extends instance.web.search.Input * * @param {Array<instance.web.search.Filter>} filters elements of the group - * @param {instance.web.SearchView} view view in which the filters are contained + * @param {instance.web.SearchView} parent parent in which the filters are contained */ - init: function (filters, view) { + init: function (filters, parent) { // If all filters are group_by and we're not initializing a GroupbyGroup, // create a GroupbyGroup instead of the current FilterGroup if (!(this instanceof instance.web.search.GroupbyGroup) && _(filters).all(function (f) { - return f.attrs.context && f.attrs.context.group_by; })) { - return new instance.web.search.GroupbyGroup(filters, view); + if (!f.attrs.context) { return false; } + var c = instance.web.pyeval.eval('context', f.attrs.context); + return !_.isEmpty(c.group_by);})) { + return new instance.web.search.GroupbyGroup(filters, parent); } - this._super(view); + this._super(parent); this.filters = filters; this.view.query.on('add remove change reset', this.proxy('search_change')); }, @@ -997,7 +1039,9 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in facet.values.each(function (v) { var i = _(self.filters).indexOf(v.get('value')); if (i === -1) { return; } - $filters.eq(i).addClass('oe_selected'); + $filters.filter(function () { + return Number($(this).data('index')) === i; + }).addClass('oe_selected'); }); }, /** @@ -1087,7 +1131,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in }); }, toggle_filter: function (e) { - this.toggle(this.filters[$(e.target).index()]); + this.toggle(this.filters[Number($(e.target).data('index'))]); }, toggle: function (filter) { this.view.query.toggle(this.make_facet([this.make_value(filter)])); @@ -1096,6 +1140,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in var self = this; item = item.toLowerCase(); var facet_values = _(this.filters).chain() + .filter(function (filter) { return filter.visible(); }) .filter(function (filter) { var at = { string: filter.attrs.string || '', @@ -1122,8 +1167,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ icon: 'w', completion_label: _lt("Group by: %s"), - init: function (filters, view) { - this._super(filters, view); + init: function (filters, parent) { + this._super(filters, parent); // Not flanders: facet unicity is handled through the // (category, field) pair of facet attributes. This is all well and // good for regular filter groups where a group matches a facet, but for @@ -1131,8 +1176,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ // view which proxies to the first GroupbyGroup, so it can be used // for every GroupbyGroup and still provides the various methods needed // by the search view. Use weirdo name to avoid risks of conflicts - if (!this.getParent()._s_groupby) { - this.getParent()._s_groupby = { + if (!this.view._s_groupby) { + this.view._s_groupby = { help: "See GroupbyGroup#init", get_context: this.proxy('get_context'), get_domain: this.proxy('get_domain'), @@ -1141,14 +1186,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ } }, match_facet: function (facet) { - return facet.get('field') === this.getParent()._s_groupby; + return facet.get('field') === this.view._s_groupby; }, make_facet: function (values) { return { category: _t("GroupBy"), icon: this.icon, values: values, - field: this.getParent()._s_groupby + field: this.view._s_groupby }; } }); @@ -1166,10 +1211,10 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc * @extends instance.web.search.Input * * @param node - * @param view + * @param parent */ - init: function (node, view) { - this._super(view); + init: function (node, parent) { + this._super(parent); this.load_attrs(node.attrs); }, facet_for: function () { return $.when(null); }, @@ -1185,10 +1230,10 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc * * @param view_section * @param field - * @param view + * @param parent */ - init: function (view_section, field, view) { - this._super(view); + init: function (view_section, field, parent) { + this._super(parent); this.load_attrs(_.extend({}, field, view_section.attrs)); }, facet_for: function (value) { @@ -1228,7 +1273,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc * * @param {String} name the field's name * @param {String} operator the field's operator (either attribute-specified or default operator for the field - * @param {Number|String} value parsed value for the field + * @param {Number|String} facet parsed value for the field * @returns {Array<Array>} domain to include in the resulting search */ make_domain: function (name, operator, facet) { @@ -1294,20 +1339,22 @@ instance.web.search.CharField = instance.web.search.Field.extend( /** @lends ins } }); instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{ - value_from: function () { - if (!this.$el.val()) { - return null; - } - var val = this.parse(this.$el.val()), - check = Number(this.$el.val()); - if (isNaN(val) || val !== check) { - this.$el.addClass('error'); - throw new instance.web.search.Invalid( - this.attrs.name, this.$el.val(), this.error_message); - } - this.$el.removeClass('error'); - return val; - } + complete: function (value) { + var val = this.parse(value); + if (isNaN(val)) { return $.when(); } + var label = _.str.sprintf( + _t("Search %(field)s for: %(value)s"), { + field: '<em>' + this.attrs.string + '</em>', + value: '<strong>' + _.str.escapeHTML(value) + '</strong>'}); + return $.when([{ + label: label, + facet: { + category: this.attrs.string, + field: this, + values: [{label: value, value: val}] + } + }]); + }, }); /** * @class @@ -1460,8 +1507,8 @@ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @le }); instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ default_operator: {}, - init: function (view_section, field, view) { - this._super(view_section, field, view); + init: function (view_section, field, parent) { + this._super(view_section, field, parent); this.model = new instance.web.Model(this.attrs.relation); }, complete: function (needle) { @@ -1673,6 +1720,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ if (!_.isEmpty(results.group_by)) { results.context.group_by = results.group_by; } + // Don't save user_context keys in the custom filter, otherwise end + // up with e.g. wrong uid or lang stored *and used in subsequent + // reqs* + var ctx = results.context; + _(_.keys(instance.session.user_context)).each(function (key) { + delete ctx[key]; + }); var filter = { name: $name.val(), user_id: private_filter ? instance.session.uid : false, @@ -1702,22 +1756,28 @@ instance.web.search.Filters = instance.web.search.Input.extend({ var running_count = 0; // get total filters count var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; }; - var filters_count = _(this.view.controls).chain() + var visible_filters = _(this.view.controls).chain().reject(function (group) { + return _(_(group.children).filter(is_group)).isEmpty() + || group.modifiers.invisible; + }); + var filters_count = visible_filters + .pluck('children') .flatten() .filter(is_group) .map(function (i) { return i.filters.length; }) .sum() .value(); - var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) { - var filters = _(inputs).filter(is_group); - return { - name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group, - filters: filters, - length: _(filters).chain().map(function (i) { - return i.filters.length; }).sum().value() - }; - }); + var col1 = [], col2 = visible_filters.map(function (group) { + var filters = _(group.children).filter(is_group); + return { + name: _.str.sprintf("<span class='oe_i'>%s</span> %s", + group.icon, group.name), + filters: filters, + length: _(filters).chain().map(function (i) { + return i.filters.length; }).sum().value() + }; + }).value(); while (col2.length) { // col1 + group should be smaller than col2 + group diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index e7aad43ffc7..e665a43db01 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -176,9 +176,9 @@ openerp.testing = {}; }); QUnit.module(testing.current_module + '.' + name, {_oe: options}); - body(testing.case); + body(testing['case']); }; - testing.case = function (name, options, callback) { + testing['case'] = function (name, options, callback) { if (_.isFunction(options)) { callback = options; options = {}; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 9432a106f6f..0373af78358 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -765,7 +765,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.has_been_loaded.done(function() { if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { self.dataset.unlink([self.datarecord.id]).done(function() { - self.execute_pager_action('next'); + if (self.dataset.size()) { + self.execute_pager_action('next'); + } else { + self.do_action('history_back'); + } def.resolve(); }); } else { @@ -802,6 +806,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (save_obj.error) return $.Deferred().reject(); return $.when.apply($, save_obj.ret); + }).done(function() { + self.$el.removeClass('oe_form_dirty'); }); }, _process_save: function(save_obj) { @@ -810,7 +816,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM try { var form_invalid = false, values = {}, - first_invalid_field = null; + first_invalid_field = null, + readonly_values = {}; for (var f in self.fields) { if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; @@ -819,11 +826,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { + } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields - values[f.name] = f.get_value(); + if (!f.get("readonly")) { + values[f.name] = f.get_value(); + } else { + readonly_values[f.name] = f.get_value(); + } } } if (form_invalid) { @@ -836,7 +847,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var save_deferral; if (!self.datarecord.id) { // Creation save - save_deferral = self.dataset.create(values).then(function(r) { + save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) { return self.record_created(r, prepend_on_create); }, null); } else if (_.isEmpty(values)) { @@ -844,7 +855,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM save_deferral = $.Deferred().resolve({}).promise(); } else { // Write save - save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) { + save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) { return self.record_saved(r); }, null); } @@ -1016,7 +1027,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return value; } var fields = _.chain(this.fields) - .map(function (field, name) { + .map(function (field) { var value = field.get_value(); // ignore fields which are empty, invisible, readonly, o2m // or m2m @@ -1031,7 +1042,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } return { - name: name, + name: field.name, string: field.string, value: value, displayed: display(field, value), @@ -1042,10 +1053,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM .value(); var conditions = _.chain(self.fields) .filter(function (field) { return field.field.change_default; }) - .map(function (field, name) { + .map(function (field) { var value = field.get_value(); return { - name: name, + name: field.name, string: field.string, value: value, displayed: display(field, value), @@ -1439,6 +1450,9 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt $(this).children().each(function() { var $td = $(this), $child = $td.children(':first'); + if ($child.attr('cell-class')) { + $td.addClass($child.attr('cell-class')); + } switch ($child[0].tagName.toLowerCase()) { case 'separator': break; @@ -1515,7 +1529,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (! page.__ic) return; page.__ic.on("change:effective_invisible", null, function() { - if (!page.__ic.get('effective_invisible')) { + if (!page.__ic.get('effective_invisible') && page.autofocus) { $new_notebook.tabs('select', i); return; } @@ -1887,10 +1901,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png'; } - this.view.on('view_content_has_changed', this, this.check_disable); }, start: function() { this._super.apply(this, arguments); + this.view.on('view_content_has_changed', this, this.check_disable); + this.check_disable(); this.$el.click(this.on_click); if (this.node.attrs.help || instance.session.debug) { this.do_attach_tooltip(); @@ -1916,18 +1931,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ modal: true, buttons: [ {text: _t("Cancel"), click: function() { - def.resolve(); $(this).dialog("close"); } }, {text: _t("Ok"), click: function() { - self.on_confirmed().done(function() { - def.resolve(); + var self2 = this; + self.on_confirmed().always(function() { + $(self2).dialog("close"); }); - $(this).dialog("close"); } } - ] + ], + beforeClose: function() { + def.resolve(); + }, }); return def.promise(); } else { @@ -2118,7 +2135,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w value without triggering a re-rendering. */ internal_set_value: function(value_) { - var tmp = this.no_render; + var tmp = this.no_rerender; this.no_rerender = true; this.set({'value': value_}); this.no_rerender = tmp; @@ -2218,6 +2235,18 @@ instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.Reini }, }); +/** + Some hack to make placeholders work in ie9. +*/ +if ($.browser.msie && $.browser.version === "9.0") { + document.addEventListener("DOMNodeInserted",function(event){ + var nodename = event.target.nodeName.toLowerCase(); + if ( nodename === "input" || nodename == "textarea" ) { + $(event.target).placeholder(); + } + }); +} + instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldChar', widget_class: 'oe_form_field_char', @@ -2276,7 +2305,8 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.we return this.get('value') === '' || this._super(); }, focus: function() { - this.$('input:first')[0].focus(); + var input = this.$('input:first')[0]; + return input ? input.focus() : false; }, set_dimensions: function (height, width) { this._super(height, width); @@ -2337,7 +2367,8 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ if (!s) { tmp = "http://" + this.get('value'); } - this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : ''); + var text = this.get('value') ? this.node.attrs.text || tmp : ''; + this.$el.find('a').attr('href', tmp).text(text); } }, on_button_clicked: function() { @@ -2372,7 +2403,8 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ this._super.apply(this, [value_]); }, focus: function () { - this.$('input:first').select(); + var $input = this.$('input:first'); + return $input.length ? $input.select() : false; } }); @@ -2382,7 +2414,6 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ type_of_date: "datetime", events: { 'change .oe_datepicker_master': 'change_datetime', - 'dragstart img.oe_datepicker_trigger': function () { return false; }, }, init: function(parent) { this._super(parent); @@ -2393,6 +2424,42 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ this.$input = this.$el.find('input.oe_datepicker_master'); this.$input_picker = this.$el.find('input.oe_datepicker_container'); + $.datepicker.setDefaults({ + clearText: _t('Clear'), + clearStatus: _t('Erase the current date'), + closeText: _t('Done'), + closeStatus: _t('Close without change'), + prevText: _t('<Prev'), + prevStatus: _t('Show the previous month'), + nextText: _t('Next>'), + nextStatus: _t('Show the next month'), + currentText: _t('Today'), + currentStatus: _t('Show the current month'), + monthNames: Date.CultureInfo.monthNames, + monthNamesShort: Date.CultureInfo.abbreviatedMonthNames, + monthStatus: _t('Show a different month'), + yearStatus: _t('Show a different year'), + weekHeader: _t('Wk'), + weekStatus: _t('Week of the year'), + dayNames: Date.CultureInfo.dayNames, + dayNamesShort: Date.CultureInfo.abbreviatedDayNames, + dayNamesMin: Date.CultureInfo.shortestDayNames, + dayStatus: _t('Set DD as first week day'), + dateStatus: _t('Select D, M d'), + firstDay: Date.CultureInfo.firstDayOfWeek, + initStatus: _t('Select a date'), + isRTL: false + }); + $.timepicker.setDefaults({ + timeOnlyTitle: _t('Choose Time'), + timeText: _t('Time'), + hourText: _t('Hour'), + minuteText: _t('Minute'), + secondText: _t('Second'), + currentText: _t('Now'), + closeText: _t('Done') + }); + this.picker({ onClose: this.on_picker_select, onSelect: this.on_picker_select, @@ -2519,9 +2586,8 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc return this.get('value') === '' || this._super(); }, focus: function() { - if (this.datewidget && this.datewidget.$input) { - this.datewidget.$input[0].focus(); - } + var input = this.datewidget && this.datewidget.$input[0]; + return input ? input.focus() : false; }, set_dimensions: function (height, width) { this._super(height, width); @@ -2546,42 +2612,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we }, 'change textarea': 'store_dom_value', }, - init: function (field_manager, node) { - this._super(field_manager, node); - }, initialize_content: function() { var self = this; - this.$textarea = this.$el.find('textarea'); - this.auto_sized = false; - this.default_height = this.$textarea.css('height'); - if (this.get("effective_readonly")) { - this.$textarea.attr('disabled', 'disabled'); + if (! this.get("effective_readonly")) { + this.$textarea = this.$el.find('textarea'); + this.auto_sized = false; + this.default_height = this.$textarea.css('height'); + if (this.get("effective_readonly")) { + this.$textarea.attr('disabled', 'disabled'); + } + this.setupFocus(this.$textarea); + } else { + this.$textarea = undefined; } - this.setupFocus(this.$textarea); }, commit_value: function () { - this.store_dom_value(); + if (! this.get("effective_readonly") && this.$textarea) { + this.store_dom_value(); + } return this._super(); }, store_dom_value: function () { - if (!this.get('effective_readonly') && this.$('textarea').length) { - this.internal_set_value( - instance.web.parse_value( - this.$textarea.val(), - this)); - } + this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this)); }, render_value: function() { - var show_value = instance.web.format_value(this.get('value'), this, ''); - if (show_value === '') { - this.$textarea.css('height', parseInt(this.default_height)+"px"); - } - this.$textarea.val(show_value); - if (! this.auto_sized) { - this.auto_sized = true; - this.$textarea.autosize(); + if (! this.get("effective_readonly")) { + var show_value = instance.web.format_value(this.get('value'), this, ''); + if (show_value === '') { + this.$textarea.css('height', parseInt(this.default_height)+"px"); + } + this.$textarea.val(show_value); + if (! this.auto_sized) { + this.auto_sized = true; + this.$textarea.autosize(); + } else { + this.$textarea.trigger("autosize"); + } } else { - this.$textarea.trigger("autosize"); + var txt = this.get("value") || ''; + this.$(".oe_form_text_content").text(txt); } }, is_syntax_valid: function() { @@ -2599,14 +2668,17 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we return this.get('value') === '' || this._super(); }, focus: function($el) { - this.$textarea[0].focus(); + var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0]; + return input ? input.focus() : false; }, set_dimensions: function (height, width) { this._super(height, width); - this.$textarea.css({ - width: width, - minHeight: height - }); + if (!this.get("effective_readonly") && this.$textarea) { + this.$textarea.css({ + width: width, + minHeight: height + }); + } }, }); @@ -2637,7 +2709,7 @@ instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instanc "| removeformat | bullets numbering | outdent " + "indent | link unlink | source", bodyStyle: // style to assign to document body contained within the editor - "margin:4px; color:#4c4c4c; font-size:13px; font-family:\"Lucida Grande\",Helvetica,Verdana,Arial,sans-serif; cursor:text" + "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text" }); this.$cleditor = this.$textarea.cleditor()[0]; this.$cleditor.change(function() { @@ -2681,7 +2753,8 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ this.$checkbox[0].checked = this.get('value'); }, focus: function() { - this.$checkbox[0].focus(); + var input = this.$checkbox && this.$checkbox[0]; + return input ? input.focus() : false; }, click_disabled_boolean: function(){ var $disabled = $(document).find('input[type=checkbox]:disabled'); @@ -2694,7 +2767,6 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ $parent.append($overlay); }); } - }); /** @@ -2779,7 +2851,8 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan } }, focus: function() { - this.$('select:first')[0].focus(); + var input = this.$('select:first')[0]; + return input ? input.focus() : false; }, set_dimensions: function (height, width) { this._super(height, width); @@ -2790,6 +2863,99 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan } }); +instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + template: 'FieldRadio', + events: { + 'click input': 'click_change_value' + }, + init: function(field_manager, node) { + /* Radio button widget: Attributes options: + * - "horizontal" to display in column + * - "no_radiolabel" don't display text values + */ + this._super(field_manager, node); + this.selection = _.clone(this.field.selection) || []; + this.domain = false; + }, + initialize_content: function () { + this.uniqueId = _.uniqueId("radio"); + this.on("change:effective_readonly", this, this.render_value); + this.field_manager.on("view_content_has_changed", this, this.get_selection); + this.get_selection(); + }, + click_change_value: function (event) { + var val = $(event.target).val(); + val = this.field.type == "selection" ? val : +val; + if (val == this.get_value()) { + this.set_value(false); + } else { + this.set_value(val); + } + }, + /** Get the selection and render it + * selection: [[identifier, value_to_display], ...] + * For selection fields: this is directly given by this.field.selection + * For many2one fields: perform a search on the relation of the many2one field + */ + get_selection: function() { + var self = this; + var selection = []; + var def = $.Deferred(); + if (self.field.type == "many2one") { + var domain = instance.web.pyeval.eval('domain', this.build_domain()) || []; + if (! _.isEqual(self.domain, domain)) { + self.domain = domain; + var ds = new instance.web.DataSet(self, self.field.relation); + ds.call('search', [self.domain]) + .then(function (records) { + ds.name_get(records).then(function (records) { + selection = records; + def.resolve(); + }); + }); + } else { + selection = self.selection; + def.resolve(); + } + } + else if (self.field.type == "selection") { + selection = self.field.selection || []; + def.resolve(); + } + return def.then(function () { + if (! _.isEqual(selection, self.selection)) { + self.selection = _.clone(selection); + self.renderElement(); + self.render_value(); + } + }); + }, + set_value: function (value_) { + if (value_) { + if (this.field.type == "selection") { + value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_}); + } + else if (!this.selection.length) { + this.selection = [value_]; + } + } + this._super(value_); + }, + get_value: function () { + var value = this.get('value'); + return value instanceof Array ? value[0] : value; + }, + render_value: function () { + var self = this; + this.$el.toggleClass("oe_readonly", this.get('effective_readonly')); + this.$("input:checked").prop("checked", false); + if (this.get_value()) { + this.$("input").filter(function () {return this.value == self.get_value()}).prop("checked", true); + this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : ""); + } + } +}); + // jquery autocomplete tweak to allow html and classnames (function() { var proto = $.ui.autocomplete.prototype, @@ -2983,8 +3149,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc e.stopPropagation(); } }, - 'dragstart .oe_m2o_drop_down_button img': function () { return false; }, - 'dragstart .oe_m2o_cm_button': function () { return false; } }, init: function(field_manager, node) { this._super(field_manager, node); @@ -3015,6 +3179,26 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc if (!this.get("effective_readonly")) this.render_editable(); }, + destroy_content: function () { + if (this.$drop_down) { + this.$drop_down.off('click'); + delete this.$drop_down; + } + if (this.$input) { + this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll'); + this.$input.off('keyup blur autocompleteclose autocompleteopen ' + + 'focus focusout change keydown'); + delete this.$input; + } + if (this.$follow_button) { + this.$follow_button.off('blur focus click'); + delete this.$follow_button; + } + }, + destroy: function () { + this.destroy_content(); + return this._super(); + }, init_error_displayer: function() { // nothing }, @@ -3211,7 +3395,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc } if (! no_recurse) { var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); - dataset.name_get([self.get("value")]).done(function(data) { + this.alive(dataset.name_get([self.get("value")])).done(function(data) { self.display_value["" + self.get("value")] = data[0][1]; self.render_value(true); }); @@ -3245,7 +3429,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc res_model: self.field.relation, res_id: self.get("value"), views: [[false, 'form']], - target: 'current' + target: 'current', + context: self.build_context().eval(), }); return false; }); @@ -3275,9 +3460,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc return ! this.get("value"); }, focus: function () { - if (!this.get('effective_readonly')) { - this.$input[0].focus(); - } + var input = !this.get('effective_readonly') && this.$input && this.$input[0]; + return input ? input.focus() : false; }, _quick_create: function() { this.no_ed = true; @@ -3695,8 +3879,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ var pop = new instance.web.form.FormOpenPopup(this); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { title: _t("Open: ") + self.o2m.string, - create_function: function(data) { - return self.o2m.dataset.create(data).done(function(r) { + create_function: function(data, options) { + return self.o2m.dataset.create(data, options).done(function(r) { self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); self.o2m.dataset.trigger("dataset_changed", r); }); @@ -3755,8 +3939,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this.o2m.trigger_on_change(); }, is_valid: function () { - var form = this.editor.form; - + var editor = this.editor; + var form = editor.form; + // If no edition is pending, the listview can not be invalid (?) + if (!editor.record) { + return true + } // If the form has not been modified, the view can only be valid // NB: is_dirty will also be set on defaults/onchanges/whatever? // oe_form_dirty seems to only be set on actual user actions @@ -3786,11 +3974,11 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ title: _t("Create: ") + self.o2m.string, initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - create_function: function(data, callback, error_callback) { - return self.o2m.dataset.create(data).done(function(r) { + create_function: function(data, options) { + return self.o2m.dataset.create(data, options).done(function(r) { self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); self.o2m.dataset.trigger("dataset_changed", r); - }).done(callback).fail(error_callback); + }); }, read_function: function() { return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); @@ -4003,6 +4191,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in if (this.get("effective_readonly")) return; var self = this; + var ignore_blur = false; self.$text = this.$("textarea"); self.$text.textext({ plugins : 'tags arrow autocomplete', @@ -4021,6 +4210,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in if (data.id) { self.add_id(data.id); } else { + ignore_blur = true; data.action(); } }, @@ -4071,10 +4261,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in self.$text .focusin(function () { self.trigger('focused'); + ignore_blur = false; }) .focusout(function() { self.$text.trigger("setInputData", ""); - self.trigger('blurred'); + if (!ignore_blur) { + self.trigger('blurred'); + } }).keydown(function(e) { if (e.which === $.ui.keyCode.TAB && self._drop_shown) { self.$text.textext()[0].autocomplete().selectFromDropdown(); @@ -4128,7 +4321,8 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in this.set({'value': _.uniq(this.get('value').concat([id]))}); }, focus: function () { - this.$text[0].focus(); + var input = this.$text && this.$text[0]; + return input ? input.focus() : false; }, set_dimensions: function (height, width) { this._super(height, width); @@ -4281,6 +4475,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in }); } }, + is_action_enabled: function () { return true; }, }); instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { @@ -4515,9 +4710,9 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ this.created_elements = []; this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context); this.dataset.read_function = this.options.read_function; - this.dataset.create_function = function(data, sup) { + this.dataset.create_function = function(data, options, sup) { var fct = self.options.create_function || sup; - return fct.call(this, data).done(function(r) { + return fct.call(this, data, options).done(function(r) { self.trigger('create_completed saved', r); self.created_elements.push(r); }); @@ -5066,7 +5261,7 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ }); /** - * Widget for (one2many field) to upload one or more file in same time and display in list. + * Widget for (many2many field) to upload one or more file in same time and display in list. * The user can delete his files. * Options on attribute ; "blockui" {Boolean} block the UI or not * during the file is uploading @@ -5080,6 +5275,8 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') { throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string); } + this.data = {}; + this.set_value([]); this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); this.fileupload_id = _.uniqueId('oe_fileupload_temp'); $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this)); @@ -5089,73 +5286,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie 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 ); + value_ = value_ || []; + if (value_.length >= 1 && value_[0] instanceof Array) { + value_ = value_[0][2]; + } + this._super(value_); }, get_value: function() { - return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); }); + var tmp = [commands.replace_with(this.get("value"))]; + return tmp; }, get_file_url: function (attachment) { 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); - } - }); + // don't reset know values + var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } ); // send request for get_name - if (values.length) { - return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) { + if (_value.length) { + return this.ds_file.call('read', [_value, ['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; - } - }); + self.data[data.id] = data; }); }); } else { - return $.when(this.get('value')); + return $.when(); } }, render_value: function () { var self = this; - this.read_name_values().then(function (datas) { + this.read_name_values().then(function () { var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self})); render.on('click', '.oe_delete', _.bind(self.on_file_delete, self)); @@ -5173,45 +5336,36 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie var self = this; var $target = $(event.target); if ($target.val() !== '') { - var filename = $target.val().replace(/.*[\\\/]/,''); - - // if the files is currently uploded, don't send again - if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) { + // don't uplode more of one file in same time + if (self.data[0] && self.data[0].upload ) { return false; } + for (var id in this.get('value')) { + // if the files exits, delete the file before upload (if it's a new file) + if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) { + self.ds_file.unlink([id]); + } + } // block UI or not if(this.node.attrs.blockui>0) { instance.web.blockUI(); } - // if the files exits for this answer, delete the file before upload - var files = _.filter(this.get('value'), function (file) { - if((file.filename || file.name) == filename) { - self.ds_file.unlink([file.id]); - return false; - } else { - return true; - } - }); - // TODO : unactivate send on wizard and form // submit file this.$('form.oe_form_binary_form').submit(); this.$(".oe_fileupload").hide(); - - // add file on result - files.push({ + // add file on data result + this.data[0] = { 'id': 0, 'name': filename, 'filename': filename, 'url': '', 'upload': true - }); - - this.set({'value': files}); + }; } }, on_file_loaded: function (event, result) { @@ -5222,39 +5376,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie instance.web.unblockUI(); } - // TODO : activate send on wizard and form - if (result.error || !result.id ) { this.do_warn( _t('Uploading error'), result.error); - files = _.filter(files, function (val) { return !val.upload; }); + delete this.data[0]; } else { - for(var i in files){ - if(files[i].filename == result.filename && files[i].upload) { - files[i] = { - 'id': result.id, - 'name': result.name, - 'filename': result.filename, - 'url': this.get_file_url(result) - }; - } + if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) { + delete this.data[0]; + this.data[result.id] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': this.get_file_url(result) + }; + } else { + this.data[result.id] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': this.get_file_url(result) + }; } + var values = _.clone(this.get('value')); + values.push(result.id); + this.set({'value': values}); } - - this.set({'value': files}); this.render_value() }, on_file_delete: function (event) { event.stopPropagation(); var file_id=$(event.target).data("id"); if (file_id) { - var files=[]; - for(var i in this.get('value')){ - if(file_id != this.get('value')[i].id){ - files.push(this.get('value')[i]); - } - else if(!this.get('value')[i].no_unlink) { - this.ds_file.unlink([file_id]); - } + var files = _.filter(this.get('value'), function (id) {return id != file_id;}); + if(!this.data[file_id].no_unlink) { + this.ds_file.unlink([file_id]); } this.set({'value': files}); } @@ -5277,11 +5431,11 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ this.calc_domain(); this.on("change:value", this, this.get_selection); this.on("change:evaluated_selection_domain", this, this.get_selection); - this.get_selection(); this.on("change:selection", this, function() { this.selection = this.get("selection"); this.render_value(); }); + this.get_selection(); if (this.options.clickable) { this.$el.on('click','li',this.on_click_stage); } @@ -5300,15 +5454,15 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ var self = this; var content = QWeb.render("FieldStatus.content", {widget: self}); self.$el.html(content); - var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}"); - var color = colors[self.get('value')]; - if (color) { - self.$("oe_active").css("color", color); - } }, calc_domain: function() { var d = instance.web.pyeval.eval('domain', this.build_domain()); - domain = ['|', ['id', '=', this.get('value')]].concat(d); + var domain = []; //if there is no domain defined, fetch all the records + + if (d.length) { + domain = ['|',['id', '=', this.get('value')]].concat(d); + } + if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) { this.set("evaluated_selection_domain", domain); } @@ -5400,10 +5554,10 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({ }); }, parse_value: function(val, def) { - return instance.web.parse_value(val, {type: "float"}, def); + return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def); }, format_value: function(val, def) { - return instance.web.format_value(val, {type: "float"}, def); + return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def); }, }); @@ -5423,6 +5577,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'date' : 'instance.web.form.FieldDate', 'datetime' : 'instance.web.form.FieldDatetime', 'selection' : 'instance.web.form.FieldSelection', + 'radio' : 'instance.web.form.FieldRadio', 'many2one' : 'instance.web.form.FieldMany2One', 'many2onebutton' : 'instance.web.form.Many2OneButton', 'many2many' : 'instance.web.form.FieldMany2Many', diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index d23bedf1d1c..b96079d2051 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi .appendTo($this.empty()) .click(function (e) {e.stopPropagation();}) .append('<option value="80">80</option>' + - '<option value="100">100</option>' + '<option value="200">200</option>' + '<option value="500">500</option>' + + '<option value="2000">2000</option>' + '<option value="NaN">' + _t("Unlimited") + '</option>') .change(function () { var val = parseInt($select.val(), 10); @@ -578,7 +578,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.no_leaf = !!context['group_by_no_leaf']; this.grouped = !!group_by; - return this.load_view(context).then( + return this.alive(this.load_view(context)).then( this.proxy('reload_content')); }, /** @@ -895,8 +895,9 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.record_callbacks = { 'remove': function (event, record) { - var $row = self.$current.children( - '[data-id=' + record.get('id') + ']'); + var id = record.get('id'); + self.dataset.remove_ids([id]) + var $row = self.$current.children('[data-id=' + id + ']'); var index = $row.data('index'); $row.remove(); }, diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index f1bd7c4325e..4e042b60c5d 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) { }); }, editable: function () { - return !this.options.disable_editable_mode + return !this.grouped + && !this.options.disable_editable_mode && (this.fields_view.arch.attrs.editable || this._context_editable || this.options.editable); diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index f5638004b47..53e2e21c7c3 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -149,6 +149,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ for (var i = 0; i < this.breadcrumbs.length; i += 1) { var item = this.breadcrumbs[i]; var tit = item.get_title(); + if (item.hide_breadcrumb) { + continue; + } if (!_.isArray(tit)) { tit = [tit]; } @@ -282,6 +285,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ * @param {Object} [options] * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on. + * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions) * @param {Function} [options.action_menu_id] Manually set the menu id on the fly. * @param {Object} [options.additional_context] Additional context to be merged with the action's context. @@ -291,6 +295,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ options = _.defaults(options || {}, { clear_breadcrumbs: false, on_reverse_breadcrumb: function() {}, + hide_breadcrumb: false, on_close: function() {}, action_menu_id: null, additional_context: {}, @@ -403,7 +408,12 @@ instance.web.ActionManager = instance.web.Widget.extend({ widget: function () { return new instance.web.ViewManagerAction(self, action); }, action: action, klass: 'oe_act_window', - post_process: function (widget) { widget.add_breadcrumb(options.on_reverse_breadcrumb); } + post_process: function (widget) { + widget.add_breadcrumb({ + on_reverse_breadcrumb: options.on_reverse_breadcrumb, + hide_breadcrumb: options.hide_breadcrumb, + }); + }, }, options); }, ir_actions_client: function (action, options) { @@ -427,6 +437,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ widget: widget, title: action.name, on_reverse_breadcrumb: options.on_reverse_breadcrumb, + hide_breadcrumb: options.hide_breadcrumb, }); if (action.tag !== 'reload') { self.do_push_state({}); @@ -649,7 +660,8 @@ instance.web.ViewManager = instance.web.Widget.extend({ set_title: function(title) { this.$el.find('.oe_view_title_text:first').text(title); }, - add_breadcrumb: function(on_reverse_breadcrumb) { + add_breadcrumb: function(options) { + var options = options || {}; var self = this; var views = [this.active_view || this.views_src[0].view_type]; this.on('switch_mode', self, function(mode) { @@ -661,7 +673,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ views.push(mode); } }); - this.getParent().push_breadcrumb({ + var item = _.extend({ widget: this, action: this.action, show: function(index) { @@ -695,9 +707,9 @@ instance.web.ViewManager = instance.web.Widget.extend({ titles.pop(); } return titles; - }, - on_reverse_breadcrumb: on_reverse_breadcrumb, - }); + } + }, options); + this.getParent().push_breadcrumb(item); }, /** * Returns to the view preceding the caller view in this manager's @@ -992,7 +1004,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ switch_mode: function (view_type, no_store, options) { var self = this; - return $.when(this._super.apply(this, arguments)).done(function () { + return this.alive($.when(this._super.apply(this, arguments))).done(function () { var controller = self.views[self.active_view].controller; self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', { view: controller, @@ -1263,6 +1275,7 @@ instance.web.View = instance.web.Widget.extend({ "view_id": this.view_id, "view_type": this.view_type, "toolbar": !!this.options.$sidebar, + "context": this.dataset.get_context(), }); } return view_loaded_def.then(function(r) { @@ -1351,7 +1364,7 @@ instance.web.View = instance.web.Widget.extend({ } else if (action_data.type=="action") { return this.rpc('/web/action/load', { action_id: action_data.name, - context: instance.web.pyeval.eval('context', context), + context: _.extend({'active_model': dataset.model, 'active_ids': dataset.ids, 'active_id': record_id}, instance.web.pyeval.eval('context', context)), do_not_eval: true }).then(handler); } else { @@ -1439,18 +1452,18 @@ instance.web.View = instance.web.Widget.extend({ is_action_enabled: function(action) { var attrs = this.fields_view.arch.attrs; return (action in attrs) ? JSON.parse(attrs[action]) : true; - } + }, }); /** * Performs a fields_view_get and apply postprocessing. * return a {$.Deferred} resolved with the fvg * - * @param {Object} [args] + * @param {Object} args * @param {String|Object} args.model instance.web.Model instance or string repr of the model - * @param {null|Object} args.context context if args.model is a string - * @param {null|Number} args.view_id id of the view to be loaded, default view if null - * @param {null|String} args.view_type type of view to be loaded if view_id is null + * @param {Object} [args.context] context if args.model is a string + * @param {Number} [args.view_id] id of the view to be loaded, default view if null + * @param {String} [args.view_type] type of view to be loaded if view_id is null * @param {Boolean} [args.toolbar=false] get the toolbar definition */ instance.web.fields_view_get = function(args) { @@ -1477,7 +1490,7 @@ instance.web.fields_view_get = function(args) { if (typeof model === 'string') { model = new instance.web.Model(args.model, args.context); } - return args.model.call('fields_view_get', [args.view_id, args.view_type, model.context(), args.toolbar]).then(function(fvg) { + return args.model.call('fields_view_get', [args.view_id, args.view_type, args.context, args.toolbar]).then(function(fvg) { return postprocess(fvg); }); }; @@ -1555,12 +1568,17 @@ instance.web.xml_to_str = function(node) { } else { throw new Error(_t("Could not serialize XML")); } - // Browsers won't deal with self closing tags except br, hr, input, ... - // http://stackoverflow.com/questions/97522/what-are-all-the-valid-self-closing-elements-in-xhtml-as-implemented-by-the-maj - // + // Browsers won't deal with self closing tags except void elements: + // http://www.w3.org/TR/html-markup/syntax.html + var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' '); + // The following regex is a bit naive but it's ok for the xmlserializer output str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) { - return "<" + tag + attrs + "></" + tag + ">"; + if (void_elements.indexOf(tag) < 0) { + return "<" + tag + attrs + "></" + tag + ">"; + } else { + return match; + } }); return str; }; |
