/*--------------------------------------------------------- * OpenERP Web core *--------------------------------------------------------*/ var console; if (!console) { console = {log: function () {}}; } if (!console.debug) { console.debug = console.log; } openerp.web.core = function(openerp) { openerp.web.Class = nova.Class; openerp.web.callback = function(obj, method) { var callback = function() { var args = Array.prototype.slice.call(arguments); var r; for(var i = 0; i < callback.callback_chain.length; i++) { var c = callback.callback_chain[i]; if(c.unique) { callback.callback_chain.splice(i, 1); i -= 1; } var result = c.callback.apply(c.self, c.args.concat(args)); if (c.callback === method) { // return the result of the original method r = result; } // TODO special value to stop the chain // openerp.web.callback_stop } return r; }; callback.callback_chain = []; callback.add = function(f) { if(typeof(f) == 'function') { f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; } f.self = f.self || null; f.args = f.args || []; f.unique = !!f.unique; if(f.position == 'last') { callback.callback_chain.push(f); } else { callback.callback_chain.unshift(f); } return callback; }; callback.add_first = function(f) { return callback.add.apply(null,arguments); }; callback.add_last = function(f) { return callback.add({ callback: f, args: Array.prototype.slice.call(arguments, 1), position: "last" }); }; callback.remove = function(f) { callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) { return el.callback === f; })); return callback; }; return callback.add({ callback: method, self:obj, args:Array.prototype.slice.call(arguments, 2) }); }; /** * web error for lookup failure * * @class */ openerp.web.NotFound = openerp.web.Class.extend( /** @lends openerp.web.NotFound# */ { }); openerp.web.KeyNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.KeyNotFound# */ { /** * Thrown when a key could not be found in a mapping * * @constructs openerp.web.KeyNotFound * @extends openerp.web.NotFound * @param {String} key the key which could not be found */ init: function (key) { this.key = key; }, toString: function () { return "The key " + this.key + " was not found"; } }); openerp.web.ObjectNotFound = openerp.web.NotFound.extend( /** @lends openerp.web.ObjectNotFound# */ { /** * Thrown when an object path does not designate a valid class or object * in the openerp hierarchy. * * @constructs openerp.web.ObjectNotFound * @extends openerp.web.NotFound * @param {String} path the invalid object path */ init: function (path) { this.path = path; }, toString: function () { return "Could not find any object of path " + this.path; } }); openerp.web.Registry = openerp.web.Class.extend( /** @lends openerp.web.Registry# */ { /** * Stores a mapping of arbitrary key (strings) to object paths (as strings * as well). * * Resolves those paths at query time in order to always fetch the correct * object, even if those objects have been overloaded/replaced after the * registry was created. * * An object path is simply a dotted name from the openerp root to the * object pointed to (e.g. ``"openerp.web.Connection"`` for an OpenERP * connection object). * * @constructs openerp.web.Registry * @param {Object} mapping a mapping of keys to object-paths */ init: function (mapping) { this.parent = null; this.map = mapping || {}; }, /** * Retrieves the object matching the provided key string. * * @param {String} key the key to fetch the object for * @param {Boolean} [silent_error=false] returns undefined if the key or object is not found, rather than throwing an exception * @returns {Class} the stored class, to initialize * * @throws {openerp.web.KeyNotFound} if the object was not in the mapping * @throws {openerp.web.ObjectNotFound} if the object path was invalid */ get_object: function (key, silent_error) { var path_string = this.map[key]; if (path_string === undefined) { if (this.parent) { return this.parent.get_object(key, silent_error); } if (silent_error) { return void 'nooo'; } throw new openerp.web.KeyNotFound(key); } var object_match = openerp; var path = path_string.split('.'); // ignore first section for(var i=1; i 12) { year += 1; month -= 12; } } var lastMonthDay = new Date(year, month, 0).getDate(); var day = asJS(this.ops.day) || asJS(other.day); if (day > lastMonthDay) { day = lastMonthDay; } var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); if (days_offset) { day = new Date(year, month-1, day + days_offset).getDate(); } // TODO: leapdays? // TODO: hours, minutes, seconds? Not used in XML domains // TODO: weekday? return new datetime.date(year, month, day); }, __radd__: function (other) { return this.__add__(other); }, __sub__: function (other) { if (!(other instanceof datetime.date)) { return py.NotImplemented; } // TODO: test this whole mess var year = asJS(this.ops.year) || asJS(other.year); if (asJS(this.ops.years)) { year -= asJS(this.ops.years); } var month = asJS(this.ops.month) || asJS(other.month); if (asJS(this.ops.months)) { month -= asJS(this.ops.months); // FIXME: no divmod in JS? while (month < 1) { year -= 1; month += 12; } while (month > 12) { year += 1; month -= 12; } } var lastMonthDay = new Date(year, month, 0).getDate(); var day = asJS(this.ops.day) || asJS(other.day); if (day > lastMonthDay) { day = lastMonthDay; } var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); if (days_offset) { day = new Date(year, month-1, day - days_offset).getDate(); } // TODO: leapdays? // TODO: hours, minutes, seconds? Not used in XML domains // TODO: weekday? return new datetime.date(year, month, day); }, __rsub__: function (other) { return this.__sub__(other); } }); return { uid: new py.float(this.uid), datetime: datetime, time: time, relativedelta: relativedelta }; }, /** * FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching */ test_eval: function (source, expected) { try { var ctx = this.test_eval_contexts(source.contexts); if (!_.isEqual(ctx, expected.context)) { console.group('Local context does not match remote, nothing is broken but please report to R&D (xmo)'); console.warn('source', source.contexts); console.warn('local', ctx); console.warn('remote', expected.context); console.groupEnd(); } } catch (e) { console.group('Failed to evaluate contexts, nothing is broken but please report to R&D (xmo)'); console.error(e); console.log('source', source.contexts); console.groupEnd(); } try { var dom = this.test_eval_domains(source.domains, this.test_eval_get_context()); if (!_.isEqual(dom, expected.domain)) { console.group('Local domain does not match remote, nothing is broken but please report to R&D (xmo)'); console.warn('source', source.domains); console.warn('local', dom); console.warn('remote', expected.domain); console.groupEnd(); } } catch (e) { console.group('Failed to evaluate domains, nothing is broken but please report to R&D (xmo)'); console.error(e); console.log('source', source.domains); console.groupEnd(); } try { var groups = this.test_eval_groupby(source.group_by_seq); if (!_.isEqual(groups, expected.group_by)) { console.group('Local groupby does not match remote, nothing is broken but please report to R&D (xmo)'); console.warn('source', source.group_by_seq); console.warn('local', groups); console.warn('remote', expected.group_by); console.groupEnd(); } } catch (e) { console.group('Failed to evaluate groupby, nothing is broken but please report to R&D (xmo)'); console.error(e); console.log('source', source.group_by_seq); console.groupEnd(); } }, test_eval_contexts: function (contexts) { var result_context = _.extend({}, this.user_context), self = this; _(contexts).each(function (ctx) { switch(ctx.__ref) { case 'context': _.extend(result_context, py.eval(ctx.__debug)); break; case 'compound_context': _.extend( result_context, self.test_eval_contexts(ctx.__contexts)); break; default: _.extend(result_context, ctx); } }); return result_context; }, test_eval_domains: function (domains, eval_context) { var result_domain = [], self = this; _(domains).each(function (dom) { switch(dom.__ref) { case 'domain': result_domain.push.apply( result_domain, py.eval(dom.__debug, eval_context)); break; case 'compound_domain': result_domain.push.apply( result_domain, self.test_eval_domains( dom.__domains, eval_context)); break; default: result_domain.push.apply( result_domain, dom); } }); return result_domain; }, test_eval_groupby: function (contexts) { var result_group = [], self = this; _(contexts).each(function (ctx) { var group; switch(ctx.__ref) { case 'context': group = py.eval(ctx.__debug).group_by; break; case 'compound_context': group = self.test_eval_contexts(ctx.__contexts).group_by; break; default: group = ctx.group_by } if (!group) { return; } if (typeof group === 'string') { result_group.push(group); } else if (group instanceof Array) { result_group.push.apply(result_group, group); } else { throw new Error('Got invalid groupby {{' + JSON.stringify(group) + '}}'); } }); return result_group; }, /** * Executes an RPC call, registering the provided callbacks. * * Registers a default error callback if none is provided, and handles * setting the correct session id and session context in the parameter * objects * * @param {String} url RPC endpoint * @param {Object} params call parameters * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ rpc: function(url, params, success_callback, error_callback) { var self = this; // url can be an $.ajax option object if (_.isString(url)) { url = { url: url }; } // Construct a JSON-RPC2 request, method is currently unused params.session_id = this.session_id; if (this.debug) params.debug = 1; var payload = { jsonrpc: '2.0', method: 'call', params: params, id: _.uniqueId('r') }; var deferred = $.Deferred(); this.on_rpc_request(); var aborter = params.aborter; delete params.aborter; var request = this.rpc_function(url, payload).then( function (response, textStatus, jqXHR) { self.on_rpc_response(); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); } deferred.resolve(response["result"], textStatus, jqXHR); } else if (response.error.data.type === "session_invalid") { self.uid = false; // TODO deprecate or use a deferred on login.do_ask_login() self.on_session_invalid(function() { self.rpc(url, payload.params, function() { deferred.resolve.apply(deferred, arguments); }, function() { deferred.reject.apply(deferred, arguments); }); }); } else { deferred.reject(response.error, $.Event()); } }, function(jqXHR, textStatus, errorThrown) { self.on_rpc_response(); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, data: {type: "xhr"+textStatus, debug: jqXHR.responseText, objects: [jqXHR, errorThrown] } }; deferred.reject(error, $.Event()); }); if (aborter) { aborter.abort_last = function () { if (!(request.isResolved() || request.isRejected())) { deferred.fail(function (error, event) { event.preventDefault(); }); request.abort(); } }; } // Allow deferred user to disable on_rpc_error in fail deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { self.on_rpc_error(error, event); } }); }).then(success_callback, error_callback).promise(); return deferred; }, /** * Raw JSON-RPC call * * @returns {jQuery.Deferred} ajax-webd deferred object */ rpc_json: function(url, payload) { var self = this; var ajax = _.extend({ type: "POST", dataType: 'json', contentType: 'application/json', data: JSON.stringify(payload), processData: false }, url); if (this.synch) ajax.async = false; return $.ajax(ajax); }, rpc_jsonp: function(url, payload) { var self = this; // extracted from payload to set on the url var data = { session_id: this.session_id, id: payload.id }; url.url = this.get_url(url.url); var ajax = _.extend({ type: "GET", dataType: 'jsonp', jsonp: 'jsonp', cache: false, data: data }, url); if (this.synch) ajax.async = false; var payload_str = JSON.stringify(payload); var payload_url = $.param({r:payload_str}); if(payload_url.length < 2000) { // Direct jsonp request ajax.data.r = payload_str; return $.ajax(ajax); } else { // Indirect jsonp request var ifid = _.uniqueId('oe_rpc_iframe'); var display = options.openerp.debug ? 'block' : 'none'; var $iframe = $(_.str.sprintf("", ifid, ifid, display)); var $form = $('
') .attr('method', 'POST') .attr('target', ifid) .attr('enctype', "multipart/form-data") .attr('action', ajax.url + '?' + $.param(data)) .append($('').attr('value', payload_str)) .hide() .appendTo($('body')); var cleanUp = function() { if ($iframe) { $iframe.unbind("load").attr("src", "javascript:false;").remove(); } $form.remove(); }; var deferred = $.Deferred(); // the first bind is fired up when the iframe is added to the DOM $iframe.bind('load', function() { // the second bind is fired up when the result of the form submission is received $iframe.unbind('load').bind('load', function() { $.ajax(ajax).always(function() { cleanUp(); }).then( function() { deferred.resolve.apply(deferred, arguments); }, function() { deferred.reject.apply(deferred, arguments); } ); }); // now that the iframe can receive data, we fill and submit the form $form.submit(); }); // append the iframe to the DOM (will trigger the first load) $form.after($iframe); return deferred; } }, on_rpc_request: function() { }, on_rpc_response: function() { }, on_rpc_error: function(error) { }, /** * Init a session, reloads from cookie, if it exists */ session_init: function () { var self = this; // TODO: session store in cookie should be optional this.session_id = this.get_cookie('session_id'); return this.session_reload().pipe(function(result) { var modules = openerp._modules.join(','); var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).pipe(self.do_load_qweb); if(self.session_is_valid()) { return deferred.pipe(function() { return self.load_modules(); }); } return deferred; }); }, /** * (re)loads the content of a session: db name, username, user id, session * context and status of the support contract * * @returns {$.Deferred} deferred indicating the session is done reloading */ session_reload: function () { var self = this; return this.rpc("/web/session/get_session_info", {}).then(function(result) { // If immediately follows a login (triggered by trying to restore // an invalid session or no session at all), refresh session data // (should not change, but just in case...) _.extend(self, { db: result.db, username: result.login, uid: result.uid, user_context: result.context, openerp_entreprise: result.openerp_entreprise }); }); }, session_is_valid: function() { return !!this.uid; }, /** * The session is validated either by login or by restoration of a previous session */ session_authenticate: function(db, login, password, _volatile) { var self = this; var base_location = document.location.protocol + '//' + document.location.host; var params = { db: db, login: login, password: password, base_location: base_location }; return this.rpc("/web/session/authenticate", params).pipe(function(result) { _.extend(self, { session_id: result.session_id, db: result.db, username: result.login, uid: result.uid, user_context: result.context, openerp_entreprise: result.openerp_entreprise }); if (!_volatile) { self.set_cookie('session_id', self.session_id); } return self.load_modules(); }); }, session_logout: function() { this.set_cookie('session_id', ''); return this.rpc("/web/session/destroy", {}); }, on_session_valid: function() { }, /** * Called when a rpc call fail due to an invalid session. * By default, it's a noop */ on_session_invalid: function(retry_callback) { }, /** * Fetches a cookie stored by an openerp session * * @private * @param name the cookie's name */ get_cookie: function (name) { if (!this.name) { return null; } var nameEQ = this.name + '|' + name + '='; var cookies = document.cookie.split(';'); for(var i=0; i', { 'href': self.get_url(file), 'rel': 'stylesheet', 'type': 'text/css' })); }); }, do_load_js: function(files) { var self = this; var d = $.Deferred(); if(files.length != 0) { var file = files.shift(); var tag = document.createElement('script'); tag.type = 'text/javascript'; tag.src = self.get_url(file); 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).then(function () { d.resolve(); }); }; var head = document.head || document.getElementsByTagName('head')[0]; head.appendChild(tag); } else { d.resolve(); } return d; }, do_load_qweb: function(files) { var self = this; _.each(files, function(file) { self.qweb_mutex.exec(function() { return self.rpc('/web/proxy/load', {path: file}).pipe(function(xml) { if (!xml) { return; } openerp.web.qweb.add_template(_.str.trim(xml)); }); }); }); return self.qweb_mutex.def; }, on_modules_loaded: function() { for(var j=0; j'); var complete = function () { if (options.complete) { options.complete(); } clearTimeout(timer); $form_data.remove(); $target.remove(); if (remove_form && $form) { $form.remove(); } }; var $target = $('