diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py
index d06c42bb935..1bf81956dc3 100644
--- a/addons/base/controllers/main.py
+++ b/addons/base/controllers/main.py
@@ -9,10 +9,7 @@ import openerpweb
import openerpweb.ast
import openerpweb.nonliterals
-__all__ = ['Session', 'Menu', 'DataSet', 'DataRecord',
- 'View', 'FormView', 'ListView', 'SearchView',
- 'Action']
-
+# Should move to openerpweb.Xml2Json
class Xml2Json:
# xml2json-direct
# Simple and straightforward XML-to-JSON converter in Python
@@ -280,12 +277,9 @@ class DataSet(openerpweb.Controller):
return {'fields': req.session.model(model).fields_get()}
@openerpweb.jsonrequest
- def find(self, request, model, fields=False, offset=0, limit=False,
- domain=None, context=None, sort=None):
- return self.do_find(request, model, fields, offset, limit,
- domain, context, sort)
- def do_find(self, request, model, fields=False, offset=0, limit=False,
- domain=None, context=None, sort=None):
+ def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
+ return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
+ def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
""" Performs a search() followed by a read() (if needed) using the
provided search criteria
@@ -313,7 +307,6 @@ class DataSet(openerpweb.Controller):
@openerpweb.jsonrequest
def get(self, request, model, ids, fields=False):
return self.do_get(request, model, ids, fields)
-
def do_get(self, request, model, ids, fields=False):
""" Fetches and returns the records of the model ``model`` whose ids
are in ``ids``.
@@ -336,11 +329,8 @@ class DataSet(openerpweb.Controller):
record_map = dict((record['id'], record) for record in records)
return [record_map[id] for id in ids if record_map.get(id)]
-
-class DataRecord(openerpweb.Controller):
- _cp_path = "/base/datarecord"
-
@openerpweb.jsonrequest
+
def load(self, req, model, id, fields):
m = req.session.model(model)
value = {}
@@ -350,9 +340,15 @@ class DataRecord(openerpweb.Controller):
return {'value': value}
@openerpweb.jsonrequest
- def save(self, req, model, id, data):
+ def save(self, req, model, id, data, context={}):
m = req.session.model(model)
- r = m.write([id], data)
+ r = m.write([id], data, context)
+ return {'result': r}
+
+ @openerpweb.jsonrequest
+ def call(self, req, model, method, ids, args):
+ m = req.session.model(model)
+ r = getattr(m, method)(ids, *args)
return {'result': r}
class View(openerpweb.Controller):
@@ -466,7 +462,6 @@ class FormView(View):
fields_view = self.fields_view_get(req.session, model, view_id, 'form', toolbar=toolbar)
return {'fields_view': fields_view}
-
class ListView(View):
_cp_path = "/base/listview"
@@ -475,7 +470,6 @@ class ListView(View):
fields_view = self.fields_view_get(req.session, model, view_id, 'tree', toolbar=toolbar)
return {'fields_view': fields_view}
-
class SearchView(View):
_cp_path = "/base/searchview"
@@ -484,7 +478,6 @@ class SearchView(View):
fields_view = self.fields_view_get(req.session, model, view_id, 'search')
return {'fields_view': fields_view}
-
class Action(openerpweb.Controller):
_cp_path = "/base/action"
diff --git a/addons/base/static/lib/jquery/jquery-1.5.1.js b/addons/base/static/lib/jquery/jquery-1.5.2.js
similarity index 97%
rename from addons/base/static/lib/jquery/jquery-1.5.1.js
rename to addons/base/static/lib/jquery/jquery-1.5.2.js
index ade6b94651c..ae0234d2bb4 100644
--- a/addons/base/static/lib/jquery/jquery-1.5.1.js
+++ b/addons/base/static/lib/jquery/jquery-1.5.2.js
@@ -1,5 +1,5 @@
/*!
- * jQuery JavaScript Library v1.5.1
+ * jQuery JavaScript Library v1.5.2
* http://jquery.com/
*
* Copyright 2011, John Resig
@@ -11,7 +11,7 @@
* Copyright 2011, The Dojo Foundation
* Released under the MIT, BSD, and GPL Licenses.
*
- * Date: Wed Feb 23 13:55:29 2011 -0500
+ * Date: Thu Mar 31 15:28:23 2011 -0400
*/
(function( window, undefined ) {
@@ -69,15 +69,9 @@ var jQuery = function( selector, context ) {
// For matching the engine and version of the browser
browserMatch,
- // Has the ready events already been bound?
- readyBound = false,
-
// The deferred used on DOM ready
readyList,
- // Promise methods
- promiseMethods = "then done fail isResolved isRejected promise".split( " " ),
-
// The ready event handler
DOMContentLoaded,
@@ -202,7 +196,7 @@ jQuery.fn = jQuery.prototype = {
selector: "",
// The current version of jQuery being used
- jquery: "1.5.1",
+ jquery: "1.5.2",
// The default length of a jQuery object is 0
length: 0,
@@ -427,11 +421,11 @@ jQuery.extend({
},
bindReady: function() {
- if ( readyBound ) {
+ if ( readyList ) {
return;
}
- readyBound = true;
+ readyList = jQuery._Deferred();
// Catch cases where $(document).ready() is called after the
// browser event has already occurred.
@@ -811,171 +805,6 @@ jQuery.extend({
return (new Date()).getTime();
},
- // Create a simple deferred (one callbacks list)
- _Deferred: function() {
- var // callbacks list
- callbacks = [],
- // stored [ context , args ]
- fired,
- // to avoid firing when already doing so
- firing,
- // flag to know if the deferred has been cancelled
- cancelled,
- // the deferred itself
- deferred = {
-
- // done( f1, f2, ...)
- done: function() {
- if ( !cancelled ) {
- var args = arguments,
- i,
- length,
- elem,
- type,
- _fired;
- if ( fired ) {
- _fired = fired;
- fired = 0;
- }
- for ( i = 0, length = args.length; i < length; i++ ) {
- elem = args[ i ];
- type = jQuery.type( elem );
- if ( type === "array" ) {
- deferred.done.apply( deferred, elem );
- } else if ( type === "function" ) {
- callbacks.push( elem );
- }
- }
- if ( _fired ) {
- deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
- }
- }
- return this;
- },
-
- // resolve with given context and args
- resolveWith: function( context, args ) {
- if ( !cancelled && !fired && !firing ) {
- firing = 1;
- try {
- while( callbacks[ 0 ] ) {
- callbacks.shift().apply( context, args );
- }
- }
- // We have to add a catch block for
- // IE prior to 8 or else the finally
- // block will never get executed
- // catch (e) {
- // throw e;
- // }
- finally {
- fired = [ context, args ];
- firing = 0;
- }
- }
- return this;
- },
-
- // resolve with this as context and given arguments
- resolve: function() {
- deferred.resolveWith( jQuery.isFunction( this.promise ) ? this.promise() : this, arguments );
- return this;
- },
-
- // Has this deferred been resolved?
- isResolved: function() {
- return !!( firing || fired );
- },
-
- // Cancel
- cancel: function() {
- cancelled = 1;
- callbacks = [];
- return this;
- }
- };
-
- return deferred;
- },
-
- // Full fledged deferred (two callbacks list)
- Deferred: function( func ) {
- var deferred = jQuery._Deferred(),
- failDeferred = jQuery._Deferred(),
- promise;
- // Add errorDeferred methods, then and promise
- jQuery.extend( deferred, {
- then: function( doneCallbacks, failCallbacks ) {
- deferred.done( doneCallbacks ).fail( failCallbacks );
- return this;
- },
- fail: failDeferred.done,
- rejectWith: failDeferred.resolveWith,
- reject: failDeferred.resolve,
- isRejected: failDeferred.isResolved,
- // Get a promise for this deferred
- // If obj is provided, the promise aspect is added to the object
- promise: function( obj ) {
- if ( obj == null ) {
- if ( promise ) {
- return promise;
- }
- promise = obj = {};
- }
- var i = promiseMethods.length;
- while( i-- ) {
- obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
- }
- return obj;
- }
- } );
- // Make sure only one callback list will be used
- deferred.done( failDeferred.cancel ).fail( deferred.cancel );
- // Unexpose cancel
- delete deferred.cancel;
- // Call given func if any
- if ( func ) {
- func.call( deferred, deferred );
- }
- return deferred;
- },
-
- // Deferred helper
- when: function( object ) {
- var lastIndex = arguments.length,
- deferred = lastIndex <= 1 && object && jQuery.isFunction( object.promise ) ?
- object :
- jQuery.Deferred(),
- promise = deferred.promise();
-
- if ( lastIndex > 1 ) {
- var array = slice.call( arguments, 0 ),
- count = lastIndex,
- iCallback = function( index ) {
- return function( value ) {
- array[ index ] = arguments.length > 1 ? slice.call( arguments, 0 ) : value;
- if ( !( --count ) ) {
- deferred.resolveWith( promise, array );
- }
- };
- };
- while( ( lastIndex-- ) ) {
- object = array[ lastIndex ];
- if ( object && jQuery.isFunction( object.promise ) ) {
- object.promise().then( iCallback(lastIndex), deferred.reject );
- } else {
- --count;
- }
- }
- if ( !count ) {
- deferred.resolveWith( promise, array );
- }
- } else if ( deferred !== object ) {
- deferred.resolve( object );
- }
- return promise;
- },
-
// Use of jQuery.browser is frowned upon.
// More details: http://docs.jquery.com/Utilities/jQuery.browser
uaMatch: function( ua ) {
@@ -1014,9 +843,6 @@ jQuery.extend({
browser: {}
});
-// Create readyList deferred
-readyList = jQuery._Deferred();
-
// Populate the class2type map
jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
@@ -1090,6 +916,178 @@ return jQuery;
})();
+var // Promise methods
+ promiseMethods = "then done fail isResolved isRejected promise".split( " " ),
+ // Static reference to slice
+ sliceDeferred = [].slice;
+
+jQuery.extend({
+ // Create a simple deferred (one callbacks list)
+ _Deferred: function() {
+ var // callbacks list
+ callbacks = [],
+ // stored [ context , args ]
+ fired,
+ // to avoid firing when already doing so
+ firing,
+ // flag to know if the deferred has been cancelled
+ cancelled,
+ // the deferred itself
+ deferred = {
+
+ // done( f1, f2, ...)
+ done: function() {
+ if ( !cancelled ) {
+ var args = arguments,
+ i,
+ length,
+ elem,
+ type,
+ _fired;
+ if ( fired ) {
+ _fired = fired;
+ fired = 0;
+ }
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = jQuery.type( elem );
+ if ( type === "array" ) {
+ deferred.done.apply( deferred, elem );
+ } else if ( type === "function" ) {
+ callbacks.push( elem );
+ }
+ }
+ if ( _fired ) {
+ deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+ }
+ }
+ return this;
+ },
+
+ // resolve with given context and args
+ resolveWith: function( context, args ) {
+ if ( !cancelled && !fired && !firing ) {
+ // make sure args are available (#8421)
+ args = args || [];
+ firing = 1;
+ try {
+ while( callbacks[ 0 ] ) {
+ callbacks.shift().apply( context, args );
+ }
+ }
+ finally {
+ fired = [ context, args ];
+ firing = 0;
+ }
+ }
+ return this;
+ },
+
+ // resolve with this as context and given arguments
+ resolve: function() {
+ deferred.resolveWith( this, arguments );
+ return this;
+ },
+
+ // Has this deferred been resolved?
+ isResolved: function() {
+ return !!( firing || fired );
+ },
+
+ // Cancel
+ cancel: function() {
+ cancelled = 1;
+ callbacks = [];
+ return this;
+ }
+ };
+
+ return deferred;
+ },
+
+ // Full fledged deferred (two callbacks list)
+ Deferred: function( func ) {
+ var deferred = jQuery._Deferred(),
+ failDeferred = jQuery._Deferred(),
+ promise;
+ // Add errorDeferred methods, then and promise
+ jQuery.extend( deferred, {
+ then: function( doneCallbacks, failCallbacks ) {
+ deferred.done( doneCallbacks ).fail( failCallbacks );
+ return this;
+ },
+ fail: failDeferred.done,
+ rejectWith: failDeferred.resolveWith,
+ reject: failDeferred.resolve,
+ isRejected: failDeferred.isResolved,
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ if ( obj == null ) {
+ if ( promise ) {
+ return promise;
+ }
+ promise = obj = {};
+ }
+ var i = promiseMethods.length;
+ while( i-- ) {
+ obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
+ }
+ return obj;
+ }
+ } );
+ // Make sure only one callback list will be used
+ deferred.done( failDeferred.cancel ).fail( deferred.cancel );
+ // Unexpose cancel
+ delete deferred.cancel;
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+ return deferred;
+ },
+
+ // Deferred helper
+ when: function( firstParam ) {
+ var args = arguments,
+ i = 0,
+ length = args.length,
+ count = length,
+ deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
+ firstParam :
+ jQuery.Deferred();
+ function resolveFunc( i ) {
+ return function( value ) {
+ args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
+ if ( !( --count ) ) {
+ // Strange bug in FF4:
+ // Values changed onto the arguments object sometimes end up as undefined values
+ // outside the $.when method. Cloning the object into a fresh array solves the issue
+ deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) );
+ }
+ };
+ }
+ if ( length > 1 ) {
+ for( ; i < length; i++ ) {
+ if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) {
+ args[ i ].promise().then( resolveFunc(i), deferred.reject );
+ } else {
+ --count;
+ }
+ }
+ if ( !count ) {
+ deferred.resolveWith( deferred, args );
+ }
+ } else if ( deferred !== firstParam ) {
+ deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
+ }
+ return deferred.promise();
+ }
+});
+
+
+
+
(function() {
jQuery.support = {};
@@ -1157,7 +1155,8 @@ return jQuery;
boxModel: null,
inlineBlockNeedsLayout: false,
shrinkWrapBlocks: false,
- reliableHiddenOffsets: true
+ reliableHiddenOffsets: true,
+ reliableMarginRight: true
};
input.checked = true;
@@ -1175,15 +1174,15 @@ return jQuery;
script = document.createElement("script"),
id = "script" + jQuery.now();
+ // Make sure that the execution of code works by injecting a script
+ // tag with appendChild/createTextNode
+ // (IE doesn't support this, fails, and uses .text instead)
try {
script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
} catch(e) {}
root.insertBefore( script, root.firstChild );
- // Make sure that the execution of code works by injecting a script
- // tag with appendChild/createTextNode
- // (IE doesn't support this, fails, and uses .text instead)
if ( window[ id ] ) {
_scriptEval = true;
delete window[ id ];
@@ -1192,8 +1191,6 @@ return jQuery;
}
root.removeChild( script );
- // release memory in IE
- root = script = id = null;
}
return _scriptEval;
@@ -1278,6 +1275,17 @@ return jQuery;
jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0;
div.innerHTML = "";
+ // Check if div with explicit width and no margin-right incorrectly
+ // gets computed margin-right based on width of container. For more
+ // info see bug #3333
+ // Fails in WebKit before Feb 2011 nightlies
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ if ( document.defaultView && document.defaultView.getComputedStyle ) {
+ div.style.width = "1px";
+ div.style.marginRight = "0";
+ jQuery.support.reliableMarginRight = ( parseInt(document.defaultView.getComputedStyle(div, null).marginRight, 10) || 0 ) === 0;
+ }
+
body.removeChild( div ).style.display = "none";
div = tds = null;
});
@@ -1301,8 +1309,6 @@ return jQuery;
el.setAttribute(eventName, "return;");
isSupported = typeof el[eventName] === "function";
}
- el = null;
-
return isSupported;
};
@@ -2194,10 +2200,10 @@ jQuery.event = {
}
if ( !eventHandle ) {
- elemData.handle = eventHandle = function() {
+ elemData.handle = eventHandle = function( e ) {
// Handle the second event of a trigger and when
// an event is called after a page has unloaded
- return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
+ return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
jQuery.event.handle.apply( eventHandle.elem, arguments ) :
undefined;
};
@@ -2504,7 +2510,7 @@ jQuery.event = {
target[ "on" + targetType ] = null;
}
- jQuery.event.triggered = true;
+ jQuery.event.triggered = event.type;
target[ targetType ]();
}
@@ -2515,7 +2521,7 @@ jQuery.event = {
target[ "on" + targetType ] = old;
}
- jQuery.event.triggered = false;
+ jQuery.event.triggered = undefined;
}
}
},
@@ -2785,7 +2791,7 @@ var withinElement = function( event ) {
// Chrome does something similar, the parentNode property
// can be accessed but is null.
- if ( parent !== document && !parent.parentNode ) {
+ if ( parent && parent !== document && !parent.parentNode ) {
return;
}
// Traverse up the tree
@@ -2992,19 +2998,33 @@ function trigger( type, elem, args ) {
// Create "bubbling" focus and blur events
if ( document.addEventListener ) {
jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+ // Attach a single capturing handler while someone wants focusin/focusout
+ var attaches = 0;
+
jQuery.event.special[ fix ] = {
setup: function() {
- this.addEventListener( orig, handler, true );
+ if ( attaches++ === 0 ) {
+ document.addEventListener( orig, handler, true );
+ }
},
teardown: function() {
- this.removeEventListener( orig, handler, true );
+ if ( --attaches === 0 ) {
+ document.removeEventListener( orig, handler, true );
+ }
}
};
- function handler( e ) {
- e = jQuery.event.fix( e );
+ function handler( donor ) {
+ // Donor event is always a native one; fix it and switch its type.
+ // Let focusin/out handler cancel the donor focus/blur event.
+ var e = jQuery.event.fix( donor );
e.type = fix;
- return jQuery.event.handle.call( this, e );
+ e.originalEvent = {};
+ jQuery.event.trigger( e, null, e.target );
+ if ( e.isDefaultPrevented() ) {
+ donor.preventDefault();
+ }
}
});
}
@@ -3918,10 +3938,12 @@ var Expr = Sizzle.selectors = {
},
text: function( elem ) {
+ var attr = elem.getAttribute( "type" ), type = elem.type;
// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
// use getAttribute instead to test this case
- return "text" === elem.getAttribute( 'type' );
+ return "text" === type && ( attr === type || attr === null );
},
+
radio: function( elem ) {
return "radio" === elem.type;
},
@@ -4496,19 +4518,23 @@ if ( document.querySelectorAll ) {
(function(){
var html = document.documentElement,
- matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector,
- pseudoWorks = false;
-
- try {
- // This should fail with an exception
- // Gecko does not error, returns false instead
- matches.call( document.documentElement, "[test!='']:sizzle" );
-
- } catch( pseudoError ) {
- pseudoWorks = true;
- }
+ matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
if ( matches ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9 fails this)
+ var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ),
+ pseudoWorks = false;
+
+ try {
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( document.documentElement, "[test!='']:sizzle" );
+
+ } catch( pseudoError ) {
+ pseudoWorks = true;
+ }
+
Sizzle.matchesSelector = function( node, expr ) {
// Make sure that attribute selectors are quoted
expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
@@ -4516,7 +4542,15 @@ if ( document.querySelectorAll ) {
if ( !Sizzle.isXML( node ) ) {
try {
if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
- return matches.call( node, expr );
+ var ret = matches.call( node, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || !disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9, so check for that
+ node.document && node.document.nodeType !== 11 ) {
+ return ret;
+ }
}
} catch(e) {}
}
@@ -5260,7 +5294,9 @@ jQuery.fn.extend({
}
});
} else {
- return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value );
+ return this.length ?
+ this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+ this;
}
},
@@ -5707,7 +5743,8 @@ function evalScript( i, elem ) {
var ralpha = /alpha\([^)]*\)/i,
ropacity = /opacity=([^)]*)/,
rdashAlpha = /-([a-z])/ig,
- rupper = /([A-Z])/g,
+ // fixed for IE9, see #8346
+ rupper = /([A-Z]|^ms)/g,
rnumpx = /^-?\d+(?:px)?$/i,
rnum = /^-?\d/,
@@ -5944,6 +5981,28 @@ if ( !jQuery.support.opacity ) {
};
}
+jQuery(function() {
+ // This hook cannot be added until DOM ready because the support test
+ // for it is not run until after DOM ready
+ if ( !jQuery.support.reliableMarginRight ) {
+ jQuery.cssHooks.marginRight = {
+ get: function( elem, computed ) {
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ // Work around by temporarily setting element display to inline-block
+ var ret;
+ jQuery.swap( elem, { "display": "inline-block" }, function() {
+ if ( computed ) {
+ ret = curCSS( elem, "margin-right", "marginRight" );
+ } else {
+ ret = elem.style.marginRight;
+ }
+ });
+ return ret;
+ }
+ };
+ }
+});
+
if ( document.defaultView && document.defaultView.getComputedStyle ) {
getComputedStyle = function( elem, newName, name ) {
var ret, defaultView, computedStyle;
@@ -6048,7 +6107,7 @@ var r20 = /%20/g,
rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
// #7653, #8125, #8152: local protocol detection
- rlocalProtocol = /(?:^file|^widget|\-extension):$/,
+ rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|widget):$/,
rnoContent = /^(?:GET|HEAD)$/,
rprotocol = /^\/\//,
rquery = /\?/,
@@ -6060,7 +6119,7 @@ var r20 = /%20/g,
rucHeadersFunc = function( _, $1, $2 ) {
return $1 + $2.toUpperCase();
},
- rurl = /^([\w\+\.\-]+:)\/\/([^\/?#:]*)(?::(\d+))?/,
+ rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,
// Keep a copy of the old load method
_load = jQuery.fn.load,
@@ -6102,7 +6161,7 @@ try {
}
// Segment location into parts
-ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() );
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
function addToPrefiltersOrTransports( structure ) {
@@ -6360,7 +6419,6 @@ jQuery.extend({
cache: null,
traditional: false,
headers: {},
- crossDomain: null,
*/
accepts: {
@@ -6645,7 +6703,7 @@ jQuery.extend({
s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax );
// Determine if a cross-domain request is in order
- if ( !s.crossDomain ) {
+ if ( s.crossDomain == null ) {
parts = rurl.exec( s.url.toLowerCase() );
s.crossDomain = !!( parts &&
( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] ||
@@ -7024,7 +7082,7 @@ function ajaxConvert( s, response ) {
var jsc = jQuery.now(),
- jsre = /(\=)\?(&|$)|()\?\?()/i;
+ jsre = /(\=)\?(&|$)|\?\?/i;
// Default jsonp settings
jQuery.ajaxSetup({
@@ -7285,11 +7343,12 @@ if ( jQuery.support.ajax ) {
xhr.overrideMimeType( s.mimeType );
}
- // Requested-With header
- // Not set for crossDomain requests with no content
- // (see why at http://trac.dojotoolkit.org/ticket/9486)
- // Won't change header if already provided
- if ( !( s.crossDomain && !s.hasContent ) && !headers["X-Requested-With"] ) {
+ // X-Requested-With header
+ // For cross-domain requests, seeing as conditions for a preflight are
+ // akin to a jigsaw puzzle, we simply never set it to be sure.
+ // (it can always be set on a per-request basis or even using ajaxSetup)
+ // For same-domain requests, won't change header if already provided.
+ if ( !s.crossDomain && !headers["X-Requested-With"] ) {
headers[ "X-Requested-With" ] = "XMLHttpRequest";
}
@@ -7979,8 +8038,8 @@ if ( "getBoundingClientRect" in document.documentElement ) {
win = getWindow(doc),
clientTop = docElem.clientTop || body.clientTop || 0,
clientLeft = docElem.clientLeft || body.clientLeft || 0,
- scrollTop = (win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop ),
- scrollLeft = (win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft),
+ scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop,
+ scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft,
top = box.top + scrollTop - clientTop,
left = box.left + scrollLeft - clientLeft;
@@ -8093,7 +8152,6 @@ jQuery.offset = {
this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
body.removeChild( container );
- body = container = innerDiv = checkDiv = table = td = null;
jQuery.offset.initialize = jQuery.noop;
},
@@ -8123,10 +8181,10 @@ jQuery.offset = {
curOffset = curElem.offset(),
curCSSTop = jQuery.css( elem, "top" ),
curCSSLeft = jQuery.css( elem, "left" ),
- calculatePosition = (position === "absolute" && jQuery.inArray('auto', [curCSSTop, curCSSLeft]) > -1),
+ calculatePosition = (position === "absolute" || position === "fixed") && jQuery.inArray('auto', [curCSSTop, curCSSLeft]) > -1,
props = {}, curPosition = {}, curTop, curLeft;
- // need to be able to calculate position if either top or left is auto and position is absolute
+ // need to be able to calculate position if either top or left is auto and position is either absolute or fixed
if ( calculatePosition ) {
curPosition = curElem.position();
}
diff --git a/addons/base/static/lib/qweb/qweb-test.js.html b/addons/base/static/lib/qweb/qweb-test.js.html
index 76892845306..a7742cc5142 100644
--- a/addons/base/static/lib/qweb/qweb-test.js.html
+++ b/addons/base/static/lib/qweb/qweb-test.js.html
@@ -1,7 +1,7 @@
-
+
diff --git a/addons/base/static/lib/underscore/underscore.strings.js b/addons/base/static/lib/underscore/underscore.strings.js
new file mode 100644
index 00000000000..6e66014e3e9
--- /dev/null
+++ b/addons/base/static/lib/underscore/underscore.strings.js
@@ -0,0 +1,161 @@
+// Underscore.strings
+// (c) 2010 Esa-Matti Suuronen
+// Underscore.strings is freely distributable under the terms of the MIT license.
+// Documentation: http://bitbucket.org/epeli/underscore.strings/
+// Some code is borrowed from MooTools and Alexandru Marasteanu.
+
+// Version 1.0
+
+
+(function(){
+ // ------------------------- Baseline setup ---------------------------------
+
+ // Establish the root object, "window" in the browser, or "global" on the server.
+ var root = this;
+
+ var nativeTrim = String.prototype.trim;
+
+ function str_repeat(i, m) {
+ for (var o = []; m > 0; o[--m] = i);
+ return o.join('');
+ }
+
+ function defaultToWhiteSpace(characters){
+ if (characters) {
+ return _s.escapeRegExp(characters);
+ }
+ return '\\s';
+ }
+
+ var _s;
+ _s = root._s = {
+
+ capitalize : function(str) {
+ return str.charAt(0).toUpperCase() + str.substring(1).toLowerCase();
+ },
+
+ join: function(sep) {
+ // TODO: Could this be faster by converting
+ // arguments to Array and using array.join(sep)?
+ sep = String(sep);
+ var str = "";
+ for (var i=1; i < arguments.length; i += 1) {
+ str += String(arguments[i]);
+ if ( i !== arguments.length-1 ) {
+ str += sep;
+ }
+ }
+ return str;
+ },
+
+ escapeRegExp: function(str){
+ // From MooTools core 1.2.4
+ return str.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ },
+
+ reverse: function(str){
+ return Array.prototype.reverse.apply(str.split('')).join('');
+ },
+
+ contains: function(str, needle){
+ return str.indexOf(needle) !== -1;
+ },
+
+ clean: function(str){
+ return _s.strip(str.replace(/\s+/g, ' '));
+ },
+
+ trim: function(str, characters){
+ if (!characters && nativeTrim) {
+ return nativeTrim.call(str);
+ }
+ characters = defaultToWhiteSpace(characters);
+ return str.replace(new RegExp('\^[' + characters + ']+|[' + characters + ']+$', 'g'), '');
+ },
+
+
+
+ ltrim: function(str, characters){
+ characters = defaultToWhiteSpace(characters);
+ return str.replace(new RegExp('\^[' + characters + ']+', 'g'), '');
+ },
+
+ rtrim: function(str, characters){
+ characters = defaultToWhiteSpace(characters);
+ return str.replace(new RegExp('[' + characters + ']+$', 'g'), '');
+ },
+
+
+ startsWith: function(str, starts){
+ return str.length >= starts.length && str.substring(0, starts.length) === starts;
+ },
+
+
+ endsWith: function(str, ends){
+ return str.length >= ends.length && str.substring(str.length - ends.length) === ends;
+ },
+
+
+ /**
+ * Credits for this function goes to
+ * http://www.diveintojavascript.com/projects/sprintf-for-javascript
+ *
+ * Copyright (c) Alexandru Marasteanu
+ * All rights reserved.
+ * */
+ sprintf: function(){
+
+ var i = 0, a, f = arguments[i++], o = [], m, p, c, x, s = '';
+ while (f) {
+ if (m = /^[^\x25]+/.exec(f)) {
+ o.push(m[0]);
+ }
+ else if (m = /^\x25{2}/.exec(f)) {
+ o.push('%');
+ }
+ else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) {
+ if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) {
+ throw('Too few arguments.');
+ }
+ if (/[^s]/.test(m[7]) && (typeof(a) != 'number')) {
+ throw('Expecting number but found ' + typeof(a));
+ }
+ switch (m[7]) {
+ case 'b': a = a.toString(2); break;
+ case 'c': a = String.fromCharCode(a); break;
+ case 'd': a = parseInt(a); break;
+ case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
+ case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
+ case 'o': a = a.toString(8); break;
+ case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
+ case 'u': a = Math.abs(a); break;
+ case 'x': a = a.toString(16); break;
+ case 'X': a = a.toString(16).toUpperCase(); break;
+ }
+ a = (/[def]/.test(m[7]) && m[2] && a >= 0 ? '+'+ a : a);
+ c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' ';
+ x = m[5] - String(a).length - s.length;
+ p = m[5] ? str_repeat(c, x) : '';
+ o.push(s + (m[4] ? a + p : p + a));
+ }
+ else {
+ throw('Huh ?!');
+ }
+ f = f.substring(m[0].length);
+ }
+ return o.join('');
+ }
+ }
+
+ // Some aliases
+ root._s.strip = _s.trim;
+ root._s.lstrip = _s.ltrim;
+ root._s.rstrip = _s.rtrim;
+
+
+ // Integrate with Underscore.js
+ if (root._) {
+ root._.mixin(root._s);
+ }
+
+}());
diff --git a/addons/base/static/src/base.html b/addons/base/static/src/base.html
index 4b9eb5409ed..8bb4793d42c 100644
--- a/addons/base/static/src/base.html
+++ b/addons/base/static/src/base.html
@@ -7,8 +7,9 @@
+
-
+
diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css
index 075150f6ca6..61b1f68879b 100644
--- a/addons/base/static/src/css/base.css
+++ b/addons/base/static/src/css/base.css
@@ -54,6 +54,9 @@ body.openerp {
z-index: 1001;
display: none;
}
+.openerp .oe_notification * {
+ color: white;
+}
/* Login */
.openerp .login {
@@ -440,6 +443,16 @@ body.openerp {
border-radius: 3px;
background: white;
}
+.openerp input[type="text"], .openerp input[type="password"], .openerp select, .openerp .button {
+ height: 22px;
+}
+.openerp .button {
+ white-space: nowrap;
+}
+.openerp .button span {
+ position: relative;
+ top: -3px;
+}
.openerp input.field_date, .openerp input.field_datetime {
background: #fff url('../img/ui/field_calendar.png') no-repeat right center;
background-origin: content-box;
diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js
index 439d34e4ad2..db2db6b91e5 100644
--- a/addons/base/static/src/js/chrome.js
+++ b/addons/base/static/src/js/chrome.js
@@ -217,7 +217,7 @@ openerp.base.BasicController = Class.extend( /** @lends openerp.base.BasicContro
}
}
- },
+ }
});
openerp.base.Session = openerp.base.BasicController.extend( /** @lends openerp.base.Session# */{
@@ -468,9 +468,24 @@ openerp.base.Controller = openerp.base.BasicController.extend( /** @lends opener
});
openerp.base.CrashManager = openerp.base.Controller.extend({
-// Controller to display exception and stacktrace and eventually report error trought maitenance contract
-// if should hook on Session.on_rpc_error: function(error) {
-// and display OPW etc...
+ init: function(session, element_id) {
+ this._super(session, element_id);
+ this.session.on_rpc_error.add(this.on_rpc_error);
+ },
+ on_rpc_error: function(error) {
+ var msg = error.message + "\n" + error.data.debug;
+ this.display_error(msg);
+ },
+ display_error: function(message) {
+ $('').text(message).dialog({
+ modal: true,
+ buttons: {
+ OK: function() {
+ $(this).dialog("close");
+ }
+ }
+ });
+ }
});
openerp.base.Database = openerp.base.Controller.extend({
@@ -503,8 +518,14 @@ openerp.base.Notification = openerp.base.Controller.extend({
speed: 500
});
},
- add: function(title, text) {
- this.$element.notify("create", {
+ 'default': function(title, text) {
+ this.$element.notify('create', {
+ title: title,
+ text: text
+ });
+ },
+ alert: function(title, text) {
+ this.$element.notify('create', 'oe_notification_alert', {
title: title,
text: text
});
@@ -671,6 +692,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({
this.session = new openerp.base.Session("oe_errors");
this.loading = new openerp.base.Loading(this.session, "oe_loading");
+ this.crashmanager = new openerp.base.CrashManager(this.session);
// Do you autorize this ?
openerp.base.Controller.prototype.notification = new openerp.base.Notification(this.session, "oe_notification");
@@ -690,7 +712,7 @@ openerp.base.WebClient = openerp.base.Controller.extend({
this.header.start();
this.login.start();
this.menu.start();
- this.notification.add("OpenERP Client", "The openerp client has been initialized.");
+ this.notification['default']("OpenERP Client", "The openerp client has been initialized.");
},
on_logged: function() {
this.action = new openerp.base.ActionManager(this.session, "oe_app");
diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js
index b3923dc6c76..4dbfb47dd28 100644
--- a/addons/base/static/src/js/data.js
+++ b/addons/base/static/src/js/data.js
@@ -29,35 +29,119 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
init: function(session, model) {
this._super(session);
this.model = model;
-
- this.ids = [];
- this.offset
+ this.context = {};
this.index = 0;
this.count = 0;
-
- this.sort = [];
- this.domain = [];
- this.context = {};
},
start: function() {
},
-
+ previous: function () {
+ this.index -= 1;
+ if (this.index < 0) {
+ this.index = this.count - 1;
+ }
+ return this;
+ },
+ next: function () {
+ this.index += 1;
+ if (this.index >= this.count) {
+ this.index = 0;
+ }
+ return this;
+ },
/**
- * Fetch all the records selected by this DataSet, based on its domain
- * and context.
- *
- * Fires the on_ids event.
- *
- * TODO: return deferred
+ * Read records.
+ */
+ read_ids: function (ids, fields, callback) {
+ var self = this;
+ this.rpc('/base/dataset/get', {
+ model: this.model,
+ ids: ids,
+ fields: fields
+ }, callback);
+ },
+ /**
+ * Read a slice of the records represented by this DataSet, based on its
+ * domain and context.
*
* @param {Number} [offset=0] The index from which selected records should be returned
* @param {Number} [limit=null] The maximum number of records to return
- * @returns itself
*/
- fetch: function (fields, offset, limit, callback) {
+ read_slice: function (fields, offset, limit, callback) {
+ },
+ /**
+ * Read the indexed record.
+ */
+ read_index: function (fields, callback) {
+ if (_.isEmpty(this.ids)) {
+ callback([]);
+ } else {
+ fields = fields || false;
+ this.read_ids([this.ids[this.index]], fields, function(records) {
+ callback(records[0]);
+ });
+ }
+ },
+ default_get: function() {
+ },
+ create: function() {
+ },
+ write: function (id, data, callback) {
+ this.rpc('/base/dataset/save', {
+ model: this.model,
+ id: id,
+ data: data,
+ context: this.context
+ }, callback);
+ },
+ unlink: function() {
+ },
+ call: function (method, ids, args, callback) {
+ ids = ids || [];
+ args = args || [];
+ this.rpc('/base/dataset/call', {
+ model: this.model,
+ method: method,
+ ids: ids,
+ args: args
+ }, callback);
+ },
+});
+
+openerp.base.DataSetStatic = openerp.base.DataSet.extend({
+ init: function(session, model) {
+ this._super(session, model);
+ // all local records
+ this.ids = [];
+ this.count = 0;
+ },
+ read_slice: function (fields, offset, limit, callback) {
+ this.read_ids(this.ids.slice(offset, offset + limit));
+ },
+});
+
+openerp.base.DataSetSearch = openerp.base.DataSet.extend({
+ init: function(session, model) {
+ this._super(session, model);
+ this.domain = [];
+ this.sort = [];
+ this.offset = 0;
+ // subset records[offset:offset+limit]
+ // is it necessary ?
+ this.ids = [];
+ },
+ read_slice: function (fields, offset, limit, callback) {
var self = this;
offset = offset || 0;
- this.rpc('/base/dataset/find', {
+ // cached search, not sure it's a good idea
+ if(this.offset <= offset) {
+ var start = offset - this.offset;
+ if(this.ids.length - start >= limit) {
+ // TODO: check if this could work do only read if possible
+ // return read_ids(ids.slice(start,start+limit),fields,callback)
+ }
+ }
+ this.rpc('/base/dataset/search_read', {
model: this.model,
fields: fields,
domain: this.domain,
@@ -74,55 +158,6 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
callback(records);
});
},
- fetch_ids: function (ids, fields, callback) {
- var self = this;
- this.rpc('/base/dataset/get', {
- model: this.model,
- ids: ids,
- fields: fields
- }, callback);
- },
- fetch_index: function (fields, callback) {
- if (_.isEmpty(this.ids)) {
- callback([]);
- } else {
- fields = fields || false;
- this.fetch_ids([this.ids[this.index]], fields, function(records) {
- callback(records[0]);
- });
- }
- },
-
- write: function (id, data, callback) {
- this.rpc('/base/datarecord/save', {
- model: this.model,
- id: id,
- data: data
- }, callback);
- },
- /**
- * Activates the previous id in the active sequence. If there is no previous id, wraps around to the last one
- * @returns itself
- */
- previous: function () {
- this.index -= 1;
- if (this.index < 0) {
- this.index = this.ids.length - 1;
- }
- return this;
- },
-
- /**
- * Activates the next id in the active sequence. If there is no next id, wraps around to the first one
- * @returns itself
- */
- next: function () {
- this.index += 1;
- if (this.index >= this.ids.length) {
- this.index = 0;
- }
- return this;
- }
});
};
diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js
index 12609ed9c53..528dc47eaf1 100644
--- a/addons/base/static/src/js/form.js
+++ b/addons/base/static/src/js/form.js
@@ -57,6 +57,15 @@ openerp.base.FormView = openerp.base.Controller.extend( /** @lends openerp.base
this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar);
}
},
+ do_show: function () {
+ // this should not append as start return a deferred resolved when fields_view is known
+ if(this.fields_view && this.fields_view.fields)
+ this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
+ this.$element.show();
+ },
+ do_hide: function () {
+ this.$element.hide();
+ },
on_record_loaded: function(record) {
if (record) {
this.datarecord = record;
@@ -76,14 +85,88 @@ openerp.base.FormView = openerp.base.Controller.extend( /** @lends openerp.base
w.process_attrs();
w.update_dom();
}
- if (widget) {
- // TODO: check if and trigger
- // if (onchange for this field) {
- // this.ready = false;
- // rpc - process.callback ( this.ready = true; )
- // }
+ if (widget && widget.node.attrs.on_change) {
+ this.do_onchange(widget);
}
},
+ on_pager_action: function(action) {
+ switch (action) {
+ case 'first':
+ this.dataset.index = 0;
+ break;
+ case 'previous':
+ this.dataset.previous();
+ break;
+ case 'next':
+ this.dataset.next();
+ break;
+ case 'last':
+ this.dataset.index = this.dataset.ids.length - 1;
+ break;
+ }
+ this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
+ },
+ do_update_pager: function() {
+ var $pager = this.$element.find('div.oe_form_pager');
+ $pager.find("button[data-pager-action='first'], button[data-pager-action='previous']").attr('disabled', this.dataset.index == 0);
+ $pager.find("button[data-pager-action='next'], button[data-pager-action='last']").attr('disabled', this.dataset.index == this.dataset.ids.length - 1);
+ this.$element.find('span.oe_pager_index').html(this.dataset.index + 1);
+ this.$element.find('span.oe_pager_count').html(this.dataset.count);
+ },
+ do_onchange: function(widget) {
+ var self = this;
+ this.ready = false;
+ var onchange = _.trim(widget.node.attrs.on_change);
+ var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
+ if (call) {
+ var method = call[1], args = [];
+ _.each(call[2].split(','), function(a) {
+ var field = _.trim(a);
+ if (self.fields[field]) {
+ args.push(self.fields[field].value);
+ } else {
+ args.push(false);
+ this.log("warning : on_change can't find field " + field, onchange);
+ }
+ });
+ this.dataset.call(method, [this.datarecord.id], args, this.on_processed_onchange);
+ } else {
+ this.log("Wrong on_change format", on_change);
+ }
+ },
+ on_processed_onchange: function(response) {
+ console.log("onchange result :", response);
+ var result = response.result;
+ if (result.value) {
+ for (var f in result.value) {
+ var field = this.fields[f];
+ if (field) {
+ var value = result.value[f];
+ if (field.value != value) {
+ field.set_value(value);
+ // TODO: Recursive on_change
+ // this.on_form_changed(field);
+ }
+ } else {
+ this.log("warning : on_processed_onchange can't find field " + field, result);
+ }
+ }
+ }
+ if (result.warning) {
+ $(QWeb.render("DialogWarning", result.warning)).dialog({
+ modal: true,
+ buttons: {
+ Ok: function() {
+ $(this).dialog("close");
+ }
+ }
+ });
+ }
+ if (result.domain) {
+ // Will be removed ?
+ }
+ this.ready = true;
+ },
do_save: function() {
if (!this.ready) {
return false;
@@ -110,50 +193,26 @@ openerp.base.FormView = openerp.base.Controller.extend( /** @lends openerp.base
this.switch_readonly();
}
},
- do_show: function () {
- this.dataset.fetch_index(this.fields_view.fields, this.on_record_loaded);
- this.$element.show();
- },
- do_hide: function () {
- this.$element.hide();
- },
- do_update_pager: function() {
- var $pager = this.$element.find('div.oe_form_pager');
- $pager.find("button[data-pager-action='first'], button[data-pager-action='previous']").attr('disabled', this.dataset.index == 0);
- $pager.find("button[data-pager-action='next'], button[data-pager-action='last']").attr('disabled', this.dataset.index == this.dataset.ids.length - 1);
- this.$element.find('span.oe_pager_index').html(this.dataset.index + 1);
- this.$element.find('span.oe_pager_count').html(this.dataset.count);
- },
- on_pager_action: function(action) {
- switch (action) {
- case 'first':
- this.dataset.index = 0;
- break;
- case 'previous':
- this.dataset.previous();
- break;
- case 'next':
- this.dataset.next();
- break;
- case 'last':
- this.dataset.index = this.dataset.ids.length - 1;
- break;
- }
- this.dataset.fetch_index(this.fields_view.fields, this.on_record_loaded);
- },
switch_readonly: function() {
},
switch_editable: function() {
},
on_invalid: function() {
- console.info("Form invalid");
+ var msg = "";
+ _.each(this.fields, function(f) {
+ if (f.invalid) {
+ msg += "- " + f.string + "
";
+ }
+ });
+ msg += "
";
+ this.notification.alert("The following fields are invalid :", msg);
},
on_saved: function(r) {
if (!r.result) {
this.log("Record was not saved");
} else {
// Check response for exceptions, display error
- this.notification.add("Record saved", "The record #" + this.datarecord.id + " has been saved.");
+ this.notification['default']("Record saved", "The record #" + this.datarecord.id + " has been saved.");
}
},
do_search: function (domains, contexts, groupbys) {
@@ -403,6 +462,7 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({
},
set_value: function(value) {
this.value = value;
+ this.invalid = false;
},
get_value: function() {
return this.value;
@@ -414,7 +474,6 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({
this.$element.toggleClass('invalid', this.invalid);
},
on_ui_change: function() {
- this.view.on_form_changed(this);
this.touched = true;
}
});
@@ -438,9 +497,10 @@ openerp.base.form.FieldChar = openerp.base.form.Field.extend({
this.$element.find('input').attr('disabled', this.readonly);
},
on_ui_change: function() {
+ this._super.apply(this, arguments);
this.value = this.$element.find('input').val();
this.invalid = this.required && this.value == "";
- this._super.apply(this, arguments);
+ this.view.on_form_changed(this);
}
});
@@ -457,9 +517,10 @@ openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({
this.$element.find('input').val(value);
},
on_ui_change: function() {
- this.value = parseFloat(this.$element.find('input').val()); // TODO: do it well
- this.invalid = this.required && this.value == "";
this._super.apply(this, arguments);
+ this.value = this.$element.find('input').val().replace(/,/g, '.');
+ this.invalid = (this.required && this.value == "") || !this.value.match(/^\d+(\.\d+)?$/) ;
+ this.view.on_form_changed(this);
}
});
@@ -480,9 +541,10 @@ openerp.base.form.FieldDate = openerp.base.form.FieldChar.extend({
this.$element.find('input').val(show_value);
},
on_ui_change: function() {
+ this._super.apply(this, arguments);
this.value = this.$element.find('input').val();
this.invalid = this.required && this.value == "";
- this._super.apply(this, arguments);
+ this.view.on_form_changed(this);
}
});
@@ -504,9 +566,10 @@ openerp.base.form.FieldDatetime = openerp.base.form.FieldChar.extend({
this.$element.find('input').val(show_value);
},
on_ui_change: function() {
+ this._super.apply(this, arguments);
this.value = this.$element.find('input').val();
this.invalid = this.required && this.value == "";
- this._super.apply(this, arguments);
+ this.view.on_form_changed(this);
}
});
@@ -529,9 +592,10 @@ openerp.base.form.FieldText = openerp.base.form.Field.extend({
this.$element.find('textarea').attr('disabled', this.readonly);
},
on_ui_change: function() {
+ this._super.apply(this, arguments);
this.value = this.$element.find('textarea').val();
this.invalid = this.required && this.value == "";
- this._super.apply(this, arguments);
+ this.view.on_form_changed(this);
}
});
@@ -544,7 +608,7 @@ openerp.base.form.FieldBoolean = openerp.base.form.Field.extend({
var self = this;
this._super.apply(this, arguments);
this.$element.find('input').click(function() {
- if ($(this)[0].checked != this.value) {
+ if ($(this).is(':checked') != self.value) {
self.on_ui_change();
}
});
@@ -558,9 +622,10 @@ openerp.base.form.FieldBoolean = openerp.base.form.Field.extend({
this.$element.find('textarea').attr('disabled', this.readonly);
},
on_ui_change: function() {
+ this._super.apply(this, arguments);
this.value = this.$element.find('input').is(':checked');
this.invalid = this.required && !this.value;
- this._super.apply(this, arguments);
+ this.view.on_form_changed(this);
}
});
@@ -590,9 +655,10 @@ openerp.base.form.FieldSelection = openerp.base.form.Field.extend({
this.$element.find('select').attr('disabled', this.readonly);
},
on_ui_change: function() {
+ this._super.apply(this, arguments);
this.value = this.$element.find('select').val();
this.invalid = this.required && this.value == "";
- this._super.apply(this, arguments);
+ this.view.on_form_changed(this);
}
});
@@ -612,30 +678,44 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({
}
});
+openerp.base.form.FieldOne2ManyDatasSet = openerp.base.DataSetStatic.extend({
+ start: function() {
+ },
+ write: function (id, data, callback) {
+ this._super(id, data, callback);
+ },
+ write: function (id, data, callback) {
+ this._super(id, data, callback);
+ },
+ unlink: function() {
+ }
+});
+
+openerp.base.form.FieldOne2ManyViewManager = openerp.base.ViewManager.extend({
+ init: function(session, element_id, dataset, views) {
+ this._super(session, element_id, dataset, views);
+ },
+});
+
openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
init: function(view, node) {
this._super(view, node);
this.template = "FieldOne2Many";
- this.viewmanager = null;
this.operations = [];
-
},
start: function() {
this._super.apply(this, arguments);
this.log("o2m.start");
- this.viewmanager = new openerp.base.ViewManager(this.view.session, this.element_id);
- this.viewmanager.searchable = false;
- var action = {
- res_model: this.field.relation,
- views: [ [false,"list"], ],
- };
- this.viewmanager.do_action_window(action);
-
+ var views = [ [false,"list"], [false,"form"] ];
+ this.dataset = new openerp.base.form.FieldOne2ManyDatasSet(this.session, this.field.relation);
+ this.viewmanager = new openerp.base.form.FieldOne2ManyViewManager(this.view.session, this.element_id, this.dataset, views);
+ this.viewmanager.start();
},
set_value: function(value) {
this.value = value;
this.log("o2m.set_value",value);
this.viewmanager.dataset.ids = value;
+ this.viewmanager.dataset.ids.count = value.length;
this.viewmanager.views.list.controller.do_update();
},
get_value: function(value) {
@@ -647,7 +727,6 @@ openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
this.$element.toggleClass('required', this.required);
},
on_ui_change: function() {
- this.view.on_form_changed(this);
}
});
diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js
index 7f88b13e979..a63c776db58 100644
--- a/addons/base/static/src/js/list.js
+++ b/addons/base/static/src/js/list.js
@@ -95,12 +95,12 @@ openerp.base.ListView = openerp.base.Controller.extend({
// TODO: handle non-empty results.group_by with read_group
self.dataset.context = results.context;
self.dataset.domain = results.domain;
- self.dataset.fetch(self.dataset.fields, 0, self.limit, self.do_fill_table);
+ self.dataset.read_slice(self.dataset.fields, 0, self.limit, self.do_fill_table);
});
},
do_update: function () {
var self = this;
- self.dataset.fetch(self.dataset.fields, 0, self.limit, self.do_fill_table);
+ self.dataset.read_ids(self.dataset.ids, self.dataset.fields, self.do_fill_table);
}
});
diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js
index d656bc0550c..39646472437 100644
--- a/addons/base/static/src/js/views.js
+++ b/addons/base/static/src/js/views.js
@@ -20,8 +20,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
if (this.viewmanager) {
this.viewmanager.stop();
}
- this.viewmanager = new openerp.base.ViewManager(this.session,this.element_id, true);
- this.viewmanager.do_action_window(action);
+ this.viewmanager = new openerp.base.ViewManagerAction(this.session,this.element_id, action, true);
this.viewmanager.start();
}
}
@@ -33,63 +32,32 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
openerp.base.views = new openerp.base.Registry();
openerp.base.ViewManager = openerp.base.Controller.extend({
- init: function(session, element_id, has_sidebar) {
+ init: function(session, element_id, dataset, views) {
this._super(session, element_id);
- this.action = null;
- this.dataset = null;
+ this.model = dataset.model;
+ this.dataset = dataset;
this.searchview = null;
this.active_view = null;
+ this.views_src = views;
this.views = {};
- if (has_sidebar)
- this.sidebar = new openerp.base.Sidebar(null, this);
- else
- this.sidebar = null;
- },
- start: function() {
- if (this.sidebar) {
- this.$element.find('.view-manager-main-sidebar').html(this.sidebar.render());
- this.sidebar.start();
- }
- },
- stop: function() {
- // should be replaced by automatic destruction implemented in BaseWidget
- if (this.sidebar) {
- this.sidebar.stop();
- }
- this._super();
- },
- do_action_window: function(action) {
- var self = this;
- this.action = action;
-
- // switch to the first one in sequence
- var inital_view_loaded = this.setup_initial_view(action.res_model,action.views);
-
- var searchview_loaded = this.setup_search_view(action);
-
- if (action['auto_search']) {
- $.when(searchview_loaded, inital_view_loaded)
- .then(this.searchview.do_search);
- }
},
/**
* @returns {jQuery.Deferred} initial view loading promise
*/
- setup_initial_view: function(model,views) {
+ start: function() {
var self = this;
- this.dataset = new openerp.base.DataSet(this.session, model);
this.dataset.start();
-
- this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: views}));
-
+ this.$element.html(QWeb.render("ViewManager", {"prefix": this.element_id, views: this.views_src}));
this.$element.find('.oe_vm_switch button').click(function() {
self.on_mode_switch($(this).data('view-type'));
});
- _.each(views, function(view) {
+ _.each(this.views_src, function(view) {
self.views[view[1]] = { view_id: view[0], controller: null };
});
-
- return this.on_mode_switch(views[0][1]);
+ // switch to the first one in sequence
+ return this.on_mode_switch(this.views_src[0][1]);
+ },
+ stop: function() {
},
/**
* Asks the view manager to switch visualization mode.
@@ -103,8 +71,8 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
var view = this.views[view_type];
if (!view.controller) {
// Lazy loading of views
- var controller = new (openerp.base.views.get_object(view_type))(
- this, this.session, this.element_id + "_view_" + view_type, this.dataset, view.view_id);
+ var controllerclass = openerp.base.views.get_object(view_type);
+ var controller = new controllerclass( this, this.session, this.element_id + "_view_" + view_type, this.dataset, view.view_id);
view_promise = controller.start();
this.views[view_type].controller = controller;
}
@@ -133,37 +101,18 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
}
return view_promise;
},
- /**
- * Extract search view defaults from the current action's context.
- *
- * These defaults are of the form {search_default_*: value}
- *
- * @returns {Object} a clean defaults mapping of {field_name: value}
- */
- search_defaults: function () {
- var defaults = {};
- _.each(this.action.context, function (value, key) {
- var match = /^search_default_(.*)$/.exec(key);
- if (match) {
- defaults[match[1]] = value;
- }
- });
- return defaults;
- },
/**
* Sets up the current viewmanager's search view.
*
- * @param action the action being executed
+ * @param view_id the view to use or false for a default one
* @returns {jQuery.Deferred} search view startup deferred
*/
- setup_search_view:function (action) {
+ setup_search_view: function(view_id, search_defaults) {
var self = this;
if (this.searchview) {
this.searchview.stop();
}
- var view_id = action.search_view_id ? action.search_view_id[0] || false : false;
-
- this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, this.search_defaults());
+ this.searchview = new openerp.base.SearchView(this, this.session, this.element_id + "_search", this.dataset, view_id, search_defaults);
this.searchview.on_search.add(function() {
self.views[self.active_view].controller.do_search.apply(self, arguments);
});
@@ -182,12 +131,49 @@ openerp.base.ViewManager = openerp.base.Controller.extend({
}
});
-openerp.base.ViewManagerRoot = openerp.base.ViewManager.extend({
-// Extends view manager
-});
+openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({
+ init: function(session, element_id, action, sidebar) {
+ var dataset = new openerp.base.DataSetSearch(session, action.res_model);
+ this._super(session, element_id, dataset, action.views);
+ this.action = action;
+ this.sidebar = sidebar;
+ if (sidebar)
+ this.sidebar = new openerp.base.Sidebar(null, this);
+ },
+ start: function() {
+ var self = this;
+ var inital_view_loaded = this._super();
-openerp.base.ViewManagerUsedAsAMany2One = openerp.base.ViewManager.extend({
-// Extends view manager
+ // init sidebar
+ if (this.sidebar) {
+ this.$element.find('.view-manager-main-sidebar').html(this.sidebar.render());
+ this.sidebar.start();
+ }
+
+ // init search view
+ var view_id = this.action.search_view_id ? this.action.search_view_id[0] || false : false;
+ var search_defaults = {};
+ _.each(this.action.context, function (value, key) {
+ var match = /^search_default_(.*)$/.exec(key);
+ if (match) {
+ search_defaults[match[1]] = value;
+ }
+ });
+ var searchview_loaded = this.setup_search_view(view_id,search_defaults);
+
+ // schedule auto_search
+ if (this.action['auto_search']) {
+ $.when(searchview_loaded, inital_view_loaded)
+ .then(this.searchview.do_search);
+ }
+ },
+ stop: function() {
+ // should be replaced by automatic destruction implemented in BaseWidget
+ if (this.sidebar) {
+ this.sidebar.stop();
+ }
+ this._super();
+ },
});
openerp.base.BaseWidget = openerp.base.Controller.extend({
@@ -326,15 +312,14 @@ openerp.base.handle_action = function(session, action) {
dialog.dialog({
title: action.name
});
- var viewmanager = new openerp.base.ViewManager(session,element_id, false);
- viewmanager.do_action_window(action);
+ debugger;
+ var viewmanager = new openerp.base.ViewManagerAction(session,element_id, action, false);
viewmanager.start();
}
}
};
openerp.base.views.add('calendar', 'openerp.base.CalendarView');
-
openerp.base.CalendarView = openerp.base.Controller.extend({
start: function () {
this._super();
diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml
index fbf3f728ef3..23685544094 100644
--- a/addons/base/static/src/xml/base.xml
+++ b/addons/base/static/src/xml/base.xml
@@ -10,6 +10,12 @@
#{title}
#{text}
+
+
x
+
+
#{title}
+
#{text}
+
@@ -191,7 +197,7 @@
-
+
|
|
@@ -226,7 +233,7 @@
-
+
+
+
diff --git a/addons/base/static/test/dataset.js b/addons/base/static/test/dataset.js
deleted file mode 100644
index ac7a899723a..00000000000
--- a/addons/base/static/test/dataset.js
+++ /dev/null
@@ -1,345 +0,0 @@
-/**
- * @function
- * Defines a module scope (which lasts until the next call to module).
- *
- * This module scopes implies setup and teardown callbacks running for each test.
- *
- * @param {String} name the name of the module
- * @param {Object} [lifecycle] callbacks to run before and after each test of the module
- * @param {Function} lifecycle.setup function running before each test of this module
- * @param {Function} lifecycle.teardown function running after each test of this module
- */
-var module;
-/**
- * @function
- * Defines a given test to run. Runs all the assertions present in the test
- *
- * @param {String} name the name of the test
- * @param {Number} [expected] number of assertions expected to run in this test (useful for asynchronous tests)
- * @param {Function} test the testing code to run, holding a sequence of assertions (at least one)
- */
-var test;
-/**
- * @function
- * Defines an asynchronous test: equivalent to calling stop() at the start of
- * a normal test().
- *
- * The test code needs to restart the test runner via start()
- *
- * @param {String} name the name of the test
- * @param {Number} [expected] number of assertions expected to run in this test (useful for asynchronous tests)
- * @param {Function} test the testing code to run, holding a sequence of assertions (at least one)
- */
-var asyncTest;
-/**
- * @function
- * The most basic boolean assertion (~assertTrue or assert).
- *
- * Passes if its argument is truthy
- *
- * @param {Boolean} state an arbitrary expression, evaluated in a boolean context
- * @param {String} [message] the message to output with the assertion result
- */
-var ok;
-/**
- * @function
- * Equality assertion (~assertEqual)
- *
- * Passes if both arguments are equal (via ==)
- *
- * @param {Object} actual the object to check for correctness (processing result)
- * @param {Object} expected the object to check against
- * @param {String} [message] message output with the assertion result
- */
-var equal;
-/**
- * @function
- * Inequality assertion (~assertNotEqual)
- *
- * Passes if the arguments are different (via !=)
- *
- * @param {Object} actual the object to check for correctness (processing result)
- * @param {Object} expected the object to check against
- * @param {String} [message] message output with the assertion result
- */
-var notEqual;
-/**
- * @function
- * Recursive equality assertion.
- *
- * Works on primitive types using === and traversing through
- * Objects and Arrays as well checking their components
- *
- * @param {Object} actual the object to check for correctness (processing result)
- * @param {Object} expected the object to check against
- * @param {String} [message] message output with the assertion result
- */
-var deepEqual;
-/**
- * @function
- * Recursive inequality assertion.
- *
- * Works on primitive types using !== and traversing through
- * Objects and Arrays as well checking their components
- *
- * @param {Object} actual the object to check for correctness (processing result)
- * @param {Object} expected the object to check against
- * @param {String} [message] message output with the assertion result
- */
-var notDeepEqual;
-/**
- * @function
- * Strict equality assertion (~assertEqual)
- *
- * Passes if both arguments are identical (via ===)
- *
- * @param {Object} actual the object to check for correctness (processing result)
- * @param {Object} expected the object to check against
- * @param {String} [message] message output with the assertion result
- */
-var strictEqual;
-/**
- * @function
- * Strict inequality assertion (~assertNotEqual)
- *
- * Passes if both arguments are identical (via !==)
- *
- * @param {Object} actual the object to check for correctness (processing result)
- * @param {Object} expected the object to check against
- * @param {String} [message] message output with the assertion result
- */
-var notStrictEqual;
-/**
- * @function
- * Passes if the provided block raised an exception.
- *
- * The expect argument can be provided to perform further assertion checks on the exception itself:
- * * If it's a RegExp test the exception against the regexp (message?)
- * * If it's a constructor, check if the exception is an instance of it
- * * If it's an other type of function, call it with the exception as first parameter
- * - If the function returns true, the assertion validates
- * - Otherwise it fails
- *
- * @param {Function} block function which should raise an exception when called
- * @param {Object} [expect] a RegExp, a constructor or a Function
- * @param {String} [message] message output with the assertion result
- */
-var raises;
-/**
- * @function
- * Starts running the test runner again from the point where it was
- * stopped.
- *
- * Used to resume testing after a callback.
- */
-var start;
-/**
- * @function
- * Stops the test runner in order to wait for an asynchronous test to run
- *
- * @param {Number} [timeout] fails the test after the timeout triggers, only for debugging tests
- */
-var stop;
-
-var Session = function () {
- return {
- rpc: function (_url, params, on_success) {
- setTimeout(on_success);
- }
- };
-};
-$(document).ready(function () {
- var openerp;
- module("ids_callback", {
- setup: function () {
- openerp = window.openerp.init(true);
- window.openerp.base.chrome(openerp);
- window.openerp.base.data(openerp);
- }
- });
- asyncTest("Baseline event attributes", 6, function () {
- var dataset = new openerp.base.DataSet(
- new Session());
- dataset.on_fetch.add(function (records, event) {
- deepEqual(records, [], 'No records returned');
- equal(event.offset, 0, 'No offset set in call');
- equal(event.limit, null, 'No limit set in call');
- deepEqual(event.domain, [], 'No domain on the dataset');
- deepEqual(event.context, {}, 'No context on the dataset');
- deepEqual(event.sort, [], 'The dataset is not sorted');
- start();
- });
- dataset.fetch();
- });
- asyncTest("Offset and limit", 2, function () {
- var dataset = new openerp.base.DataSet(
- new Session());
- dataset.on_fetch.add(function (records, event) {
- equal(event.offset, 20);
- equal(event.limit, 42);
- start();
- });
- dataset.fetch({
- name : { type : 'char' }
- }, 20, 42);
- });
- asyncTest("Domain and context propagation", 3, function () {
- var dataset = new openerp.base.DataSet(
- new Session());
- var domain_value = [['foo', '=', 'bar']];
- var context_value= {active_id:3, active_ids:42};
- var sort_value = ['foo'];
- dataset.on_fetch.add(function (records, event) {
- deepEqual(event.domain, domain_value);
- deepEqual(event.context, context_value);
- deepEqual(event.sort, sort_value);
- start();
- });
- dataset.set({
- domain: domain_value,
- context: context_value,
- sort: sort_value
- });
- dataset.fetch();
- });
-
- var dataset;
- module("set", {
- setup: function () {
- var openerp = window.openerp.init(true);
- window.openerp.base.chrome(openerp);
- window.openerp.base.data(openerp);
- dataset = new openerp.base.DataSet();
- }
- });
- test('Basic properties setting', function () {
- var domain_value = [['foo', '=', 'bar']];
- var result = dataset.set({
- domain: domain_value
- });
- ok(dataset === result);
- deepEqual(domain_value, dataset._domain);
- });
- test("Ensure changes don't stick", function () {
- var domain = [['foo', '=', 'bar']];
- dataset.set({
- domain: domain
- });
- domain.pop();
- deepEqual([['foo', '=', 'bar']], dataset._domain);
- });
-
- module('ids_activation', {
- setup: function () {
- var openerp = window.openerp.init(true);
- window.openerp.base.chrome(openerp);
- window.openerp.base.data(openerp);
- dataset = new openerp.base.DataSet();
- }
- });
- test('activate id', function () {
- dataset.activate(1);
- deepEqual(dataset.get_active_ids(), [1]);
- equal(dataset.get_active_id(), 1);
- });
- test('set active_ids', function () {
- dataset.select([1, 2, 3]);
- deepEqual(dataset.get_active_ids(), [1, 2, 3],
- "selecting an ids range");
- equal(dataset.get_active_id(), 1);
- });
- test('activate incorrect id', function () {
- dataset.select([1, 2, 3]);
- raises(function () { dataset.activate(42); },
- "Activating an id not present in the selection is an error");
- });
- test('reset active id on set active ids', function () {
- dataset.select([1, 2, 3]).activate(3).select([1, 2, 3]);
- equal(dataset.get_active_id(), 1,
- "selecting an ids range resets the active id");
- });
-
- module('active_id_iteration', {
- setup: function () {
- var openerp = window.openerp.init(true);
- window.openerp.base.chrome(openerp);
- window.openerp.base.data(openerp);
- dataset = new openerp.base.DataSet();
- dataset.select([1, 2, 3]);
- }
- });
- test('step forward', function () {
- dataset.activate(1);
- dataset.next();
- equal(dataset.get_active_id(), 2);
- });
- test('wraparound forward', function () {
- dataset.activate(3);
- dataset.next();
- equal(dataset.get_active_id(), 1);
- });
- test('step back', function () {
- dataset.activate(3);
- dataset.prev();
- equal(dataset.get_active_id(), 2);
- });
- test('wraparound back', function () {
- dataset.activate(1);
- dataset.prev();
- equal(dataset.get_active_id(), 3);
- });
-
- var ResponseAssertSession = function (response_ids) {
- return {
- rpc: function (url, params, on_success) {
- equal(url, '/base/dataset/get');
- deepEqual(params.ids, response_ids);
- _.delay(on_success, 0, _.map(
- params.ids, function (id) {
- return {id: id, sequence: id, name: 'foo'+id};
- }
- ));
- }
- };
- };
-
- module('active_ids', {
- setup: function () {
- openerp = window.openerp.init(true);
- window.openerp.base.chrome(openerp);
- window.openerp.base.data(openerp);
- }
- });
- asyncTest('Get pre-set active_ids', 6, function () {
- var dataset = new openerp.base.DataSet(
- new ResponseAssertSession([1, 2, 3]));
- dataset.select([1, 2, 3]);
- dataset.on_active_ids.add(function (data_records) {
- equal(data_records.length, 3);
- equal(data_records[0].values.id, 1);
- equal(data_records[1].values.id, 2);
- equal(data_records[2].values.id, 3);
- start();
- });
- dataset.active_ids();
- });
-
- module('active_id', {
- setup: function () {
- openerp = window.openerp.init(true);
- window.openerp.base.chrome(openerp);
- window.openerp.base.data(openerp);
- }
- });
- test('Get pre-set active_id', 3, function () {
- var dataset = new openerp.base.DataSet(
- new ResponseAssertSession([42]));
- stop(500);
- dataset.select([1, 2, 3, 42]).activate(42);
- dataset.on_active_id.add(function (data_record) {
- equal(data_record.values.id, 42);
- start();
- });
- dataset.active_id();
- });
-});
diff --git a/addons/base/static/test/test.html b/addons/base/static/test/test.html
index 8f448e8f8c6..9681be8e389 100644
--- a/addons/base/static/test/test.html
+++ b/addons/base/static/test/test.html
@@ -11,7 +11,7 @@
-
+
@@ -33,7 +33,6 @@
-
diff --git a/addons/web_chat/__init__.py b/addons/web_chat/__init__.py
new file mode 100644
index 00000000000..ee5959455ad
--- /dev/null
+++ b/addons/web_chat/__init__.py
@@ -0,0 +1 @@
+import controllers
diff --git a/addons/web_chat/__openerp__.py b/addons/web_chat/__openerp__.py
new file mode 100644
index 00000000000..132986ff90d
--- /dev/null
+++ b/addons/web_chat/__openerp__.py
@@ -0,0 +1,8 @@
+{
+ "name": "Web Chat",
+ "version": "2.0",
+ "depends": [],
+ "js": ["static/src/web_chat.js"],
+ "css": [],
+ 'active': True,
+}
diff --git a/addons/web_chat/controllers/__init__.py b/addons/web_chat/controllers/__init__.py
new file mode 100644
index 00000000000..8ee9bae18d9
--- /dev/null
+++ b/addons/web_chat/controllers/__init__.py
@@ -0,0 +1 @@
+import main
diff --git a/addons/web_chat/controllers/main.py b/addons/web_chat/controllers/main.py
new file mode 100644
index 00000000000..d8d33bac445
--- /dev/null
+++ b/addons/web_chat/controllers/main.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+import sys, time
+
+import simplejson
+
+import openerpweb
+
+#----------------------------------------------------------
+# OpenERP Web ajaxim Controllers
+#----------------------------------------------------------
+class PollServerMessageQueue(object):
+ def __init__(self):
+ # message queue
+ self.l = []
+ # online users
+ self.users = {}
+ # should contains: {
+ # 'user1234' : { s:1, m:"status message", timestamp: last_contact_timestamp }
+ # }
+ def userlist():
+ # return user list
+ pass
+
+ def write(self, m_type, m_sender, m_recipent, m_message, m_group):
+ # appends messages to l
+ # when status message uupdate users
+ pass
+ def read(self, recpient, timestamp):
+ # return matching message
+ pass
+ def gc():
+ # remove message older than 300s from self.l
+ # remove dead users from self.users
+ pass
+
+class PollServer(openerpweb.Controller):
+ _cp_path = "/web_chat/pollserver"
+
+ @openerpweb.httprequest
+ def login(self, req, **kw):
+ """
+ --> POST http://ajaxim.com/wp-content/plugins/im/ajaxim.php/login
+ Form Data
+ username:""
+ password:"d41d8cd98f00b204e9800998ecf8427e"
+ <-- 200 OK
+ Content-Type:text/html
+
+ {
+ "r":"logged in",
+ "u":"Guest130213866190.85",
+ "s":"f9e1811536f19ad5b9e00376f9ff1532",
+ "f":[
+ {"u":"Guest130205108745.47","s":{"s":1,"m":""},"g":"Users"},
+ {"u":"Guest130209838956.76","s":{"s":1,"m":""},"g":"Users"},
+ ]
+ }
+ """
+ mq = req.applicationsession.setdefault("web_chat",PollServerMessageQueue())
+ print "chat login",kw
+ # r = 'loggued in'
+ #u = generate random.randint(0,2**32)
+ #s = cherrypy cookie id
+ #f = mq.userlist()
+ return """
+ {
+ "r":"logged in",
+ "u":"Guest130213866190.85",
+ "s":"f9e1811536f19ad5b9e00376f9ff1532",
+ "f":[
+ {"u":"Guest130205108745.47","s":{"s":1,"m":""},"g":"Users"},
+ {"u":"Guest130209838956.76","s":{"s":1,"m":""},"g":"Users"},
+ ]
+ }
+ """
+
+ @openerpweb.httprequest
+ def logout(self, req):
+ """
+ --> GET http://im.ajaxim.com/logout
+ { "r":"logged out" }
+ """
+
+ @openerpweb.httprequest
+ def poll(self, req, **kw):
+ """
+ --> GET http://im.ajaxim.com/poll?callback=jsonp1302138663582&_1302138663582=
+ <-- 200 OK
+ Content-Type:text/html
+
+ noop:
+ jsonp1302138663582([]);
+
+ roster user online:
+ jsonp1302140366243([{"t":"s","s":"Guest130214038974.31","r":"all","m":"1:","g":"Users"}]);
+
+ roster user left:
+ jsonp1302140441577([{"t":"s","s":"Guest130214038974.31","r":"","m":"0:"}]);
+
+ receive message:
+ jsonp1302140191599([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"fuck"}]);
+
+ ('t' => $msg->type, 's' => $msg->from, 'r' => $msg->to, 'm' => $msg->message )
+ mag type s or m
+ echo ''
+
+
+ """
+ # for i in range(60):
+ #r = mq.read(username,timestamp)
+ # if messages
+ # return r
+ # if no message sleep 2s and retry
+ # sleep 2
+ # else
+ # return emptylist
+ print "chat poll",kw
+ time.sleep(2)
+ # it's http://localhost:8002/web_chat/pollserver/poll?method=long?callback=jsonp1302147330483&_1302147330483=
+ return '%s([{"t":"m","s":"Guest130214008855.5","r":"Guest130214013134.26","m":"fuck"}]);'%kw.get('callback','')
+ return None
+
+ @openerpweb.jsonrequest
+ def send(self, req, **kw):
+ """
+ --> GET http://im.ajaxim.com/send?callback=jsonp1302139980022&to=Guest130205108745.47&message=test&_1302139980022=
+ callback: jsonp1302139980022
+ to: Guest130205108745.47
+ message: test
+ _1302139980022:
+
+ <-- 200 OK
+ Content-Type:text/html
+
+ return array('r' => 'sent');
+ return array('r' => 'error', 'e' => 'no session found');
+ return array('r' => 'error', 'e' => 'no_recipient');
+ return array('r' => 'error', 'e' => 'send error');
+
+ """
+ print "chat send",kw
+ # mq.write()
+ return {"action": actions}
+
+ @openerpweb.jsonrequest
+ def status(self, req, **kw):
+ """
+ --> GET status call
+ const Offline = 0;
+ const Available = 1;
+ const Away = 2;
+ const Invisible = 3;
+
+ <-- 200 OK
+ Content-Type:text/html
+
+ return array('r' => 'status set');
+ return array('r' => 'error', 'e' => 'no session found');
+ return array('r' => 'error', 'e' => 'status error');
+ """
+ print "chat status",kw
+ # mq.write()
+ return {"action": actions}
+
+
diff --git a/addons/web_chat/controllers/noz_ajaxim.py b/addons/web_chat/controllers/noz_ajaxim.py
new file mode 100644
index 00000000000..abcaded7d0d
--- /dev/null
+++ b/addons/web_chat/controllers/noz_ajaxim.py
@@ -0,0 +1,21 @@
+
+import im
+from server import main # import the server library
+
+im = Webchat_IM()
+action = preg_replace('/^' . preg_quote($_SERVER['SCRIPT_NAME'], '/') . '\/(.+?)(\?.+)?$/', '\1', $_SERVER['REQUEST_URI']);
+
+if(substr(action, 0, 1) != '_' && method_exists(im, action))
+ if(action == 'poll') {
+ if($_GET['method'] == 'comet') {
+ im->poll('comet');
+ } else {
+ print $_GET['callback'] . '(' . json_encode(im->poll($_GET['method'])) . ')';
+ }
+ } else {
+ execute = call_user_func_array(array(im, action), $_POST);
+ if(execute)
+ print json_encode(execute !== false ? execute : array('e'=>'wrong args'));
+ }
+else
+ print json_encode(array('e'=>'no method'));
\ No newline at end of file
diff --git a/addons/web_chat/controllers/noz_im.py b/addons/web_chat/controllers/noz_im.py
new file mode 100644
index 00000000000..392a56645fa
--- /dev/null
+++ b/addons/web_chat/controllers/noz_im.py
@@ -0,0 +1,22 @@
+
+import openerpweb
+
+class IM(openerpweb.Controller):
+ def __init__(self):
+ super(IM, self).__init__()
+ return;
+
+ def chat_login(self, username, password):
+ return dict()
+
+ def chat_logout(self):
+ return dict()
+
+ def send(self):
+ return dict()
+
+ def status(self, status, message):
+ return dict()
+
+ def poll(self, method):
+ return dict()
diff --git a/addons/web_chat/controllers/noz_main.py b/addons/web_chat/controllers/noz_main.py
new file mode 100644
index 00000000000..b48513eaf0c
--- /dev/null
+++ b/addons/web_chat/controllers/noz_main.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+
+import simplejson
+import openerpweb
+
+from web_chat.controllers import im
+
+__all__ = ['Webchat_IM']
+
+chat_config_options = {'IM_LIBRARY': 'main', # The IM server library.
+ 'COOKIE_NAME': 'ajaxim_session', # Session cookie used by Ajax IM
+ 'COOKIE_PERIOD': 365 # Cookie period, in days
+ }
+
+nodejs_memcache_server = ['localhost:11211']
+
+class Webchat_IM(im.IM):
+
+ _cp_path = "/webchat/webchatim"
+
+ def __init__(self):
+ super(Webchat_IM, self).__init__()
+
+ return;
+
+ @openerpweb.jsonrequest
+ def index(self, req, username="admin", password="admin"):
+
+ if req._login:
+ self.username = req._login
+ self.uid = req._uid
+ self.db = req._db
+
+ return;
+
+ @openerpweb.jsonrequest
+ def chat_login(self, req, username="admin", password="admin"):
+ # TODO: use db and uid for unique identification.
+
+ self.auth = [['admin', 'a'], ['demo', 'demo']]
+
+ self.user_pass = []
+ self.user_session = None
+
+ if username and password:
+ self.user_pass = [username, password]
+
+ if self.user_pass in self.auth:
+ self.user_session = username + '_' + str(req.session_id)
+
+ data = {'username': username, 'user_id': username, 'user_session': self.user_session,
+ 'friends': {'u': 'demo', 'g': 'Friends', 's': '1'} # {'u': 'username', 'g': 'group', 's': 'status'}
+ }
+
+ # Get the list of friends from memcache.
+
+ return dict(r='logged in', s=self.user_session, f=data['friends'])
+
+ @openerpweb.jsonrequest
+ def chat_logout(self):
+ return dict(r='Logged out')
\ No newline at end of file
diff --git a/addons/web_chat/static/lib/AjaxIM/LICENSE.txt b/addons/web_chat/static/lib/AjaxIM/LICENSE.txt
new file mode 100644
index 00000000000..78685f1b2b8
--- /dev/null
+++ b/addons/web_chat/static/lib/AjaxIM/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2005 - 2010 Joshua Gross, http://ajaxim.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/addons/web_chat/static/lib/AjaxIM/js/im.js b/addons/web_chat/static/lib/AjaxIM/js/im.js
new file mode 100644
index 00000000000..fbf6a1552f3
--- /dev/null
+++ b/addons/web_chat/static/lib/AjaxIM/js/im.js
@@ -0,0 +1 @@
+// = im.js =
//
// **Copyright © 2005 – 2010 Joshua Gross**\\
// //MIT Licensed//
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This is the main library for Ajax IM. It encompasses the UI controls,
// and connecting with the server. It does //not// handle registration or
// account management.
(function($, _instance) {
AjaxIM = function(options, actions) {
if(this instanceof AjaxIM) {
var self = this;
// === {{{ defaults }}} ===
//
// These are the available settings for Ajax IM, and the associated
// defaults:
//
// * {{{pollType}}} determines the way in which the client will talk to
// the server.
// ** {{{comet}}} will use http streaming, wherein a connection to the server will be held open indefinitely, and the server will push down new events (messages, status updates) as they occur.
// ** {{{long}}} will hold open a connection with the server for as long as possible, or until a new event is received. Upon the server sending an event or closing the connection, the client will automatically—and immediately—reconnect.
// ** {{{short}}} will open a connection, and the server (if this method is supported) will //immediately// provide a response as to whether or not there are any new events. Once a response is received, the client will wait 5 seconds, and then reconnect.
// * {{{pollServer}}} is the default URL to which all actions refer. It is
// possible to specify certain action URLs separately (as is used with the
// NodeJS server).
// * {{{cookieName}}} is the name of the session cookie used by the server.
// If this is not set properly, the IM engine will not be able to automatically
// reinitialize sessions.
// * {{{theme}}} is the name of the theme folder that defines the HTML and
// CSS of the IM bar and chat boxes. Usually, themes are deposited in the
// provided "themes" folder and specified by that path, e.g. {{{themes/default}}}.
// Theme files within the theme folder must be named {{{theme.html}}} and
// {{{theme.css}}}.
// * {{{storageMethod}}} defines the way in which data (chat sessions) are
// temporarily stored client-side. By default, {{{"flash"}}} is used because
// it is the most widely supported method. However,
// [[http://eric.garside.name/docs.html?p=jstore#js-engines|other storage engines]]
// are available, with their respective up- and down-sides
// outlined, on the jStore website.
// * {{{storeSession}}} (**not implemented**) sets the number of days to
// retain stored chat session data before it should be deleted.
// * {{{checkResume}}} is a flag that sets whether or not the client should
// make a call to the server before resuming the session (such as on a page
// reload). This will ensure that the session has not expired. If set to {{{false}}},
// a call to the server will not be made, and the session will be assumed to
// be active.
var defaults = {
pollType: 'long',
pollServer: './ajaxim.php',
cookieName: 'ajaxim_session',
theme: 'themes/default',
storageMethod: 'auto',
flashStorage: 'js/jStore.Flash.html',
storeSession: 5, // number of days to store chat data (0 for current session only)
checkResume: true
};
// === {{{AjaxIM.}}}**{{{settings}}}** ===
//
// These are the settings for the IM. If particular options are not specified,
// the defaults (see above) will be used. //These options will be defined
// upon calling the initialization function, and not set directly.//
this.settings = $.extend(defaults, options);
// === {{{AjaxIM.}}}**{{{actions}}}** ===
//
// Each individual action that the IM engine can execute is predefined here.
// By default, it merely appends the action onto the end of the {{{pollServer}}} url,
// however, it is possible to define actions individually. //The alternative actions
// will be defined upon calling the initialization function, and not set directly.//
//
// Should you define an action at a different URL, Ajax IM will determine whether
// or not this URL is within the current domain. If it is within a subdomain of
// the current domain, it will set the document.domain variable for you,
// to match a broader hostname scope; the action will continue to use {{{$.post}}}
// (the default AJAX method for Ajax IM).
//
// On the other hand, should you choose a URL outside the current domain
// Ajax IM will switch to {{{$.getJSON}}} (a get request) to avoid
// cross-domain scripting issues. This means that a server on a different
// port or at a different address will need to be able to handle GET
// requests rather than POST requests (such as how the Node.JS Ajax IM
// server works).
this.actions = $.extend({
login: this.settings.pollServer + '/login',
logout: this.settings.pollServer + '/logout',
register: this.settings.pollServer + '/register',
poll: this.settings.pollServer + '/poll?method=' + this.settings.pollType,
send: this.settings.pollServer + '/send',
status: this.settings.pollServer + '/status',
resume: this.settings.pollServer + '/resume'
}, actions);
var httpRx = new RegExp('^http://', 'i'),
slashslashRx = new RegExp('^//', 'i'),
queryStrRx = new RegExp('[?](.+)$'),
subdomainRx = new RegExp('((http[s]?:)?//)?(.+?)[.]' + window.location.host, 'i');
$.each(this.actions, function(name, action) {
if(name == 'poll') {
if(self.settings.pollType != 'comet')
action += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?';
action = ['jsonp', action];
} else {
action = ['ajax', action];
if(subdomainRx.test(action[1])) {
document.domain = '.' + window.location.host;
} else if((http = httpRx.test(action[1])) || slashslashRx.test(action[1])) {
if(!(new RegExp('//' + window.location.host + '/', 'i')).test(action[1])) {
action[1] += (queryStrRx.test(action[1]) ? '&' : '?') + 'callback=?';
action = ['jsonp', action[1]];
}
}
}
self.actions[name] = action;
});
// We load the theme dynamically based on the passed
// settings. If the theme is set to false, we assume
// that the user is going to load it himself.
this.themeLoaded = false;
if(this.settings.theme) {
$('').appendTo('body').load(this.settings.theme + '/theme.html #imjs-bar, .imjs-tooltip',
function() {
self.themeLoaded = true;
setup.apply(self);
}
);
if(typeof document.createStyleSheet == 'function')
document.createStyleSheet(this.settings.theme + '/theme.css');
else
$('body').append('');
} else {
this.themeLoaded = true;
setup.apply(this);
}
// Client-side storage for keeping track of
// conversation states, active tabs, etc.
// The default is flash storage, however, other
// options are available via the jStore library.
if(this.settings.storageMethod) {
this.storageBrowserKey = 'unknown';
this.storeKey = '';
$.each($.browser, function(k, v) {
if(k == 'version') return true;
else if(v == true) {
self.storageBrowserKey = k;
return false;
}
});
if(this.settings.storageMethod == 'auto') {
$.each(['ie', 'html5', 'local', 'flash'], function() {
if($.jStore.Availability[this]()) {
self.settings.storageMethod = this;
return false;
}
});
}
$.extend($.jStore.defaults, {
project: 'im.js',
engine: this.settings.storageMethod,
flash: this.settings.flashStorage
});
this.storageReady = false;
if(this.settings.storageMethod == 'flash') {
$.jStore.ready(function(engine) {
$.jStore.flashReady(function() {
self.storageReady = true;
setup.apply(self);
});
})
} else {
$.jStore.ready(function(engine) {
self.storageReady = true;
setup.apply(self);
});
}
$.jStore.load();
} else {
this.storageReady = true;
setup.apply(this);
}
// Allow a chatbox to me minimized
$('.imjs-chatbox').live('click', function(e) {
e.preventDefault();
return false;
});
$('.imjs-chatbox .imjs-minimize').live('click', function() {
$(this).parents('.imjs-chatbox').data('tab').click();
});
// Allow a chatbox to be closed
$('.imjs-chatbox .imjs-close').live('click', function() {
var chatbox = $(this).parents('.imjs-chatbox');
chatbox.data('tab')
.data('state', 'closed').css('display', 'none');
if(self.settings.storageMethod && self.storageReady) {
delete self.chatstore[chatbox.data('username')];
if(self.storageReady) {
$.jStore.store(
self.storageBrowserKey + '-' +
self.username + '-chats',
self.chatstore);
}
}
});
// Setup message sending for all chatboxes
$('.imjs-chatbox .imjs-input').live('keydown', function(event) {
var obj = $(this);
if(event.keyCode == 13 && !($.browser.msie && $.browser.version < 8)) {
self.send(obj.parents('.imjs-chatbox').data('username'), obj.val());
}
}).live('keyup', function(event) {
if(event.keyCode == 13) {
if($.browser.msie && $.browser.version < 8) {
var obj = $(this);
self.send(obj.parents('.imjs-chatbox').data('username'), obj.val());
}
var obj = $(this);
obj.val('');
obj.height(obj.data('height'));
}
}).live('keypress', function(e) {
var obj = $(this);
if(!($.browser.msie && $.browser.opera)) obj.height(0);
if(this.scrollHeight > obj.height() || this.scrollHeight < obj.height()) {
obj.height(this.scrollHeight);
}
});
$('.imjs-msglog').live('click', function() {
var chatbox = $(this).parents('.imjs-chatbox');
chatbox.find('.imjs-input').focus();
});
// Create a chatbox when a buddylist item is clicked
$('.imjs-friend').live('click', function() {
var chatbox = self._createChatbox($(this).data('friend'));
if(chatbox.data('tab').data('state') != 'active')
chatbox.data('tab').click();
chatbox.find('.imjs-input').focus();
});
// Setup and hide the scrollers
$('.imjs-scroll').css('display', 'none');
$('#imjs-scroll-left').live('click', function() {
var hiddenTab = $('#imjs-bar li.imjs-tab:visible').slice(-1)
.next('#imjs-bar li.imjs-tab:hidden')
.filter(function() {
return $(this).data('state') != 'closed'
})
.not('.imjs-default').slice(-1).css('display', '');
if(hiddenTab.length) {
$('#imjs-bar li.imjs-tab:visible').eq(0).css('display', 'none');
$(this).html(parseInt($(this).html()) - 1);
$('#imjs-scroll-right').html(parseInt($('#imjs-scroll-right').html()) + 1);
}
return false;
});
$('#imjs-scroll-right').live('click', function() {
var hiddenTab = $('#imjs-bar li.imjs-tab:visible').eq(0)
.prev('#imjs-bar li.imjs-tab:hidden')
.filter(function() {
return $(this).data('state') != 'closed'
})
.not('.imjs-default').slice(-1).css('display', '');
if(hiddenTab.length) {
$('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
$(this).html(parseInt($(this).html()) - 1);
$('#imjs-scroll-left').html(parseInt($('#imjs-scroll-left').html()) + 1);
}
return false;
});
// Initialize the chatbox hash
this.chats = {};
// Try to resume any existing session
this.resume();
$(window).resize(function() {
self.bar._scrollers();
});
} else
return AjaxIM.init(options);
};
// We predefine all public functions here...
// If they are called before everything (theme, storage engine) has loaded,
// then they get put into a "prequeue" and run when everything *does*
// finally load.
//
// This ensures that nothing loads without all of the principal components
// being pre-loaded. If that were to occur (without this prequeue), things
// would surely break.
var prequeue = [];
var empty = function() {
var func = this;
return function() { prequeue.push([func, arguments]) };
};
$.extend(AjaxIM.prototype, {
settings: {},
friends: {},
chats: {},
storage: empty.apply('storage'),
login: empty.apply('login'),
logout: empty.apply('logout'),
resume: empty.apply('resume'),
form: empty.apply('form'),
poll: empty.apply('poll'),
incoming: empty.apply('incoming'),
send: empty.apply('send'),
status: empty.apply('status'),
statuses: {},
bar: {
initialize: empty.apply('bar.initialize'),
activateTab: empty.apply('bar.activateTab'),
closeTab: empty.apply('bar.closeTab'),
addTab: empty.apply('bar.addTab'),
notification: empty.apply('bar.notification'),
_scrollers: empty.apply('bar._scrollers')
}
});
setup = (function() {
var self = this;
if(!this.storageReady || !this.themeLoaded) return;
$(self).trigger('loadComplete');
$(window).resize();
$.extend(AjaxIM.prototype, {
// == Cookies ==
//
// The "cookies" functions can be used to set, get, and erase JSON-based cookies.
// These functions are primarily used to manage and read the server-set cookie
// that handles the user's chat session ID.
cookies: {
// === {{{AjaxIM.}}}**{{{cookies.set(name, value, days)}}}** ===
//
// Sets a cookie, stringifying the JSON value upon storing it.
//
// ==== Parameters ====
// * {{{name}}} is the cookie name.\\
// * {{{value}}} is the cookie data that you would like to store.\\
// * {{{days}}} is the number of days that the cookie will be stored for.
set: function(name, value, days) {
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "; expires=" + date.toGMTString();
} else var expires = "";
document.cookie = name + "=" + $.compactJSON(value) + expires + "; path=/";
},
// === {{{AjaxIM.}}}**{{{cookies.get(name)}}}** ===
//
// Gets a cookie, decoding the JSON value before returning the data.
//
// ==== Parameters ====
// * {{{name}}} is the cookie name that you would like to retrieve.
get: function(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) {
var cval = decodeURIComponent(c.substring(nameEQ.length, c.length));
return $.secureEvalJSON(cval);
}
}
return null;
},
// === {{{AjaxIM.}}}**{{{cookies.erase(name)}}}** ===
//
// Deletes a cookie.
//
// {{{name}}} is the existing cookie that you would like to delete.
erase: function(name) {
self.cookies.set(name, '', -1);
}
},
// == Main ==
// === {{{AjaxIM.}}}**{{{storage()}}}** ===
//
// Retrieves chat session data from whatever storage engine is enabled
// (provided that one is enabled at all). If a page reloads, this function
// is called to restore the user's chat state (existing conversations, active tab).
// This function is called //automatically//, upon initialization of the IM engine.
storage: function() {
if(!self.storeKey.length) return;
try {
var chatstore = $.jStore.store(self.storeKey + 'chats') || {};
} catch(e) {
$.jStore.remove(self.storeKey + 'chats');
var chatstore = {};
}
if(this.chatstore) {
$.each(this.chatstore, function(username, convo) {
if(username in chatstore)
chatstore[username] = $.merge(chatstore[username], self.chatstore[username]);
else
chatstore[username] = self.chatstore[username];
});
this.chatstore = chatstore;
$.jStore.store(self.storeKey + 'chats', chatstore);
} else {
this.chatstore = chatstore;
}
$.each(this.chatstore, function(username, convo) {
if(!convo.length) return;
var chatbox = self._createChatbox(username, true);
chatbox.data('lastDateStamp', null).css('display', 'none');
// Remove the automatic date stamp
chatbox.find('.imjs-msglog').empty();
// Restore all messages, date stamps, and errors
$.each(convo, function() {
switch(this[0]) {
case 'error':
self._addError(chatbox, decodeURIComponent(this[2]), this[3]);
break;
case 'datestamp':
self._addDateStamp(chatbox, this[3]);
break;
case 'a':
case 'b':
self._addMessage(this[0], chatbox, this[1],
decodeURIComponent(this[2]), this[3]);
break;
}
});
$(self).trigger('chatRestored', [username, chatbox]);
});
var activeTab = $.jStore.store(self.storeKey + 'activeTab') || [];
if(activeTab.length && (activeTab = activeTab[0]) && activeTab in this.chats) {
this.chats[activeTab].data('tab').click();
var msglog = this.chats[activeTab].find('.imjs-msglog');
msglog[0].scrollTop = msglog[0].scrollHeight;
}
},
// === {{{AjaxIM.}}}**{{{login(username, password)}}}** ===
//
// Authenticates a user and initializes the IM engine. If the user is
// already logged in, [s]he is logged out, the session is cleared, and
// the new user is logged in.
//
// Returns the user's properly formatted username, session ID, and online
// friends in JSON, if successful; e.g.:\\
// {{{ {u: 'username', s: 'longsessionid', f: [{u: 'friend', s: 1, g: 'group'}]} }}}
//
// If unsuccessful, {{{false}}} is returned.
//
// ==== Parameters ====
// * {{{username}}} is the user's username.\\
// * {{{password}}} is the user's password. This password will be MD5 hashed
// before it is sent to the server.
login: function(username, password) {
if(!username) username = '';
if(!password) password = '';
if(this.username)
return true; // Already logged in!
// hash password before sending it to the server
password = $.md5(password);
// authenticate
AjaxIM.request(
this.actions.login,
{'username': username, 'password': password},
function(auth) {
if(auth.r == 'logged in') {
self.username = ('u' in auth ? auth.u : username);
if(self.settings.storageMethod)
self.storeKey = [self.storageBrowserKey, self.username, ''].join('-');
var existing = self.cookies.get(self.settings.cookieName);
self.storage();
// Begin the session
$.each(auth.f, function() {
self.friends[this.u] = {status: [this.s, ''], group: this.g};
});
self._session(self.friends);
self._storeFriends();
$(self).trigger('loginSuccessful', [auth]);
return auth;
} else {
$(self).trigger('loginError', [auth]);
}
return false;
}
);
},
// === {{{AjaxIM.}}}**{{{logout()}}}** ===
//
// Logs the user out and removes his/her session cookie. As well, it
// will close all existing chat windows, clear the storage engine, and
// remove the IM bar.
logout: function() {
AjaxIM.request(
this.actions.logout,
{},
function(done) {
if(done) {
self.cookies.erase(self.settings.cookieName);
self._clearSession();
$(self).trigger('logoutSuccessful');
return true;
} else {
$(self).trigger('logoutError');
return false;
}
}
);
},
// === {{{AjaxIM.}}}**{{{resume()}}}** ===
//
// Resumes an existing session based on a session ID stored in the
// server-set cookie. This function is called //automatically// upon IM
// engine (re-)initialization, so the user does not need to re-login
// should a session already exist.
resume: function() {
var session = this.cookies.get(this.settings.cookieName);
if(session && session.sid) {
this.username = session.user;
this.storeKey = [this.storageBrowserKey, this.username, ''].join('-');
var friends = $.jStore.store(this.storeKey + 'friends') || [];
if(self.settings.checkResume) {
AjaxIM.request(
this.actions.resume,
{},
function(response) {
if(response.r == 'connected') {
self._session(friends);
self.storage();
} else {
var username = this.username;
self._clearSession();
$(self).trigger('sessionNotResumed', [username]);
}
}
);
} else {
self._session(friends);
self.storage();
}
} else {
$(self).trigger('noSession');
}
},
// === //private// {{{AjaxIM.}}}**{{{_session(friends)}}}** ===
//
// Restores session data (username, friends) and begins polling the server.
// Called only by {{{AjaxIM.resume()}}}.
//
// ==== Parameters ====
// * {{{friends}}} is a list of "friend" objects, e.g.:\\
// {{{[{u: 'friend', s: 1, g: 'group'}, ...]}}}
// ** {{{u}}} being the friend's username.
// ** {{{s}}} being one of the available status codes (see {{{AjaxIM.statuses}}}), depending on the friend's current status.
// ** {{{g}}} being the group that the friend is in.
_session: function(friends) {
$('#imjs-friends-panel .imjs-header span').html(this.username);
$('#imjs-friends').removeClass('imjs-not-connected');
$.each(friends, function(friend, info) {
self.addFriend.apply(self, [friend, info.status, info.group]);
});
self._storeFriends();
$(self).trigger('sessionResumed', [this.username]);
setTimeout(function() { self.poll(); }, 0);
},
// === //private// {{{AjaxIM.}}}**{{{_clearSession()}}}** ===
//
// Clears all session data from the last known user.
_clearSession: function() {
if(self.settings.storageMethod && self.storageReady) {
$.jStore.remove(self.storeKey + 'chats');
$.jStore.remove(self.storeKey + 'friends');
$.jStore.remove(self.storeKey + 'activeTab');
}
self.chats = {};
$('.imjs-tab').not('.imjs-tab.imjs-default').remove();
self.cookies.erase('ajaxim_session');
delete self.username;
},
// === {{{AjaxIM.}}}**{{{form(element)}}}** ===
//
// Loads a login and registration form into the specified element
// or, if no element is supplied, to the location on the page from
// which this function was called.
form: function(element) {
$(element).load(this.settings.theme + '/theme.html #imjs-lr', function() {
$('#imjs-lr .error').hide();
if(self.username) {
$('#imjs-register, #imjs-login fieldset').hide();
$('#imjs-logged-in')
.show()
.html($('#imjs-logged-in').html().replace('{username}', self.username));
} else {
$('#imjs-logged-in').hide();
}
// Handle logout success
$(self).bind('logoutSuccessful', function() {
$('#imjs-login fieldset').slideDown();
$('#imjs-register').slideDown();
$('#imjs-logged-in')
.html($('#imjs-logged-in strong').html('{username}'))
.slideUp();
});
$('#imjs-logged-in a').click(function() {
self.logout();
return false;
});
// Handle login error or success
$(self).bind('loginError', function() {
$('#imjs-login .error').html(AjaxIM.i18n.authInvalid).slideDown('fast');
$('#imjs-login input')
.addClass('imjs-lr-error')
.blur(function() { $(this).removeClass('imjs-lr-error'); });
}).bind('loginSuccessful', function() {
$('#imjs-login fieldset').slideUp();
$('#imjs-register').slideUp();
$('#imjs-logged-in')
.html($('#imjs-logged-in').html().replace('{username}', self.username))
.slideDown();
});
var login = function() {
self.login($('#imjs-login-username').val(), $('#imjs-login-password').val());
return false;
};
$('#imjs-login').submit(login);
$('#imjs-login-submit').click(login);
var regIssues = false;
var regError = function(error, fields) {
$('#imjs-register .error')
.append(AjaxIM.i18n['register' + error] + ' ')
.slideDown();
$(fields)
.addClass('imjs-lr-error')
.blur(function() { $(this).removeClass('imjs-lr-error'); });
regIssues = true;
};
var register = function() {
$('#imjs-register .error').empty();
regIssues = false;
var username = $('#imjs-register-username').val();
var password = $('#imjs-register-password').val();
if(password.length < 4)
regError('PasswordLength', '#imjs-register-password');
if(password != $('#imjs-register-cpassword').val())
regError('PasswordMatch', '#imjs-register-password, #imjs-register-cpassword');
if(username.length <= 2 ||
!$('#imjs-register-username').val().match(/^[A-Za-z0-9_.]+$/))
regError('UsernameLength', '#imjs-register-username');
if(!regIssues) {
AjaxIM.request(
self.actions.register,
{username: username, password: password},
function(response) {
if(response.r == 'registered') {
self.login(username, password);
} else if(response.r == 'error') {
switch(response.e) {
case 'unknown':
regError('Unknown', '');
break;
case 'invalid password':
regError('PasswordLength', '#imjs-register-password');
break;
case 'invalid username':
regError('UsernameLength', '#imjs-register-username');
break;
case 'username taken':
regError('UsernameTaken', '#imjs-register-username');
break;
}
}
},
function(error) {
regError('Unknown', '');
}
);
}
return false;
};
$('#imjs-register').submit(register);
$('#imjs-register-submit').click(register);
});
},
// === {{{AjaxIM.}}}**{{{poll()}}}** ===
//
// Queries the server for new messages. If a 'long' or 'short' poll
// type is used, jQuery's {{{$.post}}} or {{{$.getJSON}}} will be
// used. If 'comet' is used, the server connection will be deferred
// to the comet set of functions.
poll: function() {
if(/^(short|long)$/.test(this.settings.pollType)) {
AjaxIM.request(
this.actions.poll,
{},
function(response) {
if(!response['e']) {
if(response.length)
self._parseMessages(response);
setTimeout(function() { self.poll(); }, 0);
} else {
switch(response.e) {
case 'no session found':
self._notConnected();
break;
}
$(self).trigger('pollFailed', [response.e]);
}
},
function(error) {
self._notConnected();
$(self).trigger('pollFailed', ['not connected']);
// try reconnecting?
}
);
} else if(this.settings.pollType == 'comet') {
this.comet.connect();
}
},
// === //private// {{{AjaxIM.}}}**{{{_parseMessages(messages)}}}** ===
//
// Handles an incoming message array:\\
// {{{[{t: 'type', s: 'sender', r: 'recipient', m: 'message'}, ...]}}}
//
// * {{{t}}} (message type) is one of:
// ** {{{m}}} — a standard message
// ** {{{s}}} — a user's status update
// ** {{{b}}} — a broadcasted message (sent to many users simultaneously)
// * {{{s}}} is the sender of the message.
// * {{{r}}} is the intended recipient of the message. Most of the time, this will
// simply be the logged in user, however, a broadcasted message may not specify
// a recipient or may specify a different recipient. Also provides future
// compatability for chatrooms.
// * {{{m}}} is the actual message. For something such as a status update, this can
// be a JSON object or parsable string; e.g. {{{"2:I'm away."}}}
//
// ==== Parameters ====
// * {{{messages}}} is the message array
_parseMessages: function(messages) {
if($.isArray(messages)) {
$.each(messages, function() {
$(self).trigger('parseMessage', [this]);
switch(this.t) {
case 'm':
self.incoming(this.s, this.m);
break;
case 's':
var status = this.m.split(':');
if(this['g'])
self.addFriend(this.s, status, this.g);
self._friendUpdate(this.s, status[0], status.slice(1).join(':'));
self._storeFriends();
break;
case 'b':
break;
default:
break;
}
});
}
},
// === {{{AjaxIM.}}}**{{{incoming(from, message)}}}** ===
//
// Handles a new message from another user. If a chatbox for that
// user does not yet exist, one is created. If it does exist, but
// is minimized, the user is notified but the chatbox is not brought
// to the front. This function also stores the message, if a storage
// method is set.
//
// ==== Parameters ====
// * {{{from}}} is the username of the sender.
// * {{{message}}} is the body.
incoming: function(from, message) {
// check if IM exists, otherwise create new window
// TODO: If friend is not on the buddylist,
// should add them to a temp list?
var chatbox = this._createChatbox(from);
if(!$('#imjs-bar .imjs-selected').length) {
chatbox.data('tab').click();
} else if(chatbox.data('tab').data('state') != 'active') {
this.bar.notification(chatbox.data('tab'));
}
var time = this._addMessage('b', chatbox, from, message);
this._storeMessage('b', chatbox, from, message, time);
},
// === {{{AjaxIM.}}}**{{{addFriend(username, group)}}}** ===
//
// Inserts a new friend into the friends list. If the group specified
// doesn't exist, it is created. If the friend is already in this group,
// they aren't added again, however, the friend item is returned.
//
// ==== Parameters ====
// * {{{username}}} is the username of the new friend.
// * {{{status}}} is the current status of the friend.
// * {{{group}}} is the user group to which the friend should be added.
addFriend: function(username, status, group) {
var status_name = 'available';
$.each(this.statuses,
function(key, val) { if(status[0] == val) { status_name = key; return false; } });
var group_id = 'imjs-group-' + $.md5(group);
if(!(group_item = $('#' + group_id)).length) {
var group_item = $('.imjs-friend-group.imjs-default').clone()
.removeClass('imjs-default')
.attr('id', group_id)
.data('group', group)
.appendTo('#imjs-friends-list');
var group_header = group_item.find('.imjs-friend-group-header');
group_header.html(group_header.html().replace('{group}', group));
}
var user_id = 'imjs-friend-' + $.md5(username + group);
if(!$('#' + user_id).length) {
var user_item = group_item.find('ul li.imjs-default').clone()
.removeClass('imjs-default')
.addClass('imjs-' + status_name)
.attr('id', user_id)
.data('friend', username)
.appendTo(group_item.find('ul'));
if(status[0] == 0) user_item.hide();
user_item.html(user_item.html().replace('{username}', username));
}
this.friends[username] = {'status': status, group: group};
this._updateFriendCount();
return this.friends[username];
},
// === //private// {{{AjaxIM.}}}**{{{_updateFriendCount()}}}** ===
//
// Counts the number of online friends and updates the friends count
// in the friend tab.
_updateFriendCount: function() {
var friendsLength = 0;
for(var f in this.friends) {
if(this.friends[f].status[0] != 0)
friendsLength++;
}
$('#imjs-friends .imjs-tab-text span span').html(friendsLength);
},
// === //private// {{{AjaxIM.}}}**{{{_storeFriends()}}}** ===
//
// If a storage method is enabled, the current state of the
// user's friends list is stored.
_storeFriends: function() {
if(this.settings.storageMethod && this.storageReady)
$.jStore.store(this.storeKey + 'friends', this.friends);
},
// === //private// {{{AjaxIM.}}}**{{{_createChatbox(username)}}}** ===
//
// Builds a chatbox based on the default chatbox HTML and CSS defined
// in the current theme. Should a chatbox for this user already exist,
// a new one is not created. Instead, it is either given focus (should
// no other windows already have focus), or a notification is issued.
//
// As well, if the chatbox does not exist, an associated tab will be
// created.
//
// ==== Parameters ====
// * {{{username}}} is the name of the user for whom the chatbox is intended
// for.
// * {{{no_stamp}}} sets whther or not to add a date stamp to the chatbox
// upon creation.
//
// //Note:// New chatboxes are given an automatically generated ID in the
// format of {{{#imjs-[md5 of username]}}}.
_createChatbox: function(username, no_stamp) {
var chatbox_id = 'imjs-' + $.md5(username);
if(!(chatbox = $('#' + chatbox_id)).size()) {
// add a tab
var tab = this.bar.addTab(username, '#' + chatbox_id);
var chatbox = tab.find('.imjs-chatbox');
chatbox.attr('id', chatbox_id);
chatbox.data('tab', tab);
// remove default items from the message log
var message_log = chatbox.find('.imjs-msglog').empty();
// setup the chatbox header
var cb_header = chatbox.find('.imjs-header');
cb_header.html(cb_header.html().replace('{username}', username));
if(!no_stamp) {
// add a date stamp
var time = this._addDateStamp(chatbox);
this._storeNonMessage('datestamp', username, null, time);
}
// associate the username with the object and vice-versa
this.chats[username] = chatbox;
chatbox.data('username', username);
// did this chatbox fall down?
this.bar._scrollers();
if(username in this.friends) {
status = this.friends[username].status;
var status_name = 'available';
$.each(this.statuses,
function(key, val) { if(status[0] == val) { status_name = key; return false; } });
tab.addClass('imjs-' + status_name);
}
// store inputbox height
//var input = chatbox.find('.imjs-input');
//input.data('height', input.height());
} else if(chatbox.data('tab').data('state') == 'closed') {
chatbox.find('.imjs-msglog > *').addClass('imjs-msg-old');
var tab = chatbox.data('tab');
if(tab.css('display') == 'none')
tab.css('display', '').removeClass('imjs-selected')
.appendTo('#imjs-bar');
if(!no_stamp) {
// possibly add a date stamp
var time = this._addDateStamp(chatbox);
this._storeNonMessage('datestamp', username, null, time);
}
if(!$('#imjs-bar .imjs-selected').length) {
tab.click();
} else {
this.bar.notification(tab);
}
}
return chatbox;
},
// === //private// {{{AjaxIM.}}}**{{{_addDateStamp(chatbox)}}}** //
//
// Adds a date/time notifier to a chatbox. These are generally
// inserted upon creation of a chatbox, or upon the date changing
// since the last time a date stamp was added. If a date stamp for
// the current date already exists, a new one will not be added.
//
// ==== Parameters ====
// * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
// * {{{time}}} is the date/time the date stamp will show. It is specified
// in milliseconds since the Unix Epoch. This is //only// defined when
// date stamps are being restored from storage; if not specified, the
// current computer time will be used.
_addDateStamp: function(chatbox, time) {
var message_log = $(chatbox).find('.imjs-msglog');
if(!time)
time = (new Date()).getTime();
var date_stamp = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone();
var date_stamp_time = date_stamp.find('.imjs-msg-time');
if(date_stamp_time.length)
date_stamp_time.html(AjaxIM.dateFormat(time, date_stamp_time.html()));
var date_stamp_date = date_stamp.find('.imjs-date-date');
var formatted_date = AjaxIM.dateFormat(time, date_stamp_date.html());
if(chatbox.data('lastDateStamp') != formatted_date) {
if(date_stamp_date.length)
date_stamp_date.html(AjaxIM.dateFormat(time, date_stamp_date.html()));
chatbox.data('lastDateStamp', formatted_date);
date_stamp.appendTo(message_log);
} else {
//$('').appendTo(message_log);
}
return time;
},
// === //private// {{{AjaxIM.}}}**{{{_addError(chatbox, error)}}}** //
//
// Adds an error to a chatbox. These are generally inserted after
// a user sends a message unsuccessfully. If an error message
// was already added, another one will be added anyway.
//
// ==== Parameters ====
// * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
// * {{{error}}} is the error message string.
// * {{{time}}} is the date/time the error occurred. It is specified in
// milliseconds since the Unix Epoch. This is //only// defined when
// errors are being restored from storage; if not specified, the current
// computer time will be used.
_addError: function(chatbox, error, time) {
var message_log = $(chatbox).find('.imjs-msglog');
var error_item =
$('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-error').clone();
var error_item_time = error_item.find('.imjs-msg-time');
if(error_item_time.length) {
if(!time)
time = (new Date()).getTime();
error_item_time.html(AjaxIM.dateFormat(time, error_item_time.html()));
}
error_item.find('.imjs-error-error').html(error);
error_item.appendTo(message_log);
message_log[0].scrollTop = message_log[0].scrollHeight;
},
// === //private// {{{AjaxIM.}}}**{{{_addMessage(ab, chatbox, username, message, time)}}}** //
//
// Adds a message to a chatbox. Depending on the {{{ab}}} value,
// the color of the username may change as a way of visually
// identifying users (however, this depends on the theme's CSS).
// A timestamp is added to the message, and the chatbox is scrolled
// to the bottom, such that the new message is visible.
//
// Messages will be automatically tag-escaped, so as to prevent
// any potential cross-site scripting problems. Additionally,
// URLs will be automatically linked.
//
// ==== Parameters ====
// * {{{ab}}} refers to whether the user is "a" or "b" in a conversation.
// For the general case, "you" are "a" and "they" are "b".
// * {{{chatbox}}} refers to the jQuery-selected chatbox DOM element.
// * {{{username}}} is the username of the user who sent the message.
// * {{{time}}} is the time the message was sent in milliseconds since
// the Unix Epoch. This is //only// defined when messages are being
// restored from storage. For new messages, the current computer
// time is automatically used.
_addMessage: function(ab, chatbox, username, message, time) {
var last_message = chatbox.find('.imjs-msglog > *:last-child');
if(last_message.hasClass('imjs-msg-' + ab)) {
// Last message was from the same person, so let's just add another imjs-msg-*-msg
var message_container = (last_message.hasClass('imjs-msg-' + ab + '-container') ?
last_message :
last_message.find('.imjs-msg-' + ab + '-container'));
var single_message =
$('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-msg-' + ab + '-msg')
.clone().appendTo(message_container);
single_message.html(single_message.html().replace('{username}', username));
} else if(!last_message.length || !last_message.hasClass('imjs-msg-' + ab)) {
var message_group = $('.imjs-tab.imjs-default .imjs-chatbox .imjs-msg-' + ab)
.clone().appendTo(chatbox.find('.imjs-msglog'));
message_group.html(message_group.html().replace('{username}', username));
var single_message = message_group.find('.imjs-msg-' + ab + '-msg');
}
// clean up the message
message = message.replace(//g, '>')
.replace(/(^|.*)\*([^*]+)\*(.*|$)/, '$1$2$3');
// autolink URLs
message = message.replace(
new RegExp('([A-Za-z][A-Za-z0-9+.-]{1,120}:[A-Za-z0-9/]' +
'(([A-Za-z0-9$_.+!*,;/?:@&~=-])|%[A-Fa-f0-9]{2}){1,333}' +
'(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*,;/?:@&~=%-]{0,1000}))?)', 'g'),
'$1');
// insert the message
single_message.html(single_message.html().replace('{message}', message));
// set the message time
var msgtime = single_message.find('.imjs-msg-time');
if(!time)
time = new Date();
if(typeof time != 'string')
time = AjaxIM.dateFormat(time, msgtime.html());
msgtime.html(time);
var msglog = chatbox.find('.imjs-msglog');
msglog[0].scrollTop = msglog[0].scrollHeight;
return time;
},
// === //private// {{{AjaxIM.}}}**{{{_storeNonMessage(type, username, data, time)}}}** ===
//
// **Redundant?**\\
// Similar to {{{AjaxIM._storeMessage}}}, but stores items that aren't messages,
// such as datestamps and errors.
//
// ==== Parameters ====
// * {{{type}}} is the type of non-message (error, datestamp).
// * {{{username}}} is the username of the user being chatted with.
// * {{{data}}} is the (optional) data of the non-message to be stored.
// * {{{time}}} is the time of the message.
_storeNonMessage: function(type, username, data, time) {
// If storage is on & ready, store the non-message
if(this.settings.storageMethod) {
if(!this.chatstore) this.chatstore = {};
if(!(username in this.chatstore)) this.chatstore[username] = [];
// If the chat store gets too long, it becomes slow to load.
if(this.chatstore[username].length > 300)
this.chatstore[username].shift();
// For some reason, if we don't encode and decode the message, it *will* break
// (at least) the Flash storage engine's retrieval. Gah!
this.chatstore[username].push(
[type, username, encodeURIComponent(data), time]);
if(this.storageReady) $.jStore.store(self.storeKey + 'chats', this.chatstore);
}
},
// === //private// {{{AjaxIM.}}}**{{{_storeMessage(ab, chatbox, username, message, time)}}}** ===
//
// Taking the same arguments as {{{AjaxIM._addMessage}}}, {{{_storeMessage}}} pushes a message
// into the storage hash, if storage is enabled.
//
// Messages are added to a message array, by username. The entire chat hash is stored as
// a {{{'chats'}}} object in whatever storage engine is enabled.
_storeMessage: function(ab, chatbox, username, message, time) {
// If storage is on & ready, store the message
if(this.settings.storageMethod) {
if(!this.chatstore) this.chatstore = {};
message = message.replace(//g, '>');
if(!(username in this.chatstore)) {
this.chatstore[username] = [];
} else if(this.chatstore[username].length > 300) {
// If the chat store gets too long, it becomes slow to load.
this.chatstore[username].shift();
}
// For some reason, if we don't encode and decode the message, it *will* break
// (at least) the Flash storage engine's retrieval. Gah!
this.chatstore[chatbox.data('username')].push(
[ab, username, encodeURIComponent(message), time]);
if(this.storageReady) $.jStore.store(this.storeKey + 'chats', this.chatstore);
}
},
// === //private// {{{AjaxIM.}}}**{{{_friendUpdate(friend, status, statusMessage)}}}** ===
//
// Called when a friend's status is updated. This function will update all locations
// where a status icon is displayed (chat tab, friends list), as well as insert
// a notification, should a chatbox be open.
//
// ==== Parameters ====
// * {{{friend}}} is the username of the friend.
// * {{{status}}} is the new status code. See {{{AjaxIM.statuses}}} for a list of available
// codes. //Note: If an invalid status is specified, no action will be taken.//
// * {{{statusMessage}}} is a message that was, optionally, specified by the user. It will be
// used should "you" send the user an IM while they are away, or if their status is viewed
// in another way (such as via the friends list [**not yet implemented**]).
_friendUpdate: function(friend, status, statusMessage) {
// add friend to buddylist, update their status, etc.
var status_name = 'available';
$.each(this.statuses,
function(key, val) { if(status == val) { status_name = key; return false; } });
if(this.chats[friend]) {
var tab = this.chats[friend].data('tab');
var tab_class = 'imjs-tab';
if(tab.data('state') == 'active') tab_class += ' imjs-selected';
tab_class += ' imjs-' + status_name;
tab.attr('class', tab_class);
// display the status in the chatbox
var date_stamp =
$('.imjs-tab.imjs-default .imjs-chatbox .imjs-msglog .imjs-date').clone();
var date_stamp_time = date_stamp.find('.imjs-msg-time');
if(date_stamp_time.length)
date_stamp_time.html(AjaxIM.dateFormat(date_stamp_time.html()));
var date_stamp_date = date_stamp.find('.imjs-date-date').html(
AjaxIM.i18n[
'chat' + status_name[0].toUpperCase() + status_name.slice(1)
].replace(/%s/g, friend));
var msglog = this.chats[friend].find('.imjs-msglog');
date_stamp.appendTo(msglog);
msglog[0].scrollTop = msglog[0].scrollHeight;
}
if(this.friends[friend]) {
var friend_id = 'imjs-friend-' + $.md5(friend + this.friends[friend].group);
$('#' + friend_id).attr('class', 'imjs-friend imjs-' + status_name);
if(status == 0) {
$('#' + friend_id + ':visible').slideUp();
$('#' + friend_id + ':hidden').hide();
} else if(!$('#' + friend_id + ':visible').length) {
$('#' + friend_id).slideDown();
}
this.friends[friend].status = [status, statusMessage];
this._updateFriendCount();
}
},
// === //private// {{{AjaxIM.}}}**{{{_notConnected()}}}** ===
//
// Puts the user into a visible state of disconnection. Sets the
// friends list to "not connected" and empties it; disallows new messages
// to be sent.
_notConnected: function() {
$('#imjs-friends').addClass('imjs-not-connected').unbind('click', this.activateTab);
},
// === {{{AjaxIM.}}}**{{{send(to, message)}}}** ===
//
// Sends a message to another user. The message will be added
// to the chatbox before it is actually sent, however, if an
// error occurs during sending, that will be indicated immediately
// afterward.
//
// After sending the message, one of three status codes should be
// returned as a JSON object, e.g. {{{{r: 'code'}}}}:
// * {{{ok}}} — Message was sent successfully.
// * {{{offline}}} — The user is offline or unavailable to
// receive messages.
// * {{{error}}} — a problem occurred, unrelated to the user
// being unavailable.
//
// ==== Parameters ====
// * {{{to}}} is the username of the recipient.
// * {{{message}}} is the content to be sent.
send: function(to, message) {
if(!message) return;
if(this.chats[to]) { // REMOVE ME?
// possibly add a datestamp
var time = self._addDateStamp(this.chats[to]);
this._storeNonMessage('datestamp', to, null, time);
time = this._addMessage('a', this.chats[to], this.username, message);
this._storeMessage('a', this.chats[to], this.username, message, time);
}
$(self).trigger('sendingMessage', [to, message]);
AjaxIM.request(
this.actions.send,
{'to': to, 'message': message},
function(result) {
switch(result.r) {
case 'ok':
$(self).trigger('sendMessageSuccessful', [to, message]);
break;
case 'offline':
$(self).trigger('sendMessageFailed', ['offline', to, message]);
break;
case 'error':
default:
if(result.e == 'no session found') {
self._notConnected();
self._addError(self.chats[to], AjaxIM.i18n.notConnected);
self._storeNonMessage('error', to,
AjaxIM.i18n.notConnected, (new Date()).getTime());
}
$(self).trigger('sendMessageFailed', [result.e, to, message]);
break;
}
},
function(error) {
self._notConnected();
self._addError(self.chats[to], AjaxIM.i18n.notConnected);
self._storeNonMessage('error', to,
AjaxIM.i18n.notConnected, (new Date()).getTime());
$(self).trigger('sendMessageFailed', ['not connected', to, message]);
}
);
},
// === {{{AjaxIM.}}}**{{{status(s, message)}}}** ===
//
// Sets the user's status and status message. It is possible to not
// set a status message by setting it to an empty string. The status
// will be sent to the server, where upon the server will broadcast
// the update to all individuals with "you" on their friends list.
//
// ==== Parameters ====
// * {{{s}}} is the status code, as defined by {{{AjaxIM.statuses}}}.
// * {{{message}}} is the custom status message.
status: function(s, message) {
// update status icon(s)
if(!this.statuses[s])
return;
$('#imjs-friends').attr('class', 'imjs-' + s);
$(self).trigger('changingStatus', [s, message]);
AjaxIM.request(
this.actions.status,
{'status': this.statuses[s], 'message': message},
function(result) {
switch(result.r) {
case 'ok':
$(self).trigger('changeStatusSuccessful', [s, message]);
break;
case 'error':
default:
$(self).trigger('changeStatusFailed', [result.e, s, message]);
break;
}
},
function(error) {
$(self).trigger('changeStatusFailed', ['not connected', s, message]);
}
);
},
// === {{{AjaxIM.}}}**{{{statuses}}}** ===
//
// These are the available status codes and their associated identities:
// * {{{offline}}} (0) — Only used when signing out/when another
// user has signed out, as once this status is set, the user is removed
// from the server and friends will be unable to contact the user.
// * {{{available}}} (1) — The user is online and ready to be messaged.
// * {{{away}}} (2) — The user is online but is not available. Others
// may still contact this user, however, the user may not respond. Anyone
// contacting an away user will receive a notice stating that the user is away,
// and (if one is set) their custom status message.
// * {{{invisible}}} (3; **not yet implemented**) — The user is online,
// but other users are made unaware, and the user will be represented
// as being offline. It is still possible to contact this user, and for this
// user to contact others; no status message or notice will be sent to others
// messaging this user.
statuses: {offline: 0, available: 1, away: 2, invisible: 3},
// == Footer bar ==
//
// The footer bar is the bar that sits at the bottom of the page, in a fixed
// position. It contains a tab for the friends list, and tabs for any open
// chat boxes. It is also possible to add custom tabs for other functionality.
bar: {
// === {{{AjaxIM.}}}**{{{bar.initialize()}}}** ===
//
// Setup the footer bar and enable tab actions. This function
// uses {{{jQuery.live}}} to set hooks on any bar tabs created
// in the future.
initialize: function() {
// Set up your standard tab actions
$('.imjs-tab')
.live('click', this.activateTab);
$('.imjs-tab .imjs-close')
.live('click', this.closeTab);
// Set up the friends list actions
var self = this;
$(document).click(function(e) {
if(e.target.id == 'imjs-friends' ||
$(e.target).parents('#imjs-friends').length) {
return;
}
if($('#imjs-friends').data('state') == 'active')
self.activateTab.call($('#imjs-friends'));
});
$('#imjs-friends')
.data('state', 'minimized')
.click(function(e) {
if(!$(this).hasClass('imjs-not-connected') &&
e.target.id != 'imjs-friends-panel' &&
!$(e.target).parents('#imjs-friends-panel').length)
self.activateTab.call(this);
})
.mouseenter(function() {
if($(this).hasClass('imjs-not-connected')) {
$('.imjs-tooltip').css('display', 'block');
$('.imjs-tooltip p').html(AjaxIM.i18n.notConnectedTip);
var tip_left = $(this).offset().left -
$('.imjs-tooltip').outerWidth() +
($(this).outerWidth() / 2);
var tip_top = $(this).offset().top -
$('.imjs-tooltip').outerHeight(true);
$('.imjs-tooltip').css({
left: tip_left,
top: tip_top
});
}
})
.mouseleave(function() {
if($(this).hasClass('imjs-not-connected')) {
$('.imjs-tooltip').css('display', '');
}
});
$('#imjs-friends-panel')
.data('tab', $('#imjs-friends'))
.css('display', 'none');
},
// === {{{AjaxIM.}}}**{{{bar.activateTab()}}}** ===
//
// Activate a tab by setting it to the 'active' state and
// showing any related chatbox. If a chatbox is available
// for this tab, also focus the input box.
//
// //Note:// {{{this}}}, here, refers to the tab DOM element.
activateTab: function() {
var chatbox = $(this).find('.imjs-chatbox') || false;
if($(this).data('state') != 'active') {
if($(this).attr('id') != 'imjs-friends') {
$('#imjs-bar > li')
.not($(this))
.not('#imjs-friends')
.removeClass('imjs-selected')
.each(function() {
if($(this).data('state') != 'closed') {
$(this).data('state', 'minimized');
var chatbox = $(this).find('.imjs-chatbox');
if(chatbox.length)
chatbox.css('display', 'none');
}
});
}
if(chatbox && chatbox.css('display') == 'none')
chatbox.css('display', '');
// set the tab to active...
var tab = $(this).addClass('imjs-selected').data('state', 'active');
// ...and hide and reset the notification icon
tab.find('.imjs-notification').css('display', 'none')
.data('count', 0);
if(self.settings.storageMethod && self.storageReady &&
chatbox && (username = chatbox.data('username'))) {
$.jStore.store(self.storeKey + 'activeTab', [username]);
}
$(self).trigger('tabToggled', ['activated', tab]);
} else {
var tab = $(this).removeClass('imjs-selected').data('state', 'minimized');
if(chatbox && chatbox.css('display') != 'none')
chatbox.css('display', 'none');
if(self.settings.storageMethod && self.storageReady) {
$.jStore.store(self.storeKey + 'activeTab', ['*']);
}
$(self).trigger('tabToggled', ['minimized', tab]);
}
if(chatbox) {
if(!(input = chatbox.find('.imjs-input')).data('height')) {
// store the height for resizing later
input.data('height', input.height());
}
try {
var msglog = chatbox.find('.imjs-msglog');
msglog[0].scrollTop = msglog[0].scrollHeight;
} catch(e) {}
try { chatbox.find('.imjs-input').focus(); } catch(e) {}
}
},
// === {{{AjaxIM.}}}**{{{bar.closeTab()}}}** ===
//
// Close a tab and hide any related chatbox, such that
// the chatbox can not be reopened without reinitializing
// the tab.
//
// //Note:// {{{this}}}, here, refers to the tab DOM element.
closeTab: function() {
var tab = $(this).parents('.imjs-tab');
tab.css('display', 'none').data('state', 'closed');
if(self.settings.storageMethod && self.storageReady) {
delete self.chatstore[tab.find('.imjs-chatbox').data('username')];
if(self.storageReady) $.jStore.store(self.storeKey + 'chats', self.chatstore);
}
$(self).trigger('tabToggled', ['closed', tab]);
self.bar._scrollers();
return false;
},
// === {{{AjaxIM.}}}**{{{bar.addTab(label, action, closable)}}}** ===
//
// Adds a tab to the tab bar, with the label {{{label}}}. When
// clicked, it will call a callback function, {{{action}}}. If
// {{{action}}} is a string, it is assumed that the string is
// referring to a chatbox ID.
//
// ==== Parameters ====
// * {{{label}}} is the text that will be displayed on the tab.\\
// * {{{action}}} is the callback function, if it is a non-chatbox
// tab, or a string if it //is// a chatbox tab.\\
// * {{{closable}}} is a boolean value that determines whether or not
// it is possible for a user to close this tab.
//
// //Note:// New tabs are given an automatically generated ID
// in the format of {{{#imjs-tab-[md5 of label]}}}.
addTab: function(label, action, closable) {
var tab = $('.imjs-tab.imjs-default').clone().insertAfter('#imjs-scroll-right');
tab.removeClass('imjs-default')
.attr('id', 'imjs-tab-' + $.md5(label))
.html(tab.html().replace('{label}', label))
.data('state', 'minimized');
var notification = tab.find('.imjs-notification');
notification.css('display', 'none')
.data('count', 0)
.data('default-text', notification.html())
.html(notification.html().replace('{count}', '0'));
if(closable === false)
tab.find('.imjs-close').eq(0).remove();
if(typeof action == 'string') {
//tab.data('chatbox', action);
} else {
tab.find('.imjs-chatbox').remove();
tab.click(action);
}
return tab;
},
// === {{{AjaxIM.}}}**{{{bar.notification(tab)}}}** ===
//
// Displays a notification on a tab. Generally, this is called when
// a tab is minimized to let the user know that there is an update
// for them. The way the notification is displayed depends on the
// theme CSS.
//
// ==== Parameters ====
// * {{{tab}}} is the jQuery-selected tab DOM element.
notification: function(tab) {
var notify = tab.find('.imjs-notification');
var notify_count = notify.data('count') + 1;
notify.data('count', notify_count)
.html(notify.data('default-text').replace('{count}', notify_count))
.css('display', '');
},
// === //private// {{{AjaxIM.}}}**{{{bar._scrollers()}}}** ===
//
// Document me!
_scrollers: function() {
var needScrollers = false;
$('.imjs-tab').filter(function() {
return $(this).data('state') != 'closed'
}).css('display', '');
$.each(self.chats, function(username, chatbox) {
var tab = chatbox.data('tab');
if(tab.data('state') == 'closed') return true;
if(tab.position().top > $('#imjs-bar').height()) {
$('.imjs-scroll').css('display', '');
tab.css('display', 'none');
needScrollers = true;
} else {
tab.css('display', '');
}
});
if(!needScrollers) {
$('.imjs-scroll').css('display', 'none');
}
if($('#imjs-scroll-left').css('display') != 'none' &&
$('#imjs-scroll-left').position().top > $('#imjs-bar').height()) {
$('#imjs-bar li.imjs-tab:visible').slice(-1).css('display', 'none');
}
var hiddenLeft = $('#imjs-bar li.imjs-tab:visible').slice(-1)
.nextAll('#imjs-bar li.imjs-tab:hidden')
.not('.imjs-default')
.filter(function() {
return $(this).data('state') != 'closed'
}).length;
var hiddenRight = $('#imjs-bar li.imjs-tab:visible').eq(0)
.prevAll('#imjs-bar li.imjs-tab:hidden')
.not('.imjs-default')
.filter(function() {
return $(this).data('state') != 'closed'
}).length;
$('#imjs-scroll-left').html(hiddenLeft);
$('#imjs-scroll-right').html(hiddenRight);
}
},
// == Comet ==
//
// Comet, or HTTP streaming, holds open a connection between the client and
// the server indefinitely. As the server receives new messages or events,
// they are passed down to the client in a {{{<script>}}}
// tag which calls the {{{AjaxIM.incoming()}}} function. The connection is
// opened using either an {{{iframe}}} (in Opera or Internet Explorer) or
// an {{{XMLHTTPRequest}}} object. Due to the extra flexibility necessary
// with the {{{XMLHTTPRequest}}} object, jQuery's {{{$.ajax}}} is not used.
comet: {
type: '',
obj: null,
onbeforeunload: null,
onreadystatechange: null,
iframe: function() {
if(/loaded|complete/i.test(this.obj.readyState))
throw new Error("IM server not available!");
},
// === {{{AjaxIM.}}}**{{{comet.connect()}}}** ===
//
// Creates and initializes the object and connection between the server
// and the client. For Internet Explorer and Opera, we use an
// {{{<iframe>}}} element; for all other browsers, we create an
// {{{XMLHTTPRequest}}} object. The server connects to the URI defined
// as the "poll" action. This function is called automatically, when
// the IM engine is initialized and the {{{AjaxIM.poll()}}} function
// is called.
connect: function() {
var self = _instance;
if($.browser.opera || $.browser.msie) {
var iframe = $('');
with(iframe) {
css({
position: 'absolute',
visibility: 'visible',
display: 'block',
left: '-10000px',
top: '-10000px',
width: '1px',
height: '1px'
});
attr('src', self.actions.poll[1]);
appendTo('body');
bind('readystatechange',
self.comet.onreadystatechange = function() { self.comet.iframe() });
bind('beforeunload',
self.comet.onbeforeunload = function() { self.comet.disconnect() });
}
self.comet.type = 'iframe';
self.comet.obj = iframe;
} else {
var xhr = new XMLHttpRequest;
var length = 1029;
var code = /^\s*
+
+
+
+
+