2 * Copyright (c) 2012, OpenERP S.A.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
14 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 The only dependencies of this file are underscore >= 1.3.1, jQuery >= 1.8.3 and
28 QWeb >= 1.0.0 . No dependencies shall be added.
30 This file must compile in EcmaScript 3 and work in IE7.
34 /* jshint es3: true */
37 function declare($, _, QWeb2) {
41 * Improved John Resig's inheritance, based on:
43 * Simple JavaScript Inheritance
44 * By John Resig http://ejohn.org/
49 * Defines The Class object. That object can be used to define and inherit classes using
50 * the extend() method.
54 * var Person = openerp.Class.extend({
55 * init: function(isDancing){
56 * this.dancing = isDancing;
59 * return this.dancing;
63 * The init() method act as a constructor. This class can be instancied this way:
65 * var person = new Person(true);
68 * The Person class can also be extended again:
70 * var Ninja = Person.extend({
72 * this._super( false );
75 * // Call the inherited version of dance()
76 * return this._super();
78 * swingSword: function(){
83 * When extending a class, each re-defined method can use this._super() to call the previous
84 * implementation of that method.
87 var initializing = false,
88 fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
89 // The web Class implementation (does nothing)
90 openerp.Class = function(){};
93 * Subclass an existing class
95 * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
97 openerp.Class.extend = function() {
98 var _super = this.prototype;
99 // Support mixins arguments
100 var args = _.toArray(arguments);
102 var prop = _.extend.apply(_,args);
104 // Instantiate a web class (but only create the instance,
105 // don't run the init constructor)
108 var prototype = new This();
109 initializing = false;
111 // Copy the properties over onto the new prototype
112 _.each(prop, function(val, name) {
113 // Check if we're overwriting an existing function
114 prototype[name] = typeof prop[name] == "function" &&
115 fnTest.test(prop[name]) ?
116 (function(name, fn) {
118 var tmp = this._super;
120 // Add a new ._super() method that is the same
121 // method but on the super-class
122 this._super = _super[name];
124 // The method only need to be bound temporarily, so
125 // we remove it when we're done executing
126 var ret = fn.apply(this, arguments);
131 })(name, prop[name]) :
135 // The dummy class constructor
137 if(this.constructor !== openerp.Class){
138 throw new Error("You can only instanciate objects with the 'new' operator");
140 // All construction is actually done in the init method
142 if (!initializing && this.init) {
143 var ret = this.init.apply(this, arguments);
144 if (ret) { return ret; }
148 Class.include = function (properties) {
149 _.each(properties, function(val, name) {
150 if (typeof properties[name] !== 'function'
151 || !fnTest.test(properties[name])) {
152 prototype[name] = properties[name];
153 } else if (typeof prototype[name] === 'function'
154 && prototype.hasOwnProperty(name)) {
155 prototype[name] = (function (name, fn, previous) {
157 var tmp = this._super;
158 this._super = previous;
159 var ret = fn.apply(this, arguments);
163 })(name, properties[name], prototype[name]);
164 } else if (typeof _super[name] === 'function') {
165 prototype[name] = (function (name, fn) {
167 var tmp = this._super;
168 this._super = _super[name];
169 var ret = fn.apply(this, arguments);
173 })(name, properties[name]);
178 // Populate our constructed prototype object
179 Class.prototype = prototype;
181 // Enforce the constructor to be what we expect
182 Class.constructor = Class;
184 // And make this class extendable
185 Class.extend = this.extend;
194 * Mixin to structure objects' life-cycles folowing a parent-children
195 * relationship. Each object can a have a parent and multiple children.
196 * When an object is destroyed, all its children are destroyed too releasing
197 * any resource they could have reserved before.
199 openerp.ParentedMixin = {
200 __parentedMixin : true,
202 this.__parentedDestroyed = false;
203 this.__parentedChildren = [];
204 this.__parentedParent = null;
207 * Set the parent of the current object. When calling this method, the
208 * parent will also be informed and will return the current object
209 * when its getChildren() method is called. If the current object did
210 * already have a parent, it is unregistered before, which means the
211 * previous parent will not return the current object anymore when its
212 * getChildren() method is called.
214 setParent : function(parent) {
215 if (this.getParent()) {
216 if (this.getParent().__parentedMixin) {
217 this.getParent().__parentedChildren = _.without(this
218 .getParent().getChildren(), this);
221 this.__parentedParent = parent;
222 if (parent && parent.__parentedMixin) {
223 parent.__parentedChildren.push(this);
227 * Return the current parent of the object (or null).
229 getParent : function() {
230 return this.__parentedParent;
233 * Return a list of the children of the current object.
235 getChildren : function() {
236 return _.clone(this.__parentedChildren);
239 * Returns true if destroy() was called on the current object.
241 isDestroyed : function() {
242 return this.__parentedDestroyed;
245 Utility method to only execute asynchronous actions if the current
246 object has not been destroyed.
248 @param {$.Deferred} promise The promise representing the asynchronous
250 @param {bool} [reject=false] If true, the returned promise will be
251 rejected with no arguments if the current
252 object is destroyed. If false, the
253 returned promise will never be resolved
255 @returns {$.Deferred} A promise that will mirror the given promise if
256 everything goes fine but will either be rejected
257 with no arguments or never resolved if the
258 current object is destroyed.
260 alive: function(promise, reject) {
262 return $.Deferred(function (def) {
263 promise.then(function () {
264 if (!self.isDestroyed()) {
265 def.resolve.apply(def, arguments);
268 if (!self.isDestroyed()) {
269 def.reject.apply(def, arguments);
271 }).always(function () {
273 // noop if def already resolved or rejected
276 // otherwise leave promise in limbo
281 * Inform the object it should destroy itself, releasing any
282 * resource it could have reserved.
284 destroy : function() {
285 _.each(this.getChildren(), function(el) {
288 this.setParent(undefined);
289 this.__parentedDestroyed = true;
294 * Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
296 * This class just handle the dispatching of events, it is not meant to be extended,
297 * nor used directly. All integration with parenting and automatic unregistration of
298 * events is done in EventDispatcherMixin.
300 * Copyright notice for the following Class:
302 * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
303 * Backbone may be freely distributed under the MIT license.
304 * For all details and documentation:
305 * http://backbonejs.org
308 var Events = openerp.Class.extend({
309 on : function(events, callback, context) {
311 events = events.split(/\s+/);
312 var calls = this._callbacks || (this._callbacks = {});
313 while ((ev = events.shift())) {
314 var list = calls[ev] || (calls[ev] = {});
315 var tail = list.tail || (list.tail = list.next = {});
316 tail.callback = callback;
317 tail.context = context;
318 list.tail = tail.next = {};
323 off : function(events, callback, context) {
326 delete this._callbacks;
327 } else if ((calls = this._callbacks)) {
328 events = events.split(/\s+/);
329 while ((ev = events.shift())) {
332 if (!callback || !node)
334 while ((node = node.next) && node.next) {
335 if (node.callback === callback
336 && (!context || node.context === context))
338 this.on(ev, node.callback, node.context);
345 callbackList: function() {
347 _.each(this._callbacks || {}, function(el, eventName) {
349 while ((node = node.next) && node.next) {
350 lst.push([eventName, node.callback, node.context]);
356 trigger : function(events) {
357 var event, node, calls, tail, args, all, rest;
358 if (!(calls = this._callbacks))
361 (events = events.split(/\s+/)).push(null);
362 // Save references to the current heads & tails.
363 while ((event = events.shift())) {
370 if (!(node = calls[event]))
377 rest = Array.prototype.slice.call(arguments, 1);
378 while ((node = events.pop())) {
380 args = node.event ? [ node.event ].concat(rest) : rest;
381 while ((node = node.next) !== tail) {
382 node.callback.apply(node.context || this, args);
390 Mixin containing an event system. Events are also registered by specifying the target object
391 (the object which will receive the event when it is raised). Both the event-emitting object
392 and the target object store or reference to each other. This is used to correctly remove all
393 reference to the event handler when any of the object is destroyed (when the destroy() method
394 from ParentedMixin is called). Removing those references is necessary to avoid memory leak
395 and phantom events (events which are raised and sent to a previously destroyed object).
397 openerp.EventDispatcherMixin = _.extend({}, openerp.ParentedMixin, {
398 __eventDispatcherMixin: true,
400 openerp.ParentedMixin.init.call(this);
401 this.__edispatcherEvents = new Events();
402 this.__edispatcherRegisteredEvents = [];
404 on: function(events, dest, func) {
406 if (!(func instanceof Function)) {
407 throw new Error("Event handler must be a function.");
409 events = events.split(/\s+/);
410 _.each(events, function(eventName) {
411 self.__edispatcherEvents.on(eventName, func, dest);
412 if (dest && dest.__eventDispatcherMixin) {
413 dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
418 off: function(events, dest, func) {
420 events = events.split(/\s+/);
421 _.each(events, function(eventName) {
422 self.__edispatcherEvents.off(eventName, func, dest);
423 if (dest && dest.__eventDispatcherMixin) {
424 dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
425 return !(el.name === eventName && el.func === func && el.source === self);
431 trigger: function(events) {
432 this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
435 destroy: function() {
437 _.each(this.__edispatcherRegisteredEvents, function(event) {
438 event.source.__edispatcherEvents.off(event.name, event.func, self);
440 this.__edispatcherRegisteredEvents = [];
441 _.each(this.__edispatcherEvents.callbackList(), function(cal) {
442 this.off(cal[0], cal[2], cal[1]);
444 this.__edispatcherEvents.off();
445 openerp.ParentedMixin.destroy.call(this);
449 openerp.PropertiesMixin = _.extend({}, openerp.EventDispatcherMixin, {
451 openerp.EventDispatcherMixin.init.call(this);
452 this.__getterSetterInternalMap = {};
454 set: function(arg1, arg2, arg3) {
457 if (typeof arg1 === "string") {
460 options = arg3 || {};
463 options = arg2 || {};
467 _.each(map, function(val, key) {
468 var tmp = self.__getterSetterInternalMap[key];
472 self.__getterSetterInternalMap[key] = val;
473 if (! options.silent)
474 self.trigger("change:" + key, self, {
480 self.trigger("change", self);
483 return this.__getterSetterInternalMap[key];
488 * Base class for all visual components. Provides a lot of functionalities helpful
489 * for the management of a part of the DOM.
492 * - Rendering with QWeb.
493 * - Life-cycle management and parenting (when a parent is destroyed, all its children are
495 * - Insertion in DOM.
497 * Guide to create implementations of the Widget class:
498 * ==============================================
500 * Here is a sample child class:
502 * MyWidget = openerp.base.Widget.extend({
503 * // the name of the QWeb template to use for rendering
504 * template: "MyQWebTemplate",
506 * init: function(parent) {
507 * this._super(parent);
508 * // stuff that you want to init before the rendering
510 * start: function() {
511 * // stuff you want to make after the rendering, `this.$el` holds a correct value
512 * this.$el.find(".my_button").click(/* an example of event binding * /);
514 * // if you have some asynchronous operations, it's a good idea to return
515 * // a promise in start()
516 * var promise = this.rpc(...);
521 * Now this class can simply be used with the following syntax:
523 * var my_widget = new MyWidget(this);
524 * my_widget.appendTo($(".some-div"));
526 * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
527 * DOM inside the ".some-div" div and its events were binded.
529 * And of course, when you don't need that widget anymore, just do:
531 * my_widget.destroy();
533 * That will kill the widget in a clean way and erase its content from the dom.
535 openerp.Widget = openerp.Class.extend(openerp.PropertiesMixin, {
543 * The name of the QWeb template that will be used for rendering. Must be
544 * redefined in subclasses or the default render() method can not be used.
550 * Constructs the widget and sets its parent if a parent is given.
552 * @constructs openerp.Widget
554 * @param {openerp.Widget} parent Binds the current instance to the given Widget instance.
555 * When that widget is destroyed by calling destroy(), the current instance will be
556 * destroyed too. Can be null.
558 init: function(parent) {
559 openerp.PropertiesMixin.init.call(this);
560 this.setParent(parent);
561 // Bind on_/do_* methods to this
562 // We might remove this automatic binding in the future
563 for (var name in this) {
564 if(typeof(this[name]) == "function") {
565 if((/^on_|^do_/).test(name)) {
566 this[name] = _.bind(this[name], this);
570 // FIXME: this should not be
571 this.setElement(this._make_descriptive());
574 * Destroys the current widget, also destroys all its children before destroying itself.
576 destroy: function() {
577 _.each(this.getChildren(), function(el) {
583 openerp.PropertiesMixin.destroy.call(this);
586 * Renders the current widget and appends it to the given jQuery object or Widget.
588 * @param target A jQuery object or a Widget instance.
590 appendTo: function(target) {
592 return this.__widgetRenderAndInsert(function(t) {
593 self.$el.appendTo(t);
597 * Renders the current widget and prepends it to the given jQuery object or Widget.
599 * @param target A jQuery object or a Widget instance.
601 prependTo: function(target) {
603 return this.__widgetRenderAndInsert(function(t) {
604 self.$el.prependTo(t);
608 * Renders the current widget and inserts it after to the given jQuery object or Widget.
610 * @param target A jQuery object or a Widget instance.
612 insertAfter: function(target) {
614 return this.__widgetRenderAndInsert(function(t) {
615 self.$el.insertAfter(t);
619 * Renders the current widget and inserts it before to the given jQuery object or Widget.
621 * @param target A jQuery object or a Widget instance.
623 insertBefore: function(target) {
625 return this.__widgetRenderAndInsert(function(t) {
626 self.$el.insertBefore(t);
630 * Renders the current widget and replaces the given jQuery object.
632 * @param target A jQuery object or a Widget instance.
634 replace: function(target) {
635 return this.__widgetRenderAndInsert(_.bind(function(t) {
636 this.$el.replaceAll(t);
639 __widgetRenderAndInsert: function(insertion, target) {
640 this.renderElement();
645 * Method called after rendering. Mostly used to bind actions, perform asynchronous
648 * By convention, this method should return an object that can be passed to $.when()
649 * to inform the caller when this widget has been initialized.
651 * @returns {jQuery.Deferred or any}
657 * Renders the element. The default implementation renders the widget using QWeb,
658 * `this.template` must be defined. The context given to QWeb contains the "widget"
659 * key that references `this`.
661 renderElement: function() {
664 $el = $(openerp.qweb.render(this.template, {widget: this}).trim());
666 $el = this._make_descriptive();
668 this.replaceElement($el);
671 * Re-sets the widget's root element and replaces the old root element
672 * (if any) by the new one in the DOM.
674 * @param {HTMLElement | jQuery} $el
677 replaceElement: function ($el) {
678 var $oldel = this.$el;
679 this.setElement($el);
680 if ($oldel && !$oldel.is(this.$el)) {
681 $oldel.replaceWith(this.$el);
686 * Re-sets the widget's root element (el/$el/$el).
689 * * re-delegating events
690 * * re-binding sub-elements
691 * * if the widget already had a root element, replacing the pre-existing
694 * @param {HTMLElement | jQuery} element new root element for the widget
697 setElement: function (element) {
698 // NB: completely useless, as WidgetMixin#init creates a $el
701 this.undelegateEvents();
704 this.$el = (element instanceof $) ? element : $(element);
705 this.el = this.$el[0];
707 this.delegateEvents();
712 * Utility function to build small DOM elements.
714 * @param {String} tagName name of the DOM element to create
715 * @param {Object} [attributes] map of DOM attributes to set on the element
716 * @param {String} [content] HTML content to set on the element
719 make: function (tagName, attributes, content) {
720 var el = document.createElement(tagName);
721 if (!_.isEmpty(attributes)) {
722 $(el).attr(attributes);
730 * Makes a potential root element from the declarative builder of the
736 _make_descriptive: function () {
737 var attrs = _.extend({}, this.attributes || {});
738 if (this.id) { attrs.id = this.id; }
739 if (this.className) { attrs['class'] = this.className; }
740 return $(this.make(this.tagName, attrs));
742 delegateEvents: function () {
743 var events = this.events;
744 if (_.isEmpty(events)) { return; }
746 for(var key in events) {
747 if (!events.hasOwnProperty(key)) { continue; }
749 var method = this.proxy(events[key]);
751 var match = /^(\S+)(\s+(.*))?$/.exec(key);
752 var event = match[1];
753 var selector = match[3];
755 event += '.widget_events';
757 this.$el.on(event, method);
759 this.$el.on(event, selector, method);
763 undelegateEvents: function () {
764 this.$el.off('.widget_events');
767 * Shortcut for ``this.$el.find(selector)``
769 * @param {String} selector CSS selector, rooted in $el
770 * @returns {jQuery} selector match
772 $: function(selector) {
773 if (selector === undefined)
775 return this.$el.find(selector);
778 * Proxies a method of the object, in order to keep the right ``this`` on
779 * method invocations.
781 * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
782 * even more so to ``jQuery.proxy`` with a fundamental difference: its
783 * resolution of the method being called is lazy, meaning it will use the
784 * method as it is when the proxy is called, not when the proxy is created.
786 * Other methods will fix the bound method to what it is when creating the
787 * binding/proxy, which is fine in most javascript code but problematic in
788 * OpenERP Web where developers may want to replace existing callbacks with
791 * The semantics of this precisely replace closing over the method call.
793 * @param {String|Function} method function or name of the method to invoke
794 * @returns {Function} proxied method
796 proxy: function (method) {
799 var fn = (typeof method === 'string') ? self[method] : method;
800 return fn.apply(self, arguments);
805 var genericJsonRpc = function(fct_name, params, fct) {
810 id: Math.floor(Math.random() * 1000 * 1000 * 1000)
813 var result = xhr.pipe(function(result) {
814 if (result.error !== undefined) {
815 console.error("Server application error", result.error);
816 return $.Deferred().reject("server", result.error);
818 return result.result;
821 //console.error("JsonRPC communication error", _.toArray(arguments));
822 var def = $.Deferred();
823 return def.reject.apply(def, ["communication"].concat(_.toArray(arguments)));
826 result.abort = function () { xhr.abort && xhr.abort(); };
831 * Replacer function for JSON.stringify, serializes Date objects to UTC
832 * datetime in the OpenERP Server format.
834 * However, if a serialized value has a toJSON method that method is called
835 * *before* the replacer is invoked. Date#toJSON exists, and thus the value
836 * passed to the replacer is a string, the original Date has to be fetched
837 * on the parent object (which is provided as the replacer's context).
843 function date_to_utc(k, v) {
845 if (!(value instanceof Date)) { return v; }
847 return openerp.datetime_to_str(value);
850 openerp.jsonRpc = function(url, fct_name, params, settings) {
851 return genericJsonRpc(fct_name, params, function(data) {
852 return $.ajax(url, _.extend({}, settings, {
856 data: JSON.stringify(data, date_to_utc),
857 contentType: 'application/json'
862 openerp.jsonpRpc = function(url, fct_name, params, settings) {
863 settings = settings || {};
864 return genericJsonRpc(fct_name, params, function(data) {
865 var payload_str = JSON.stringify(data, date_to_utc);
866 var payload_url = $.param({r:payload_str});
867 var force2step = settings.force2step || false;
868 delete settings.force2step;
869 var session_id = settings.session_id || null;
870 delete settings.session_id;
871 if (payload_url.length < 2000 && ! force2step) {
872 return $.ajax(url, _.extend({}, settings, {
878 data: {r: payload_str, session_id: session_id}
881 var args = {session_id: session_id, id: data.id};
882 var ifid = _.uniqueId('oe_rpc_iframe');
883 var html = "<iframe src='javascript:false;' name='" + ifid + "' id='" + ifid + "' style='display:none'></iframe>";
884 var $iframe = $(html);
885 var nurl = 'jsonp=1&' + $.param(args);
886 nurl = url.indexOf("?") !== -1 ? url + "&" + nurl : url + "?" + nurl;
887 var $form = $('<form>')
888 .attr('method', 'POST')
889 .attr('target', ifid)
890 .attr('enctype', "multipart/form-data")
891 .attr('action', nurl)
892 .append($('<input type="hidden" name="r" />').attr('value', payload_str))
894 .appendTo($('body'));
895 var cleanUp = function() {
897 $iframe.unbind("load").remove();
901 var deferred = $.Deferred();
902 // the first bind is fired up when the iframe is added to the DOM
903 $iframe.bind('load', function() {
904 // the second bind is fired up when the result of the form submission is received
905 $iframe.unbind('load').bind('load', function() {
912 data: {session_id: session_id, id: data.id}
913 }).always(function() {
916 deferred.resolve.apply(deferred, arguments);
918 deferred.reject.apply(deferred, arguments);
921 // now that the iframe can receive data, we fill and submit the form
924 // append the iframe to the DOM (will trigger the first load)
925 $form.after($iframe);
926 if (settings.timeout) {
927 realSetTimeout(function() {
929 }, settings.timeout);
936 openerp.loadCSS = function (url) {
937 if (!$('link[href="' + url + '"]').length) {
938 $('head').append($('<link>', {
945 openerp.loadJS = function (url) {
946 var def = $.Deferred();
947 if ($('script[src="' + url + '"]').length) {
950 var script = document.createElement('script');
951 script.type = 'text/javascript';
953 script.onload = script.onreadystatechange = function() {
954 if ((script.readyState && script.readyState != "loaded" && script.readyState != "complete") || script.onload_done) {
957 script.onload_done = true;
960 script.onerror = function () {
961 console.error("Error loading file", script.src);
964 var head = document.head || document.getElementsByTagName('head')[0];
965 head.appendChild(script);
969 openerp.loadBundle = function (name) {
971 openerp.loadCSS('/web/css/' + name),
972 openerp.loadJS('/web/js/' + name)
976 var realSetTimeout = function(fct, millis) {
977 var finished = new Date().getTime() + millis;
978 var wait = function() {
979 var current = new Date().getTime();
980 if (current < finished) {
981 setTimeout(wait, finished - current);
986 setTimeout(wait, millis);
989 openerp.Session = openerp.Class.extend(openerp.PropertiesMixin, {
991 'request': 'Request sent',
992 'response': 'Response received',
993 'response_failed': 'HTTP Error response or timeout received',
994 'error': 'The received response is an JSON-RPC error'
997 @constructs openerp.Session
999 @param parent The parent of the newly created object.
1000 @param {String} origin Url of the OpenERP server to contact with this session object
1001 or `null` if the server to contact is the origin server.
1002 @param {Dict} options A dictionary that can contain the following options:
1004 * "override_session": Default to false. If true, the current session object will
1005 not try to re-use a previously created session id stored in a cookie.
1006 * "session_id": Default to null. If specified, the specified session_id will be used
1007 by this session object. Specifying this option automatically implies that the option
1008 "override_session" is set to true.
1010 init: function(parent, origin, options) {
1011 openerp.PropertiesMixin.init.call(this, parent);
1012 options = options || {};
1014 this.session_id = options.session_id || null;
1015 this.override_session = options.override_session || !!options.session_id || false;
1016 this.avoid_recursion = false;
1017 this.use_cors = options.use_cors || false;
1020 setup: function(origin) {
1021 // must be able to customize server
1022 var window_origin = location.protocol + "//" + location.host;
1023 origin = origin ? origin.replace( /\/+$/, '') : window_origin;
1024 if (!_.isUndefined(this.origin) && this.origin !== origin)
1025 throw new Error('Session already bound to ' + this.origin);
1027 this.origin = origin;
1028 this.prefix = this.origin;
1029 this.server = this.origin; // keep chs happy
1030 this.origin_server = this.origin === window_origin;
1033 * (re)loads the content of a session: db name, username, user id, session
1034 * context and status of the support contract
1036 * @returns {$.Deferred} deferred indicating the session is done reloading
1038 session_reload: function () {
1040 return self.rpc("/web/session/get_session_info", {}).then(function(result) {
1041 delete result.session_id;
1042 _.extend(self, result);
1046 * The session is validated either by login or by restoration of a previous session
1048 session_authenticate: function(db, login, password) {
1050 var params = {db: db, login: login, password: password};
1051 return this.rpc("/web/session/authenticate", params).then(function(result) {
1053 return $.Deferred().reject();
1055 delete result.session_id;
1056 _.extend(self, result);
1059 check_session_id: function() {
1061 if (this.avoid_recursion || self.use_cors)
1063 if (this.session_id)
1064 return $.when(); // we already have the session id
1065 if (this.override_session || ! this.origin_server) {
1066 // If we don't use the origin server we consider we should always create a new session.
1067 // Even if some browsers could support cookies when using jsonp that behavior is
1068 // not consistent and the browser creators are tending to removing that feature.
1069 this.avoid_recursion = true;
1070 return this.rpc("/gen_session_id", {}).then(function(result) {
1071 self.session_id = result;
1072 }).always(function() {
1073 self.avoid_recursion = false;
1076 // normal use case, just use the cookie
1077 self.session_id = openerp.get_cookie("session_id");
1082 * Executes an RPC call, registering the provided callbacks.
1084 * Registers a default error callback if none is provided, and handles
1085 * setting the correct session id and session context in the parameter
1088 * @param {String} url RPC endpoint
1089 * @param {Object} params call parameters
1090 * @param {Object} options additional options for rpc call
1091 * @param {Function} success_callback function to execute on RPC call success
1092 * @param {Function} error_callback function to execute on RPC call failure
1093 * @returns {jQuery.Deferred} jquery-provided ajax deferred
1095 rpc: function(url, params, options) {
1097 options = _.clone(options || {});
1098 var shadow = options.shadow || false;
1099 delete options.shadow;
1101 return self.check_session_id().then(function() {
1103 if (! _.isString(url)) {
1104 _.extend(options, url);
1107 // TODO correct handling of timeouts
1109 self.trigger('request');
1111 if (self.origin_server) {
1112 fct = openerp.jsonRpc;
1113 if (self.override_session) {
1114 options.headers = _.extend({}, options.headers, {
1115 "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
1118 } else if (self.use_cors) {
1119 fct = openerp.jsonRpc;
1120 url = self.url(url, null);
1121 options.session_id = self.session_id || '';
1122 if (self.override_session) {
1123 options.headers = _.extend({}, options.headers, {
1124 "X-Openerp-Session-Id": self.override_session ? self.session_id || '' : ''
1128 fct = openerp.jsonpRpc;
1129 url = self.url(url, null);
1130 options.session_id = self.session_id || '';
1132 var p = fct(url, "call", params, options);
1133 p = p.then(function (result) {
1135 self.trigger('response');
1137 }, function(type, error, textStatus, errorThrown) {
1138 if (type === "server") {
1140 self.trigger('response');
1141 if (error.code === 100) {
1144 return $.Deferred().reject(error, $.Event());
1147 self.trigger('response_failed');
1150 message: "XmlHttpRequestError " + errorThrown,
1151 data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
1153 return $.Deferred().reject(nerror, $.Event());
1156 return p.fail(function() { // Allow deferred user to disable rpc_error call in fail
1157 p.fail(function(error, event) {
1158 if (!event.isDefaultPrevented()) {
1159 self.trigger('error', error, event);
1165 url: function(path, params) {
1166 params = _.extend(params || {});
1167 if (this.override_session || (! this.origin_server))
1168 params.session_id = this.session_id;
1169 var qs = $.param(params);
1172 var prefix = _.any(['http://', 'https://', '//'], function(el) {
1173 return path.length >= el.length && path.slice(0, el.length) === el;
1174 }) ? '' : this.prefix;
1175 return prefix + path + qs;
1177 model: function(model_name) {
1178 return new openerp.Model(this, model_name);
1182 openerp.Model = openerp.Class.extend({
1184 new openerp.Model([session,] model_name)
1186 @constructs instance.Model
1187 @extends instance.Class
1189 @param {openerp.Session} [session] The session object used to communicate with
1191 @param {String} model_name name of the OpenERP model this object is bound to
1192 @param {Object} [context]
1193 @param {Array} [domain]
1196 var session, model_name;
1197 var args = _.toArray(arguments);
1199 session = args.pop();
1200 if (session && ! (session instanceof openerp.Session)) {
1201 model_name = session;
1204 model_name = args.pop();
1207 this.name = model_name;
1208 this._session = session;
1210 session: function() {
1211 if (! this._session)
1212 throw new Error("Not session specified");
1213 return this._session;
1216 * Call a method (over RPC) on the bound OpenERP model.
1218 * @param {String} method name of the method to call
1219 * @param {Array} [args] positional arguments
1220 * @param {Object} [kwargs] keyword arguments
1221 * @param {Object} [options] additional options for the rpc() method
1222 * @returns {jQuery.Deferred<>} call result
1224 call: function (method, args, kwargs, options) {
1226 kwargs = kwargs || {};
1227 if (!_.isArray(args)) {
1228 // call(method, kwargs)
1232 var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
1233 return this.session().rpc(call_kw, {
1242 /** OpenERP Translations */
1243 openerp.TranslationDataBase = openerp.Class.extend(/** @lends instance.TranslationDataBase# */{
1245 * @constructs instance.TranslationDataBase
1246 * @extends instance.Class
1250 this.parameters = {"direction": 'ltr',
1251 "date_format": '%m/%d/%Y',
1252 "time_format": '%H:%M:%S',
1254 "decimal_point": ".",
1255 "thousands_sep": ","};
1257 set_bundle: function(translation_bundle) {
1260 var modules = _.keys(translation_bundle.modules);
1262 if (_.include(modules, "web")) {
1263 modules = ["web"].concat(_.without(modules, "web"));
1265 _.each(modules, function(name) {
1266 self.add_module_translation(translation_bundle.modules[name]);
1268 if (translation_bundle.lang_parameters) {
1269 this.parameters = translation_bundle.lang_parameters;
1272 add_module_translation: function(mod) {
1274 _.each(mod.messages, function(message) {
1275 self.db[message.id] = message.string;
1278 build_translation_function: function() {
1280 var fcnt = function(str) {
1281 var tmp = self.get(str);
1282 return tmp === undefined ? str : tmp;
1284 fcnt.database = this;
1287 get: function(key) {
1288 return this.db[key];
1291 Loads the translations from an OpenERP server.
1293 @param {openerp.Session} session The session object to contact the server.
1294 @param {Array} [modules] The list of modules to load the translation. If not specified,
1295 it will default to all the modules installed in the current database.
1296 @param {Object} [lang] lang The language. If not specified it will default to the language
1297 of the current user.
1298 @returns {jQuery.Deferred}
1300 load_translations: function(session, modules, lang) {
1302 return session.rpc('/web/webclient/translations', {
1303 "mods": modules || null,
1304 "lang": lang || null
1305 }).done(function(trans) {
1306 self.set_bundle(trans);
1311 openerp._t = new openerp.TranslationDataBase().build_translation_function();
1313 openerp.get_cookie = function(c_name) {
1314 var cookies = document.cookie ? document.cookie.split('; ') : [];
1315 for (var i = 0, l = cookies.length; i < l; i++) {
1316 var parts = cookies[i].split('=');
1317 var name = parts.shift();
1318 var cookie = parts.join('=');
1320 if (c_name && c_name === name) {
1327 openerp.qweb = new QWeb2.Engine();
1329 openerp.qweb.default_dict = {
1335 openerp.Mutex = openerp.Class.extend({
1337 this.def = $.Deferred().resolve();
1339 exec: function(action) {
1340 var current = this.def;
1341 var next = this.def = $.Deferred();
1342 return current.then(function() {
1343 return $.when(action()).always(function() {
1351 * Converts a string to a Date javascript object using OpenERP's
1352 * datetime string format (exemple: '2011-12-01 15:12:35.832').
1354 * The time zone is assumed to be UTC (standard for OpenERP 6.1)
1355 * and will be converted to the browser's time zone.
1357 * @param {String} str A string representing a datetime.
1360 openerp.str_to_datetime = function(str) {
1364 var regex = /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
1365 var res = regex.exec(str);
1367 throw new Error("'" + str + "' is not a valid datetime");
1369 var tmp = new Date(2000,0,1);
1370 tmp.setUTCMonth(1970);
1373 tmp.setUTCFullYear(parseFloat(res[1]));
1374 tmp.setUTCMonth(parseFloat(res[2]) - 1);
1375 tmp.setUTCDate(parseFloat(res[3]));
1376 tmp.setUTCHours(parseFloat(res[4]));
1377 tmp.setUTCMinutes(parseFloat(res[5]));
1378 tmp.setUTCSeconds(parseFloat(res[6]));
1379 tmp.setUTCSeconds(parseFloat(res[6]));
1380 tmp.setUTCMilliseconds(parseFloat(rpad((res[7] || "").slice(0, 3), 3)));
1385 * Converts a string to a Date javascript object using OpenERP's
1386 * date string format (exemple: '2011-12-01').
1388 * As a date is not subject to time zones, we assume it should be
1389 * represented as a Date javascript object at 00:00:00 in the
1390 * time zone of the browser.
1392 * @param {String} str A string representing a date.
1395 openerp.str_to_date = function(str) {
1399 var regex = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
1400 var res = regex.exec(str);
1402 throw new Error("'" + str + "' is not a valid date");
1404 var tmp = new Date(2000,0,1);
1405 tmp.setFullYear(parseFloat(res[1]));
1406 tmp.setMonth(parseFloat(res[2]) - 1);
1407 tmp.setDate(parseFloat(res[3]));
1415 * Converts a string to a Date javascript object using OpenERP's
1416 * time string format (exemple: '15:12:35').
1418 * The OpenERP times are supposed to always be naive times. We assume it is
1419 * represented using a javascript Date with a date 1 of January 1970 and a
1420 * time corresponding to the meant time in the browser's time zone.
1422 * @param {String} str A string representing a time.
1425 openerp.str_to_time = function(str) {
1429 var regex = /^(\d\d):(\d\d):(\d\d(?:\.(\d+))?)$/;
1430 var res = regex.exec(str);
1432 throw new Error("'" + str + "' is not a valid time");
1434 var tmp = new Date();
1435 tmp.setFullYear(1970);
1438 tmp.setHours(parseFloat(res[1]));
1439 tmp.setMinutes(parseFloat(res[2]));
1440 tmp.setSeconds(parseFloat(res[3]));
1441 tmp.setMilliseconds(parseFloat(rpad((res[4] || "").slice(0, 3), 3)));
1446 * Left-pad provided arg 1 with zeroes until reaching size provided by second
1449 * @param {Number|String} str value to pad
1450 * @param {Number} size size to reach on the final padded value
1451 * @returns {String} padded string
1453 var lpad = function(str, size) {
1455 return new Array(size - str.length + 1).join('0') + str;
1458 var rpad = function(str, size) {
1460 return str + new Array(size - str.length + 1).join('0');
1464 * Converts a Date javascript object to a string using OpenERP's
1465 * datetime string format (exemple: '2011-12-01 15:12:35').
1467 * The time zone of the Date object is assumed to be the one of the
1468 * browser and it will be converted to UTC (standard for OpenERP 6.1).
1471 * @returns {String} A string representing a datetime.
1473 openerp.datetime_to_str = function(obj) {
1477 return lpad(obj.getUTCFullYear(),4) + "-" + lpad(obj.getUTCMonth() + 1,2) + "-"
1478 + lpad(obj.getUTCDate(),2) + " " + lpad(obj.getUTCHours(),2) + ":"
1479 + lpad(obj.getUTCMinutes(),2) + ":" + lpad(obj.getUTCSeconds(),2);
1483 * Converts a Date javascript object to a string using OpenERP's
1484 * date string format (exemple: '2011-12-01').
1486 * As a date is not subject to time zones, we assume it should be
1487 * represented as a Date javascript object at 00:00:00 in the
1488 * time zone of the browser.
1491 * @returns {String} A string representing a date.
1493 openerp.date_to_str = function(obj) {
1497 return lpad(obj.getFullYear(),4) + "-" + lpad(obj.getMonth() + 1,2) + "-"
1498 + lpad(obj.getDate(),2);
1502 * Converts a Date javascript object to a string using OpenERP's
1503 * time string format (exemple: '15:12:35').
1505 * The OpenERP times are supposed to always be naive times. We assume it is
1506 * represented using a javascript Date with a date 1 of January 1970 and a
1507 * time corresponding to the meant time in the browser's time zone.
1510 * @returns {String} A string representing a time.
1512 openerp.time_to_str = function(obj) {
1516 return lpad(obj.getHours(),2) + ":" + lpad(obj.getMinutes(),2) + ":"
1517 + lpad(obj.getSeconds(),2);
1520 // jQuery custom plugins
1521 jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function(arg) {
1522 return function( elem ) {
1523 return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
1527 openerp.declare = declare;
1532 if (typeof(define) !== "undefined") { // amd
1533 define(["jquery", "underscore", "qweb2"], declare);
1535 window.openerp = declare($, _, QWeb2);