[MERGE] model-backed datasets
bzr revid: xmo@openerp.com-20120419081433-6tmpdfum79lixxah
This commit is contained in:
commit
cafb491afc
|
@ -405,14 +405,14 @@ def session_context(request, storage_path, session_cookie='sessionid'):
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
addons_module = {}
|
addons_module = {}
|
||||||
addons_manifest = {}
|
addons_manifest = {}
|
||||||
controllers_class = {}
|
controllers_class = []
|
||||||
controllers_object = {}
|
controllers_object = {}
|
||||||
controllers_path = {}
|
controllers_path = {}
|
||||||
|
|
||||||
class ControllerType(type):
|
class ControllerType(type):
|
||||||
def __init__(cls, name, bases, attrs):
|
def __init__(cls, name, bases, attrs):
|
||||||
super(ControllerType, cls).__init__(name, bases, attrs)
|
super(ControllerType, cls).__init__(name, bases, attrs)
|
||||||
controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
|
controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
|
||||||
|
|
||||||
class Controller(object):
|
class Controller(object):
|
||||||
__metaclass__ = ControllerType
|
__metaclass__ = ControllerType
|
||||||
|
@ -440,12 +440,12 @@ class Root(object):
|
||||||
self.root = '/web/webclient/home'
|
self.root = '/web/webclient/home'
|
||||||
self.config = options
|
self.config = options
|
||||||
|
|
||||||
if self.config.backend == 'local':
|
if not hasattr(self.config, 'connector'):
|
||||||
conn = LocalConnector()
|
if self.config.backend == 'local':
|
||||||
else:
|
self.config.connector = LocalConnector()
|
||||||
conn = openerplib.get_connector(hostname=self.config.server_host,
|
else:
|
||||||
port=self.config.server_port)
|
self.config.connector = openerplib.get_connector(
|
||||||
self.config.connector = conn
|
hostname=self.config.server_host, port=self.config.server_port)
|
||||||
|
|
||||||
self.session_cookie = 'sessionid'
|
self.session_cookie = 'sessionid'
|
||||||
self.addons = {}
|
self.addons = {}
|
||||||
|
@ -526,7 +526,7 @@ class Root(object):
|
||||||
addons_module[module] = m
|
addons_module[module] = m
|
||||||
addons_manifest[module] = manifest
|
addons_manifest[module] = manifest
|
||||||
statics['/%s/static' % module] = path_static
|
statics['/%s/static' % module] = path_static
|
||||||
for k, v in controllers_class.items():
|
for k, v in controllers_class:
|
||||||
if k not in controllers_object:
|
if k not in controllers_object:
|
||||||
o = v()
|
o = v()
|
||||||
controllers_object[k] = o
|
controllers_object[k] = o
|
||||||
|
|
|
@ -909,11 +909,6 @@ class Menu(openerpweb.Controller):
|
||||||
class DataSet(openerpweb.Controller):
|
class DataSet(openerpweb.Controller):
|
||||||
_cp_path = "/web/dataset"
|
_cp_path = "/web/dataset"
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def fields(self, req, model):
|
|
||||||
return {'fields': req.session.model(model).fields_get(False,
|
|
||||||
req.session.eval_context(req.context))}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
|
def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
|
||||||
return self.do_search_read(req, model, fields, offset, limit, domain, sort)
|
return self.do_search_read(req, model, fields, offset, limit, domain, sort)
|
||||||
|
@ -949,7 +944,6 @@ class DataSet(openerpweb.Controller):
|
||||||
if fields and fields == ['id']:
|
if fields and fields == ['id']:
|
||||||
# shortcut read if we only want the ids
|
# shortcut read if we only want the ids
|
||||||
return {
|
return {
|
||||||
'ids': ids,
|
|
||||||
'length': length,
|
'length': length,
|
||||||
'records': [{'id': id} for id in ids]
|
'records': [{'id': id} for id in ids]
|
||||||
}
|
}
|
||||||
|
@ -957,46 +951,10 @@ class DataSet(openerpweb.Controller):
|
||||||
records = Model.read(ids, fields or False, context)
|
records = Model.read(ids, fields or False, context)
|
||||||
records.sort(key=lambda obj: ids.index(obj['id']))
|
records.sort(key=lambda obj: ids.index(obj['id']))
|
||||||
return {
|
return {
|
||||||
'ids': ids,
|
|
||||||
'length': length,
|
'length': length,
|
||||||
'records': records
|
'records': records
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def read(self, req, model, ids, fields=False):
|
|
||||||
return self.do_search_read(req, model, ids, fields)
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def get(self, req, model, ids, fields=False):
|
|
||||||
return self.do_get(req, model, ids, fields)
|
|
||||||
|
|
||||||
def do_get(self, req, model, ids, fields=False):
|
|
||||||
""" Fetches and returns the records of the model ``model`` whose ids
|
|
||||||
are in ``ids``.
|
|
||||||
|
|
||||||
The results are in the same order as the inputs, but elements may be
|
|
||||||
missing (if there is no record left for the id)
|
|
||||||
|
|
||||||
:param req: the JSON-RPC2 request object
|
|
||||||
:type req: openerpweb.JsonRequest
|
|
||||||
:param model: the model to read from
|
|
||||||
:type model: str
|
|
||||||
:param ids: a list of identifiers
|
|
||||||
:type ids: list
|
|
||||||
:param fields: a list of fields to fetch, ``False`` or empty to fetch
|
|
||||||
all fields in the model
|
|
||||||
:type fields: list | False
|
|
||||||
:returns: a list of records, in the same order as the list of ids
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
Model = req.session.model(model)
|
|
||||||
records = Model.read(ids, fields, req.session.eval_context(req.context))
|
|
||||||
|
|
||||||
record_map = dict((record['id'], record) for record in records)
|
|
||||||
|
|
||||||
return [record_map[id] for id in ids if record_map.get(id)]
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def load(self, req, model, id, fields):
|
def load(self, req, model, id, fields):
|
||||||
m = req.session.model(model)
|
m = req.session.model(model)
|
||||||
|
@ -1006,23 +964,6 @@ class DataSet(openerpweb.Controller):
|
||||||
value = r[0]
|
value = r[0]
|
||||||
return {'value': value}
|
return {'value': value}
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def create(self, req, model, data):
|
|
||||||
m = req.session.model(model)
|
|
||||||
r = m.create(data, req.session.eval_context(req.context))
|
|
||||||
return {'result': r}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def save(self, req, model, id, data):
|
|
||||||
m = req.session.model(model)
|
|
||||||
r = m.write([id], data, req.session.eval_context(req.context))
|
|
||||||
return {'result': r}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def unlink(self, req, model, ids=()):
|
|
||||||
Model = req.session.model(model)
|
|
||||||
return Model.unlink(ids, req.session.eval_context(req.context))
|
|
||||||
|
|
||||||
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
|
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
|
||||||
has_domain = domain_id is not None and domain_id < len(args)
|
has_domain = domain_id is not None and domain_id < len(args)
|
||||||
has_context = context_id is not None and context_id < len(args)
|
has_context = context_id is not None and context_id < len(args)
|
||||||
|
@ -1098,19 +1039,7 @@ class DataSet(openerpweb.Controller):
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
@openerpweb.jsonrequest
|
||||||
def exec_workflow(self, req, model, id, signal):
|
def exec_workflow(self, req, model, id, signal):
|
||||||
r = req.session.exec_workflow(model, id, signal)
|
return req.session.exec_workflow(model, id, signal)
|
||||||
return {'result': r}
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def default_get(self, req, model, fields):
|
|
||||||
Model = req.session.model(model)
|
|
||||||
return Model.default_get(fields, req.session.eval_context(req.context))
|
|
||||||
|
|
||||||
@openerpweb.jsonrequest
|
|
||||||
def name_search(self, req, model, search_str, domain=[], context={}):
|
|
||||||
m = req.session.model(model)
|
|
||||||
r = m.name_search(search_str+'%', domain, '=ilike', context)
|
|
||||||
return {'result': r}
|
|
||||||
|
|
||||||
class DataGroup(openerpweb.Controller):
|
class DataGroup(openerpweb.Controller):
|
||||||
_cp_path = "/web/group"
|
_cp_path = "/web/group"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* QUnit v1.2.0 - A JavaScript Unit Testing Framework
|
* QUnit v1.4.0pre - A JavaScript Unit Testing Framework
|
||||||
*
|
*
|
||||||
* http://docs.jquery.com/QUnit
|
* http://docs.jquery.com/QUnit
|
||||||
*
|
*
|
||||||
* Copyright (c) 2011 John Resig, Jörn Zaefferer
|
* Copyright (c) 2012 John Resig, Jörn Zaefferer
|
||||||
* Dual licensed under the MIT (MIT-LICENSE.txt)
|
* Dual licensed under the MIT (MIT-LICENSE.txt)
|
||||||
* or GPL (GPL-LICENSE.txt) licenses.
|
* or GPL (GPL-LICENSE.txt) licenses.
|
||||||
*/
|
*/
|
||||||
|
@ -54,6 +54,10 @@
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#qunit-header label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
#qunit-banner {
|
#qunit-banner {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
|
@ -223,4 +227,6 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10000px;
|
top: -10000px;
|
||||||
left: -10000px;
|
left: -10000px;
|
||||||
|
width: 1000px;
|
||||||
|
height: 1000px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* QUnit v1.2.0 - A JavaScript Unit Testing Framework
|
* QUnit v1.4.0pre - A JavaScript Unit Testing Framework
|
||||||
*
|
*
|
||||||
* http://docs.jquery.com/QUnit
|
* http://docs.jquery.com/QUnit
|
||||||
*
|
*
|
||||||
* Copyright (c) 2011 John Resig, Jörn Zaefferer
|
* Copyright (c) 2012 John Resig, Jörn Zaefferer
|
||||||
* Dual licensed under the MIT (MIT-LICENSE.txt)
|
* Dual licensed under the MIT (MIT-LICENSE.txt)
|
||||||
* or GPL (GPL-LICENSE.txt) licenses.
|
* or GPL (GPL-LICENSE.txt) licenses.
|
||||||
*/
|
*/
|
||||||
|
@ -13,8 +13,11 @@
|
||||||
var defined = {
|
var defined = {
|
||||||
setTimeout: typeof window.setTimeout !== "undefined",
|
setTimeout: typeof window.setTimeout !== "undefined",
|
||||||
sessionStorage: (function() {
|
sessionStorage: (function() {
|
||||||
|
var x = "qunit-test-string";
|
||||||
try {
|
try {
|
||||||
return !!sessionStorage.getItem;
|
sessionStorage.setItem(x, x);
|
||||||
|
sessionStorage.removeItem(x);
|
||||||
|
return true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -25,11 +28,10 @@ var testId = 0,
|
||||||
toString = Object.prototype.toString,
|
toString = Object.prototype.toString,
|
||||||
hasOwn = Object.prototype.hasOwnProperty;
|
hasOwn = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
|
var Test = function(name, testName, expected, async, callback) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.testName = testName;
|
this.testName = testName;
|
||||||
this.expected = expected;
|
this.expected = expected;
|
||||||
this.testEnvironmentArg = testEnvironmentArg;
|
|
||||||
this.async = async;
|
this.async = async;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
this.assertions = [];
|
this.assertions = [];
|
||||||
|
@ -62,6 +64,10 @@ Test.prototype = {
|
||||||
runLoggingCallbacks( 'moduleStart', QUnit, {
|
runLoggingCallbacks( 'moduleStart', QUnit, {
|
||||||
name: this.module
|
name: this.module
|
||||||
} );
|
} );
|
||||||
|
} else if (config.autorun) {
|
||||||
|
runLoggingCallbacks( 'moduleStart', QUnit, {
|
||||||
|
name: this.module
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
config.current = this;
|
config.current = this;
|
||||||
|
@ -69,9 +75,6 @@ Test.prototype = {
|
||||||
setup: function() {},
|
setup: function() {},
|
||||||
teardown: function() {}
|
teardown: function() {}
|
||||||
}, this.moduleTestEnvironment);
|
}, this.moduleTestEnvironment);
|
||||||
if (this.testEnvironmentArg) {
|
|
||||||
extend(this.testEnvironment, this.testEnvironmentArg);
|
|
||||||
}
|
|
||||||
|
|
||||||
runLoggingCallbacks( 'testStart', QUnit, {
|
runLoggingCallbacks( 'testStart', QUnit, {
|
||||||
name: this.testName,
|
name: this.testName,
|
||||||
|
@ -274,17 +277,12 @@ var QUnit = {
|
||||||
},
|
},
|
||||||
|
|
||||||
test: function(testName, expected, callback, async) {
|
test: function(testName, expected, callback, async) {
|
||||||
var name = '<span class="test-name">' + testName + '</span>', testEnvironmentArg;
|
var name = '<span class="test-name">' + escapeInnerText(testName) + '</span>';
|
||||||
|
|
||||||
if ( arguments.length === 2 ) {
|
if ( arguments.length === 2 ) {
|
||||||
callback = expected;
|
callback = expected;
|
||||||
expected = null;
|
expected = null;
|
||||||
}
|
}
|
||||||
// is 2nd argument a testEnvironment?
|
|
||||||
if ( expected && typeof expected === 'object') {
|
|
||||||
testEnvironmentArg = expected;
|
|
||||||
expected = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( config.currentModule ) {
|
if ( config.currentModule ) {
|
||||||
name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
|
name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
|
||||||
|
@ -294,7 +292,7 @@ var QUnit = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
|
var test = new Test(name, testName, expected, async, callback);
|
||||||
test.module = config.currentModule;
|
test.module = config.currentModule;
|
||||||
test.moduleTestEnvironment = config.currentModuleTestEnviroment;
|
test.moduleTestEnvironment = config.currentModuleTestEnviroment;
|
||||||
test.queue();
|
test.queue();
|
||||||
|
@ -312,6 +310,9 @@ var QUnit = {
|
||||||
* @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
|
* @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
|
||||||
*/
|
*/
|
||||||
ok: function(a, msg) {
|
ok: function(a, msg) {
|
||||||
|
if (!config.current) {
|
||||||
|
throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2));
|
||||||
|
}
|
||||||
a = !!a;
|
a = !!a;
|
||||||
var details = {
|
var details = {
|
||||||
result: a,
|
result: a,
|
||||||
|
@ -447,9 +448,14 @@ var QUnit = {
|
||||||
QUnit.constructor = F;
|
QUnit.constructor = F;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Backwards compatibility, deprecated
|
// deprecated; still export them to window to provide clear error messages
|
||||||
QUnit.equals = QUnit.equal;
|
// next step: remove entirely
|
||||||
QUnit.same = QUnit.deepEqual;
|
QUnit.equals = function() {
|
||||||
|
throw new Error("QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead");
|
||||||
|
};
|
||||||
|
QUnit.same = function() {
|
||||||
|
throw new Error("QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead");
|
||||||
|
};
|
||||||
|
|
||||||
// Maintain internal state
|
// Maintain internal state
|
||||||
var config = {
|
var config = {
|
||||||
|
@ -513,8 +519,7 @@ if ( typeof exports === "undefined" || typeof require === "undefined" ) {
|
||||||
extend(window, QUnit);
|
extend(window, QUnit);
|
||||||
window.QUnit = QUnit;
|
window.QUnit = QUnit;
|
||||||
} else {
|
} else {
|
||||||
extend(exports, QUnit);
|
module.exports = QUnit;
|
||||||
exports.QUnit = QUnit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// define these after exposing globals to keep them in these QUnit namespace only
|
// define these after exposing globals to keep them in these QUnit namespace only
|
||||||
|
@ -536,6 +541,16 @@ extend(QUnit, {
|
||||||
semaphore: 0
|
semaphore: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var qunit = id( "qunit" );
|
||||||
|
if ( qunit ) {
|
||||||
|
qunit.innerHTML =
|
||||||
|
'<h1 id="qunit-header">' + escapeInnerText( document.title ) + '</h1>' +
|
||||||
|
'<h2 id="qunit-banner"></h2>' +
|
||||||
|
'<div id="qunit-testrunner-toolbar"></div>' +
|
||||||
|
'<h2 id="qunit-userAgent"></h2>' +
|
||||||
|
'<ol id="qunit-tests"></ol>';
|
||||||
|
}
|
||||||
|
|
||||||
var tests = id( "qunit-tests" ),
|
var tests = id( "qunit-tests" ),
|
||||||
banner = id( "qunit-banner" ),
|
banner = id( "qunit-banner" ),
|
||||||
result = id( "qunit-testresult" );
|
result = id( "qunit-testresult" );
|
||||||
|
@ -564,15 +579,15 @@ extend(QUnit, {
|
||||||
/**
|
/**
|
||||||
* Resets the test setup. Useful for tests that modify the DOM.
|
* Resets the test setup. Useful for tests that modify the DOM.
|
||||||
*
|
*
|
||||||
* If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
|
* If jQuery is available, uses jQuery's replaceWith(), otherwise use replaceChild
|
||||||
*/
|
*/
|
||||||
reset: function() {
|
reset: function() {
|
||||||
if ( window.jQuery ) {
|
var main = id( 'qunit-fixture' );
|
||||||
jQuery( "#qunit-fixture" ).html( config.fixture );
|
if ( main ) {
|
||||||
} else {
|
if ( window.jQuery ) {
|
||||||
var main = id( 'qunit-fixture' );
|
jQuery( main ).replaceWith( config.fixture.cloneNode(true) );
|
||||||
if ( main ) {
|
} else {
|
||||||
main.innerHTML = config.fixture;
|
main.parentNode.replaceChild(config.fixture.cloneNode(true), main);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -636,6 +651,9 @@ extend(QUnit, {
|
||||||
},
|
},
|
||||||
|
|
||||||
push: function(result, actual, expected, message) {
|
push: function(result, actual, expected, message) {
|
||||||
|
if (!config.current) {
|
||||||
|
throw new Error("assertion outside test context, was " + sourceFromStacktrace());
|
||||||
|
}
|
||||||
var details = {
|
var details = {
|
||||||
result: result,
|
result: result,
|
||||||
message: message,
|
message: message,
|
||||||
|
@ -645,21 +663,22 @@ extend(QUnit, {
|
||||||
|
|
||||||
message = escapeInnerText(message) || (result ? "okay" : "failed");
|
message = escapeInnerText(message) || (result ? "okay" : "failed");
|
||||||
message = '<span class="test-message">' + message + "</span>";
|
message = '<span class="test-message">' + message + "</span>";
|
||||||
expected = escapeInnerText(QUnit.jsDump.parse(expected));
|
var output = message;
|
||||||
actual = escapeInnerText(QUnit.jsDump.parse(actual));
|
|
||||||
var output = message + '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>';
|
|
||||||
if (actual != expected) {
|
|
||||||
output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>';
|
|
||||||
output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>';
|
|
||||||
}
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
expected = escapeInnerText(QUnit.jsDump.parse(expected));
|
||||||
|
actual = escapeInnerText(QUnit.jsDump.parse(actual));
|
||||||
|
output += '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>';
|
||||||
|
if (actual != expected) {
|
||||||
|
output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>';
|
||||||
|
output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>';
|
||||||
|
}
|
||||||
var source = sourceFromStacktrace();
|
var source = sourceFromStacktrace();
|
||||||
if (source) {
|
if (source) {
|
||||||
details.source = source;
|
details.source = source;
|
||||||
output += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeInnerText(source) + '</pre></td></tr>';
|
output += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeInnerText(source) + '</pre></td></tr>';
|
||||||
}
|
}
|
||||||
|
output += "</table>";
|
||||||
}
|
}
|
||||||
output += "</table>";
|
|
||||||
|
|
||||||
runLoggingCallbacks( 'log', QUnit, details );
|
runLoggingCallbacks( 'log', QUnit, details );
|
||||||
|
|
||||||
|
@ -779,7 +798,7 @@ QUnit.load = function() {
|
||||||
|
|
||||||
var main = id('qunit-fixture');
|
var main = id('qunit-fixture');
|
||||||
if ( main ) {
|
if ( main ) {
|
||||||
config.fixture = main.innerHTML;
|
config.fixture = main.cloneNode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.autostart) {
|
if (config.autostart) {
|
||||||
|
@ -847,6 +866,15 @@ function done() {
|
||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear own sessionStorage items if all tests passed
|
||||||
|
if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
|
||||||
|
for (var key in sessionStorage) {
|
||||||
|
if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-") === 0 ) {
|
||||||
|
sessionStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runLoggingCallbacks( 'done', QUnit, {
|
runLoggingCallbacks( 'done', QUnit, {
|
||||||
failed: config.stats.bad,
|
failed: config.stats.bad,
|
||||||
passed: passed,
|
passed: passed,
|
||||||
|
@ -881,16 +909,21 @@ function validTest( name ) {
|
||||||
|
|
||||||
// so far supports only Firefox, Chrome and Opera (buggy)
|
// so far supports only Firefox, Chrome and Opera (buggy)
|
||||||
// could be extended in the future to use something like https://github.com/csnover/TraceKit
|
// could be extended in the future to use something like https://github.com/csnover/TraceKit
|
||||||
function sourceFromStacktrace() {
|
function sourceFromStacktrace(offset) {
|
||||||
|
offset = offset || 3;
|
||||||
try {
|
try {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
} catch ( e ) {
|
} catch ( e ) {
|
||||||
if (e.stacktrace) {
|
if (e.stacktrace) {
|
||||||
// Opera
|
// Opera
|
||||||
return e.stacktrace.split("\n")[6];
|
return e.stacktrace.split("\n")[offset + 3];
|
||||||
} else if (e.stack) {
|
} else if (e.stack) {
|
||||||
// Firefox, Chrome
|
// Firefox, Chrome
|
||||||
return e.stack.split("\n")[4];
|
var stack = e.stack.split("\n");
|
||||||
|
if (/^error$/i.test(stack[0])) {
|
||||||
|
stack.shift();
|
||||||
|
}
|
||||||
|
return stack[offset];
|
||||||
} else if (e.sourceURL) {
|
} else if (e.sourceURL) {
|
||||||
// Safari, PhantomJS
|
// Safari, PhantomJS
|
||||||
// TODO sourceURL points at the 'throw new Error' line above, useless
|
// TODO sourceURL points at the 'throw new Error' line above, useless
|
||||||
|
@ -989,6 +1022,7 @@ function fail(message, exception, callback) {
|
||||||
if ( typeof console !== "undefined" && console.error && console.warn ) {
|
if ( typeof console !== "undefined" && console.error && console.warn ) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
console.error(exception);
|
console.error(exception);
|
||||||
|
console.error(exception.stack);
|
||||||
console.warn(callback.toString());
|
console.warn(callback.toString());
|
||||||
|
|
||||||
} else if ( window.opera && opera.postError ) {
|
} else if ( window.opera && opera.postError ) {
|
||||||
|
@ -1368,9 +1402,9 @@ QUnit.jsDump = (function() {
|
||||||
var ret = [ ];
|
var ret = [ ];
|
||||||
QUnit.jsDump.up();
|
QUnit.jsDump.up();
|
||||||
for ( var key in map ) {
|
for ( var key in map ) {
|
||||||
var val = map[key];
|
var val = map[key];
|
||||||
ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack));
|
ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack));
|
||||||
}
|
}
|
||||||
QUnit.jsDump.down();
|
QUnit.jsDump.down();
|
||||||
return join( '{', ret, '}' );
|
return join( '{', ret, '}' );
|
||||||
},
|
},
|
||||||
|
@ -1594,4 +1628,5 @@ QUnit.diff = (function() {
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
})(this);
|
// get at whatever the global object is, like window in browsers
|
||||||
|
})( (function() {return this}).call() );
|
||||||
|
|
|
@ -1390,6 +1390,7 @@ instance.web.Connection = instance.web.CallbackEnabled.extend( /** @lends instan
|
||||||
// an invalid session or no session at all), refresh session data
|
// an invalid session or no session at all), refresh session data
|
||||||
// (should not change, but just in case...)
|
// (should not change, but just in case...)
|
||||||
_.extend(self, {
|
_.extend(self, {
|
||||||
|
session_id: result.session_id,
|
||||||
db: result.db,
|
db: result.db,
|
||||||
username: result.login,
|
username: result.login,
|
||||||
uid: result.uid,
|
uid: result.uid,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,79 @@
|
||||||
|
openerp.test_support = {
|
||||||
|
setup_connection: function (connection) {
|
||||||
|
var origin = location.protocol+"//"+location.host;
|
||||||
|
_.extend(connection, {
|
||||||
|
origin: origin,
|
||||||
|
prefix: origin,
|
||||||
|
server: origin, // keep chs happy
|
||||||
|
//openerp.web.qweb.default_dict['_s'] = this.origin;
|
||||||
|
rpc_function: connection.rpc_json,
|
||||||
|
session_id: false,
|
||||||
|
uid: false,
|
||||||
|
username: false,
|
||||||
|
user_context: {},
|
||||||
|
db: false,
|
||||||
|
openerp_entreprise: false,
|
||||||
|
// this.module_list = openerp._modules.slice();
|
||||||
|
// this.module_loaded = {};
|
||||||
|
// _(this.module_list).each(function (mod) {
|
||||||
|
// self.module_loaded[mod] = true;
|
||||||
|
// });
|
||||||
|
context: {},
|
||||||
|
shortcuts: [],
|
||||||
|
active_id: null
|
||||||
|
});
|
||||||
|
return connection.session_reload();
|
||||||
|
},
|
||||||
|
module: function (title, tested_core, nonliterals) {
|
||||||
|
var conf = QUnit.config.openerp = {};
|
||||||
|
QUnit.module(title, {
|
||||||
|
setup: function () {
|
||||||
|
QUnit.stop();
|
||||||
|
var oe = conf.openerp = window.openerp.init();
|
||||||
|
window.openerp.web[tested_core](oe);
|
||||||
|
var done = openerp.test_support.setup_connection(oe.connection);
|
||||||
|
if (nonliterals) {
|
||||||
|
done = done.pipe(function () {
|
||||||
|
return oe.connection.rpc('/tests/add_nonliterals', {
|
||||||
|
domains: nonliterals.domains || [],
|
||||||
|
contexts: nonliterals.contexts || []
|
||||||
|
}).then(function (r) {
|
||||||
|
oe.domains = r.domains;
|
||||||
|
oe.contexts = r.contexts;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
done.always(QUnit.start)
|
||||||
|
.then(function () {
|
||||||
|
conf.openerp = oe;
|
||||||
|
}, function (e) {
|
||||||
|
QUnit.test(title, function () {
|
||||||
|
console.error(e);
|
||||||
|
QUnit.ok(false, 'Could not obtain a session:' + e.debug);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
test: function (title, fn) {
|
||||||
|
var conf = QUnit.config.openerp;
|
||||||
|
QUnit.test(title, function () {
|
||||||
|
QUnit.stop();
|
||||||
|
fn(conf.openerp);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
expect: function (promise, fn) {
|
||||||
|
promise.always(QUnit.start)
|
||||||
|
.done(function () { QUnit.ok(false, 'RPC requests should not succeed'); })
|
||||||
|
.fail(function (e) {
|
||||||
|
if (e.code !== 200) {
|
||||||
|
QUnit.equal(e.code, 200, 'Testing connector should raise RPC faults');
|
||||||
|
if (typeof console !== 'undefined' && console.error) {
|
||||||
|
console.error(e.data.debug);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(e.data.fault_code);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
|
@ -2291,6 +2291,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
|
||||||
this.last_search = [];
|
this.last_search = [];
|
||||||
this.floating = false;
|
this.floating = false;
|
||||||
this.inhibit_on_change = false;
|
this.inhibit_on_change = false;
|
||||||
|
this.orderer = new openerp.web.DropMisordered();
|
||||||
},
|
},
|
||||||
start: function() {
|
start: function() {
|
||||||
this._super();
|
this._super();
|
||||||
|
@ -2439,14 +2440,10 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
|
||||||
var search_val = request.term;
|
var search_val = request.term;
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if (this.abort_last) {
|
var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
|
||||||
this.abort_last();
|
|
||||||
delete this.abort_last;
|
|
||||||
}
|
|
||||||
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
|
|
||||||
|
|
||||||
dataset.name_search(search_val, self.build_domain(), 'ilike',
|
this.orderer.add(dataset.name_search(
|
||||||
this.limit + 1, function(data) {
|
search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) {
|
||||||
self.last_search = data;
|
self.last_search = data;
|
||||||
// possible selections for the m2o
|
// possible selections for the m2o
|
||||||
var values = _.map(data, function(x) {
|
var values = _.map(data, function(x) {
|
||||||
|
@ -2485,7 +2482,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
|
||||||
|
|
||||||
response(values);
|
response(values);
|
||||||
});
|
});
|
||||||
this.abort_last = dataset.abort_last;
|
|
||||||
},
|
},
|
||||||
_quick_create: function(name) {
|
_quick_create: function(name) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -2493,15 +2489,15 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
|
||||||
self._search_create_popup("form", undefined, {"default_name": name});
|
self._search_create_popup("form", undefined, {"default_name": name});
|
||||||
};
|
};
|
||||||
if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) {
|
if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) {
|
||||||
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
|
new instance.web.DataSet(this, this.field.relation, self.build_context())
|
||||||
dataset.name_create(name, function(data) {
|
.name_create(name, function(data) {
|
||||||
self.display_value = {};
|
self.display_value = {};
|
||||||
self.display_value["" + data[0]] = data[1];
|
self.display_value["" + data[0]] = data[1];
|
||||||
self.set({value: data[0]});
|
self.set({value: data[0]});
|
||||||
}).fail(function(error, event) {
|
}).fail(function(error, event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
slow_create();
|
slow_create();
|
||||||
});
|
});
|
||||||
} else
|
} else
|
||||||
slow_create();
|
slow_create();
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,8 +2,8 @@ $(document).ready(function () {
|
||||||
var openerp;
|
var openerp;
|
||||||
module('web-class', {
|
module('web-class', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test('Basic class creation', function () {
|
test('Basic class creation', function () {
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
|
|
||||||
module("Class");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
ok(!!nova.Class, "Class does exist");
|
|
||||||
ok(!!nova.Class.extend, "extend does exist");
|
|
||||||
var Claz = nova.Class.extend({
|
|
||||||
test: function() {
|
|
||||||
return "ok";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
equal(new Claz().test(), "ok");
|
|
||||||
var Claz2 = Claz.extend({
|
|
||||||
test: function() {
|
|
||||||
return this._super() + "2";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
equal(new Claz2().test(), "ok2");
|
|
||||||
});
|
|
||||||
|
|
||||||
module("DestroyableMixin");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
var Claz = nova.Class.extend(_.extend({}, nova.DestroyableMixin, {}));
|
|
||||||
var x = new Claz();
|
|
||||||
equal(!!x.isDestroyed(), false);
|
|
||||||
x.destroy();
|
|
||||||
equal(x.isDestroyed(), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
module("ParentedMixin");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
var Claz = nova.Class.extend(_.extend({}, nova.ParentedMixin, {}));
|
|
||||||
var x = new Claz();
|
|
||||||
var y = new Claz();
|
|
||||||
y.setParent(x);
|
|
||||||
equal(y.getParent(), x);
|
|
||||||
equal(x.getChildren()[0], y);
|
|
||||||
x.destroy();
|
|
||||||
equal(y.isDestroyed(), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
module("Events");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
var x = new nova.internal.Events();
|
|
||||||
var tmp = 0;
|
|
||||||
var fct = function() {tmp = 1;};
|
|
||||||
x.on("test", fct);
|
|
||||||
equal(tmp, 0);
|
|
||||||
x.trigger("test");
|
|
||||||
equal(tmp, 1);
|
|
||||||
tmp = 0;
|
|
||||||
x.off("test", fct);
|
|
||||||
x.trigger("test");
|
|
||||||
equal(tmp, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
module("EventDispatcherMixin");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
var Claz = nova.Class.extend(_.extend({}, nova.EventDispatcherMixin, {}));
|
|
||||||
var x = new Claz();
|
|
||||||
var y = new Claz();
|
|
||||||
var tmp = 0;
|
|
||||||
var fct = function() {tmp = 1;};
|
|
||||||
x.on("test", y, fct);
|
|
||||||
equal(tmp, 0);
|
|
||||||
x.trigger("test");
|
|
||||||
equal(tmp, 1);
|
|
||||||
tmp = 0;
|
|
||||||
x.off("test", y, fct);
|
|
||||||
x.trigger("test");
|
|
||||||
equal(tmp, 0);
|
|
||||||
tmp = 0;
|
|
||||||
x.on("test", y, fct);
|
|
||||||
y.destroy();
|
|
||||||
x.trigger("test");
|
|
||||||
equal(tmp, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
module("GetterSetterMixin");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
var Claz = nova.Class.extend(_.extend({}, nova.GetterSetterMixin, {}));
|
|
||||||
var x = new Claz();
|
|
||||||
var y = new Claz();
|
|
||||||
x.set({test: 1});
|
|
||||||
equal(x.get("test"), 1);
|
|
||||||
var tmp = 0;
|
|
||||||
x.on("change:test", y, function(model, options) {
|
|
||||||
tmp = 1;
|
|
||||||
equal(options.oldValue, 1);
|
|
||||||
equal(options.newValue, 2);
|
|
||||||
equal(x.get("test"), 2);
|
|
||||||
equal(model, x);
|
|
||||||
});
|
|
||||||
x.set({test: 2});
|
|
||||||
equal(tmp, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("change event only when changed", function() {
|
|
||||||
var Claz = nova.Class.extend(_.extend({}, nova.GetterSetterMixin, {}));
|
|
||||||
var x = new Claz();
|
|
||||||
var exec1 = false;
|
|
||||||
var exec2 = false;
|
|
||||||
x.on("change:test", null, function() {exec1 = true;});
|
|
||||||
x.on("change", null, function() {exec2 = true;});
|
|
||||||
x.set({"test": 3});
|
|
||||||
equal(exec1, true);
|
|
||||||
equal(exec2, true);
|
|
||||||
exec1 = false;
|
|
||||||
exec2 = false;
|
|
||||||
x.set({"test": 3});
|
|
||||||
equal(exec1, false);
|
|
||||||
equal(exec2, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
module("Widget");
|
|
||||||
|
|
||||||
test("base", function() {
|
|
||||||
var Claz = nova.Widget.extend({
|
|
||||||
renderElement: function() {
|
|
||||||
this.$element.attr("id", "testdiv");
|
|
||||||
this.$element.html("test");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var x = new Claz();
|
|
||||||
x.appendTo($("body"));
|
|
||||||
var $el = $("#testdiv");
|
|
||||||
equal($el.length, 1);
|
|
||||||
equal($el.parents()[0], $("body")[0]);
|
|
||||||
equal($el.html(), "test");
|
|
||||||
|
|
||||||
var y = new Claz(x);
|
|
||||||
equal(y.getParent(), x);
|
|
||||||
|
|
||||||
x.destroy();
|
|
||||||
$el = $("#testdiv");
|
|
||||||
equal($el.length, 0);
|
|
||||||
});
|
|
||||||
|
|
|
@ -3,8 +3,9 @@ $(document).ready(function () {
|
||||||
|
|
||||||
module("eval.contexts", {
|
module("eval.contexts", {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test('context_sequences', function () {
|
test('context_sequences', function () {
|
||||||
|
|
|
@ -2,8 +2,9 @@ $(document).ready(function () {
|
||||||
var openerp;
|
var openerp;
|
||||||
module("form.widget", {
|
module("form.widget", {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init(true);
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
window.openerp.web.chrome(openerp);
|
window.openerp.web.chrome(openerp);
|
||||||
// views loader stuff
|
// views loader stuff
|
||||||
window.openerp.web.data(openerp);
|
window.openerp.web.data(openerp);
|
||||||
|
|
|
@ -3,8 +3,9 @@ $(document).ready(function () {
|
||||||
|
|
||||||
module('server-formats', {
|
module('server-formats', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
window.openerp.web.dates(openerp);
|
window.openerp.web.dates(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -40,8 +41,9 @@ $(document).ready(function () {
|
||||||
|
|
||||||
module('web-formats', {
|
module('web-formats', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
window.openerp.web.dates(openerp);
|
window.openerp.web.dates(openerp);
|
||||||
window.openerp.web.formats(openerp);
|
window.openerp.web.formats(openerp);
|
||||||
}
|
}
|
||||||
|
@ -206,8 +208,9 @@ $(document).ready(function () {
|
||||||
});
|
});
|
||||||
module('custom-date-formats', {
|
module('custom-date-formats', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
window.openerp.web.dates(openerp);
|
window.openerp.web.dates(openerp);
|
||||||
window.openerp.web.formats(openerp);
|
window.openerp.web.formats(openerp);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html style="height: 100%">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<title>OpenERP</title>
|
||||||
|
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
|
||||||
|
<script src="/web/static/lib/qunit/qunit.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<script src="/web/static/lib/underscore/underscore.js" type="text/javascript"></script>
|
||||||
|
<script src="/web/static/lib/underscore/underscore.string.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- jquery -->
|
||||||
|
<script src="/web/static/lib/jquery/jquery-1.6.4.js"></script>
|
||||||
|
<script src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js"></script>
|
||||||
|
<script src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
|
||||||
|
|
||||||
|
<script src="/web/static/lib/datejs/globalization/en-US.js"></script>
|
||||||
|
<script src="/web/static/lib/datejs/core.js"></script>
|
||||||
|
<script src="/web/static/lib/datejs/parser.js"></script>
|
||||||
|
<script src="/web/static/lib/datejs/sugarpak.js"></script>
|
||||||
|
<script src="/web/static/lib/datejs/extras.js"></script>
|
||||||
|
|
||||||
|
<script src="/web/static/lib/qweb/qweb2.js"></script>
|
||||||
|
|
||||||
|
<script src="/web/static/lib/py.js/lib/py.js"></script>
|
||||||
|
|
||||||
|
<script src="/web/static/src/js/boot.js"></script>
|
||||||
|
<script src="/web/static/src/js/corelib.js"></script>
|
||||||
|
<script src="/web/static/src/js/coresetup.js"></script>
|
||||||
|
<script src="/web/static/src/js/dates.js"></script>
|
||||||
|
<script src="/web/static/src/js/formats.js"></script>
|
||||||
|
<script src="/web/static/src/js/chrome.js"></script>
|
||||||
|
<script src="/web/static/src/js/data.js"></script>
|
||||||
|
|
||||||
|
<script src="/web/static/src/js/test_support.js"></script>
|
||||||
|
</head>
|
||||||
|
<body id="oe" class="openerp">
|
||||||
|
<h1 id="qunit-header">
|
||||||
|
OpenERP Web Test Suite: javascript to XML-RPC (excluded)
|
||||||
|
</h1>
|
||||||
|
<h2 id="qunit-banner"></h2>
|
||||||
|
<div id="qunit-testrunner-toolbar"></div>
|
||||||
|
<h2 id="qunit-userAgent"></h2>
|
||||||
|
<ol id="qunit-tests"></ol>
|
||||||
|
<div id="qunit-fixture"></div>
|
||||||
|
<script type="text/javascript" src="/web/static/test/fulltest/dataset.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,241 @@
|
||||||
|
$(document).ready(function () {
|
||||||
|
var t = window.openerp.test_support;
|
||||||
|
function context_(c) {
|
||||||
|
return _.extend({ lang: 'en_US', tz: 'UTC', uid: 87539319 }, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.module('Dataset shortcuts', 'data');
|
||||||
|
t.test('read_index', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet(
|
||||||
|
{session: openerp.connection}, 'some.model');
|
||||||
|
ds.ids = [10, 20, 30, 40, 50];
|
||||||
|
ds.index = 2;
|
||||||
|
t.expect(ds.read_index(['a', 'b', 'c']), function (result) {
|
||||||
|
strictEqual(result.method, 'read');
|
||||||
|
strictEqual(result.model, 'some.model');
|
||||||
|
|
||||||
|
strictEqual(result.args.length, 2);
|
||||||
|
deepEqual(result.args[0], [30]);
|
||||||
|
|
||||||
|
deepEqual(result.kwargs, {
|
||||||
|
context: context_()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('default_get', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet(
|
||||||
|
{session: openerp.connection}, 'some.model', {foo: 'bar'});
|
||||||
|
t.expect(ds.default_get(['a', 'b', 'c']), function (result) {
|
||||||
|
strictEqual(result.method, 'default_get');
|
||||||
|
strictEqual(result.model, 'some.model');
|
||||||
|
|
||||||
|
strictEqual(result.args.length, 1);
|
||||||
|
deepEqual(result.args[0], ['a', 'b', 'c']);
|
||||||
|
|
||||||
|
deepEqual(result.kwargs, {
|
||||||
|
context: context_({foo: 'bar'})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('create', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'some.model');
|
||||||
|
t.expect(ds.create({foo: 1, bar: 2}), function (r) {
|
||||||
|
strictEqual(r.method, 'create');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 1);
|
||||||
|
deepEqual(r.args[0], {foo: 1, bar: 2});
|
||||||
|
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
context: context_()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('write', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.write(42, {foo: 1}), function (r) {
|
||||||
|
strictEqual(r.method, 'write');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 2);
|
||||||
|
deepEqual(r.args[0], [42]);
|
||||||
|
deepEqual(r.args[1], {foo: 1});
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
context: context_()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// FIXME: can't run multiple sessions in the same test(), fucks everything up
|
||||||
|
// t.expect(ds.write(42, {foo: 1}, { context: {lang: 'bob'} }), function (r) {
|
||||||
|
// strictEqual(r.args.length, 3);
|
||||||
|
// strictEqual(r.args[2].lang, 'bob');
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
t.test('unlink', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.unlink([42]), function (r) {
|
||||||
|
strictEqual(r.method, 'unlink');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 1);
|
||||||
|
deepEqual(r.args[0], [42]);
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
context: context_()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('call', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.call('frob', ['a', 'b', 42]), function (r) {
|
||||||
|
strictEqual(r.method, 'frob');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 3);
|
||||||
|
deepEqual(r.args, ['a', 'b', 42]);
|
||||||
|
|
||||||
|
ok(_.isEmpty(r.kwargs));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('name_get', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.name_get([1, 2], null), function (r) {
|
||||||
|
strictEqual(r.method, 'name_get');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 1);
|
||||||
|
deepEqual(r.args[0], [1, 2]);
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
context: context_()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('name_search, name', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.name_search('bob'), function (r) {
|
||||||
|
strictEqual(r.method, 'name_search');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 0);
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
name: 'bob',
|
||||||
|
args: false,
|
||||||
|
operator: 'ilike',
|
||||||
|
context: context_(),
|
||||||
|
limit: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('name_search, domain & operator', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.name_search(0, [['foo', '=', 3]], 'someop'), function (r) {
|
||||||
|
strictEqual(r.method, 'name_search');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 0);
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
name: '',
|
||||||
|
args: [['foo', '=', 3]],
|
||||||
|
operator: 'someop',
|
||||||
|
context: context_(),
|
||||||
|
limit: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('exec_workflow', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSet({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.exec_workflow(42, 'foo'), function (r) {
|
||||||
|
strictEqual(r['service'], 'object');
|
||||||
|
strictEqual(r.method, 'exec_workflow');
|
||||||
|
|
||||||
|
// db, id, password, model, method, id
|
||||||
|
strictEqual(r.args.length, 6);
|
||||||
|
strictEqual(r.args[4], 'foo');
|
||||||
|
strictEqual(r.args[5], 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
t.test('DataSetSearch#read_slice', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSetSearch({session: openerp.connection}, 'mod');
|
||||||
|
t.expect(ds.read_slice(['foo', 'bar'], {
|
||||||
|
domain: [['foo', '>', 42], ['qux', '=', 'grault']],
|
||||||
|
context: {peewee: 'herman'},
|
||||||
|
offset: 160,
|
||||||
|
limit: 80
|
||||||
|
}), function (r) {
|
||||||
|
strictEqual(r.method, 'search');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 5);
|
||||||
|
deepEqual(r.args[0], [['foo', '>', 42], ['qux', '=', 'grault']]);
|
||||||
|
strictEqual(r.args[1], 160);
|
||||||
|
strictEqual(r.args[2], 80);
|
||||||
|
strictEqual(r.args[3], false);
|
||||||
|
strictEqual(r.args[4].peewee, 'herman');
|
||||||
|
|
||||||
|
ok(_.isEmpty(r.kwargs));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('DataSetSearch#read_slice sorted', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSetSearch({session: openerp.connection}, 'mod');
|
||||||
|
ds.sort('foo');
|
||||||
|
ds.sort('foo');
|
||||||
|
ds.sort('bar');
|
||||||
|
t.expect(ds.read_slice(['foo', 'bar'], { }), function (r) {
|
||||||
|
strictEqual(r.method, 'search');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 5);
|
||||||
|
deepEqual(r.args[0], []);
|
||||||
|
strictEqual(r.args[1], 0);
|
||||||
|
strictEqual(r.args[2], false);
|
||||||
|
strictEqual(r.args[3], 'bar ASC, foo DESC');
|
||||||
|
deepEqual(r.args[4], context_());
|
||||||
|
|
||||||
|
ok(_.isEmpty(r.kwargs));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
t.module('Nonliterals', 'data', {
|
||||||
|
domains: [
|
||||||
|
"[('model_id', '=', parent.model)]",
|
||||||
|
"[('product_id','=',product_id)]"
|
||||||
|
],
|
||||||
|
contexts: ['{a: b > c}']
|
||||||
|
});
|
||||||
|
t.test('Dataset', function (openerp) {
|
||||||
|
var ds = new openerp.web.DataSetSearch(
|
||||||
|
{session: openerp.connection}, 'mod');
|
||||||
|
var c = new openerp.web.CompoundContext(
|
||||||
|
{a: 'foo', b: 3, c: 5}, openerp.contexts[0]);
|
||||||
|
t.expect(ds.read_slice(['foo', 'bar'], {
|
||||||
|
context: c
|
||||||
|
}), function (r) {
|
||||||
|
strictEqual(r.method, 'search');
|
||||||
|
|
||||||
|
deepEqual(r.args[4], context_({
|
||||||
|
foo: false,
|
||||||
|
a: 'foo',
|
||||||
|
b: 3,
|
||||||
|
c: 5
|
||||||
|
}));
|
||||||
|
|
||||||
|
ok(_.isEmpty(r.kwargs));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
t.test('name_search', function (openerp) {
|
||||||
|
var eval_context = {
|
||||||
|
active_id: 42,
|
||||||
|
active_ids: [42],
|
||||||
|
active_model: 'mod',
|
||||||
|
parent: {model: 'qux'}
|
||||||
|
};
|
||||||
|
var ds = new openerp.web.DataSet(
|
||||||
|
{session: openerp.connection}, 'mod',
|
||||||
|
new openerp.web.CompoundContext({})
|
||||||
|
.set_eval_context(eval_context));
|
||||||
|
var domain = new openerp.web.CompoundDomain(openerp.domains[0])
|
||||||
|
.set_eval_context(eval_context);
|
||||||
|
t.expect(ds.name_search('foo', domain, 'ilike', 0), function (r) {
|
||||||
|
strictEqual(r.method, 'name_search');
|
||||||
|
|
||||||
|
strictEqual(r.args.length, 0);
|
||||||
|
deepEqual(r.kwargs, {
|
||||||
|
name: 'foo',
|
||||||
|
args: [['model_id', '=', 'qux']],
|
||||||
|
operator: 'ilike',
|
||||||
|
context: context_(),
|
||||||
|
limit: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,7 +10,13 @@ $(document).ready(function () {
|
||||||
};
|
};
|
||||||
module('list-events', {
|
module('list-events', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
|
window.openerp.web.chrome(openerp);
|
||||||
|
// views loader stuff
|
||||||
|
window.openerp.web.data(openerp);
|
||||||
|
window.openerp.web.views(openerp);
|
||||||
window.openerp.web.list(openerp);
|
window.openerp.web.list(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -90,7 +96,13 @@ $(document).ready(function () {
|
||||||
|
|
||||||
module('list-records', {
|
module('list-records', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
|
window.openerp.web.chrome(openerp);
|
||||||
|
// views loader stuff
|
||||||
|
window.openerp.web.data(openerp);
|
||||||
|
window.openerp.web.views(openerp);
|
||||||
window.openerp.web.list(openerp);
|
window.openerp.web.list(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -123,7 +135,13 @@ $(document).ready(function () {
|
||||||
|
|
||||||
module('list-collections-degenerate', {
|
module('list-collections-degenerate', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
|
window.openerp.web.chrome(openerp);
|
||||||
|
// views loader stuff
|
||||||
|
window.openerp.web.data(openerp);
|
||||||
|
window.openerp.web.views(openerp);
|
||||||
window.openerp.web.list(openerp);
|
window.openerp.web.list(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -245,7 +263,13 @@ $(document).ready(function () {
|
||||||
|
|
||||||
module('list-hofs', {
|
module('list-hofs', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init();
|
openerp = window.openerp.init([]);
|
||||||
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
|
window.openerp.web.chrome(openerp);
|
||||||
|
// views loader stuff
|
||||||
|
window.openerp.web.data(openerp);
|
||||||
|
window.openerp.web.views(openerp);
|
||||||
window.openerp.web.list(openerp);
|
window.openerp.web.list(openerp);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
$(document).ready(function () {
|
|
||||||
var openerp,
|
|
||||||
make_form = function (default_values) {
|
|
||||||
var fields = {};
|
|
||||||
_(default_values).each(function (value, name) {
|
|
||||||
fields[name] = value instanceof Function ? value : {
|
|
||||||
get_on_change_value: function () { return value; }
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return _.extend(new openerp.web.FormView(null, {}),
|
|
||||||
{fields: fields});
|
|
||||||
};
|
|
||||||
module("form.onchange", {
|
|
||||||
setup: function () {
|
|
||||||
openerp = window.openerp.init(true);
|
|
||||||
window.openerp.web.core(openerp);
|
|
||||||
window.openerp.web.chrome(openerp);
|
|
||||||
// views loader stuff
|
|
||||||
window.openerp.web.data(openerp);
|
|
||||||
window.openerp.web.views(openerp);
|
|
||||||
window.openerp.web.list(openerp);
|
|
||||||
window.openerp.web.form(openerp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test('Parse args-less onchange', function () {
|
|
||||||
var f = new openerp.web.FormView(null, {});
|
|
||||||
var result = f.parse_on_change('on_change_foo()', {});
|
|
||||||
equal(result.method, 'on_change_foo');
|
|
||||||
deepEqual(result.args, []);
|
|
||||||
});
|
|
||||||
test('Parse 1-arg onchange', function () {
|
|
||||||
var f = make_form({foo: 3});
|
|
||||||
var result = f.parse_on_change('on_change_foo(foo)', {});
|
|
||||||
equal(result.method, 'on_change_foo');
|
|
||||||
deepEqual(result.args, [3]);
|
|
||||||
});
|
|
||||||
test('Parse 2-args onchange', function () {
|
|
||||||
var f = make_form({foo: 3, bar: 'qux'});
|
|
||||||
var result = f.parse_on_change('on_change_foo(bar, foo)', {});
|
|
||||||
equal(result.method, 'on_change_foo');
|
|
||||||
deepEqual(result.args, ['qux', 3]);
|
|
||||||
});
|
|
||||||
test('Literal null', function () {
|
|
||||||
var f = make_form();
|
|
||||||
var result = f.parse_on_change('on_null(None)', {});
|
|
||||||
deepEqual(result.args, [null]);
|
|
||||||
});
|
|
||||||
test('Literal true', function () {
|
|
||||||
var f = make_form();
|
|
||||||
var result = f.parse_on_change('on_null(True)', {});
|
|
||||||
deepEqual(result.args, [true]);
|
|
||||||
});
|
|
||||||
test('Literal false', function () {
|
|
||||||
var f = make_form();
|
|
||||||
var result = f.parse_on_change('on_null(False)', {});
|
|
||||||
deepEqual(result.args, [false]);
|
|
||||||
});
|
|
||||||
test('Literal string', function () {
|
|
||||||
var f = make_form();
|
|
||||||
var result = f.parse_on_change('on_str("foo")', {});
|
|
||||||
deepEqual(result.args, ['foo']);
|
|
||||||
var result2 = f.parse_on_change("on_str('foo')", {});
|
|
||||||
deepEqual(result2.args, ['foo']);
|
|
||||||
});
|
|
||||||
test('Literal number', function () {
|
|
||||||
var f = make_form();
|
|
||||||
var result = f.parse_on_change('on_str(42)', {});
|
|
||||||
deepEqual(result.args, [42]);
|
|
||||||
var result2 = f.parse_on_change("on_str(-25)", {});
|
|
||||||
deepEqual(result2.args, [-25]);
|
|
||||||
var result3 = f.parse_on_change("on_str(25.02)", {});
|
|
||||||
deepEqual(result3.args, [25.02]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,8 +2,8 @@ $(document).ready(function () {
|
||||||
var openerp;
|
var openerp;
|
||||||
module('Registry', {
|
module('Registry', {
|
||||||
setup: function () {
|
setup: function () {
|
||||||
openerp = window.openerp.init(true);
|
openerp = window.openerp.init([]);
|
||||||
window.openerp.web.core(openerp);
|
window.openerp.web.corelib(openerp);
|
||||||
openerp.web.Foo = {};
|
openerp.web.Foo = {};
|
||||||
openerp.web.Bar = {};
|
openerp.web.Bar = {};
|
||||||
openerp.web.Foo2 = {};
|
openerp.web.Foo2 = {};
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
$(document).ready(function () {
|
||||||
|
var openerp;
|
||||||
|
|
||||||
|
module('Misordered resolution management', {
|
||||||
|
setup: function () {
|
||||||
|
openerp = window.openerp.init([]);
|
||||||
|
window.openerp.web.corelib(openerp);
|
||||||
|
window.openerp.web.coresetup(openerp);
|
||||||
|
window.openerp.web.data(openerp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
test('Resolve all correctly ordered, sync', function () {
|
||||||
|
var dm = new openerp.web.DropMisordered(), flag = false;
|
||||||
|
|
||||||
|
var d1 = $.Deferred(), d2 = $.Deferred(),
|
||||||
|
r1 = dm.add(d1), r2 = dm.add(d2);
|
||||||
|
|
||||||
|
$.when(r1, r2).done(function () {
|
||||||
|
flag = true;
|
||||||
|
});
|
||||||
|
d1.resolve();
|
||||||
|
d2.resolve();
|
||||||
|
|
||||||
|
ok(flag);
|
||||||
|
});
|
||||||
|
test("Don't resolve mis-ordered, sync", function () {
|
||||||
|
var dm = new openerp.web.DropMisordered(),
|
||||||
|
done1 = false, done2 = false,
|
||||||
|
fail1 = false, fail2 = false;
|
||||||
|
|
||||||
|
var d1 = $.Deferred(), d2 = $.Deferred();
|
||||||
|
dm.add(d1).then(function () { done1 = true; },
|
||||||
|
function () { fail1 = true; });
|
||||||
|
dm.add(d2).then(function () { done2 = true; },
|
||||||
|
function () { fail2 = true; });
|
||||||
|
|
||||||
|
d2.resolve();
|
||||||
|
d1.resolve();
|
||||||
|
|
||||||
|
// d1 is in limbo
|
||||||
|
ok(!done1);
|
||||||
|
ok(!fail1);
|
||||||
|
// d2 is resolved
|
||||||
|
ok(done2);
|
||||||
|
ok(!fail2);
|
||||||
|
});
|
||||||
|
test('Fail mis-ordered flag, sync', function () {
|
||||||
|
var dm = new openerp.web.DropMisordered(true),
|
||||||
|
done1 = false, done2 = false,
|
||||||
|
fail1 = false, fail2 = false;
|
||||||
|
|
||||||
|
var d1 = $.Deferred(), d2 = $.Deferred();
|
||||||
|
dm.add(d1).then(function () { done1 = true; },
|
||||||
|
function () { fail1 = true; });
|
||||||
|
dm.add(d2).then(function () { done2 = true; },
|
||||||
|
function () { fail2 = true; });
|
||||||
|
|
||||||
|
d2.resolve();
|
||||||
|
d1.resolve();
|
||||||
|
|
||||||
|
// d1 is failed
|
||||||
|
ok(!done1);
|
||||||
|
ok(fail1);
|
||||||
|
// d2 is resolved
|
||||||
|
ok(done2);
|
||||||
|
ok(!fail2);
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest('Resolve all correctly ordered, sync', 1, function () {
|
||||||
|
var dm = new openerp.web.DropMisordered();
|
||||||
|
|
||||||
|
var d1 = $.Deferred(), d2 = $.Deferred(),
|
||||||
|
r1 = dm.add(d1), r2 = dm.add(d2);
|
||||||
|
|
||||||
|
setTimeout(function () { d1.resolve(); }, 100);
|
||||||
|
setTimeout(function () { d2.resolve(); }, 200);
|
||||||
|
|
||||||
|
$.when(r1, r2).done(function () {
|
||||||
|
start();
|
||||||
|
ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
asyncTest("Don't resolve mis-ordered, sync", 4, function () {
|
||||||
|
var dm = new openerp.web.DropMisordered(),
|
||||||
|
done1 = false, done2 = false,
|
||||||
|
fail1 = false, fail2 = false;
|
||||||
|
|
||||||
|
var d1 = $.Deferred(), d2 = $.Deferred();
|
||||||
|
dm.add(d1).then(function () { done1 = true; },
|
||||||
|
function () { fail1 = true; });
|
||||||
|
dm.add(d2).then(function () { done2 = true; },
|
||||||
|
function () { fail2 = true; });
|
||||||
|
|
||||||
|
setTimeout(function () { d1.resolve(); }, 200);
|
||||||
|
setTimeout(function () { d2.resolve(); }, 100);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
start();
|
||||||
|
// d1 is in limbo
|
||||||
|
ok(!done1);
|
||||||
|
ok(!fail1);
|
||||||
|
// d2 is resolved
|
||||||
|
ok(done2);
|
||||||
|
ok(!fail2);
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
asyncTest('Fail mis-ordered flag, sync', 4, function () {
|
||||||
|
var dm = new openerp.web.DropMisordered(true),
|
||||||
|
done1 = false, done2 = false,
|
||||||
|
fail1 = false, fail2 = false;
|
||||||
|
|
||||||
|
var d1 = $.Deferred(), d2 = $.Deferred();
|
||||||
|
dm.add(d1).then(function () { done1 = true; },
|
||||||
|
function () { fail1 = true; });
|
||||||
|
dm.add(d2).then(function () { done2 = true; },
|
||||||
|
function () { fail2 = true; });
|
||||||
|
|
||||||
|
setTimeout(function () { d1.resolve(); }, 200);
|
||||||
|
setTimeout(function () { d2.resolve(); }, 100);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
start();
|
||||||
|
// d1 is failed
|
||||||
|
ok(!done1);
|
||||||
|
ok(fail1);
|
||||||
|
// d2 is resolved
|
||||||
|
ok(done2);
|
||||||
|
ok(!fail2);
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
});
|
|
@ -26,10 +26,9 @@
|
||||||
|
|
||||||
<script src="/web/static/lib/py.js/lib/py.js"></script>
|
<script src="/web/static/lib/py.js/lib/py.js"></script>
|
||||||
|
|
||||||
<script src="/web/static/lib/novajs/src/nova.js"></script>
|
|
||||||
|
|
||||||
<script src="/web/static/src/js/boot.js"></script>
|
<script src="/web/static/src/js/boot.js"></script>
|
||||||
<script src="/web/static/src/js/core.js"></script>
|
<script src="/web/static/src/js/corelib.js"></script>
|
||||||
|
<script src="/web/static/src/js/coresetup.js"></script>
|
||||||
<script src="/web/static/src/js/dates.js"></script>
|
<script src="/web/static/src/js/dates.js"></script>
|
||||||
<script src="/web/static/src/js/formats.js"></script>
|
<script src="/web/static/src/js/formats.js"></script>
|
||||||
<script src="/web/static/src/js/chrome.js"></script>
|
<script src="/web/static/src/js/chrome.js"></script>
|
||||||
|
@ -52,6 +51,6 @@
|
||||||
<script type="text/javascript" src="/web/static/test/form.js"></script>
|
<script type="text/javascript" src="/web/static/test/form.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/test/list-utils.js"></script>
|
<script type="text/javascript" src="/web/static/test/list-utils.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/test/formats.js"></script>
|
<script type="text/javascript" src="/web/static/test/formats.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/test/onchange.js"></script>
|
<script type="text/javascript" src="/web/static/test/rpc.js"></script>
|
||||||
<script type="text/javascript" src="/web/static/test/evals.js"></script>
|
<script type="text/javascript" src="/web/static/test/evals.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import xmlrpclib
|
||||||
|
from ..common.openerplib.main import Connector
|
||||||
|
|
||||||
|
execute_map = {}
|
||||||
|
|
||||||
|
class TestConnector(Connector):
|
||||||
|
def db_list_lang(self):
|
||||||
|
return [('en_US', u'Test Language')]
|
||||||
|
|
||||||
|
def common_authenticate(self, db, login, password, environment):
|
||||||
|
return 87539319
|
||||||
|
|
||||||
|
def common_login(self, db, login, password):
|
||||||
|
return self.common_authenticate(db, login, password, {})
|
||||||
|
|
||||||
|
def object_execute_kw(self, db, uid, password, model, method, args, kwargs):
|
||||||
|
if model in execute_map and hasattr(execute_map[model], method):
|
||||||
|
return getattr(execute_map[model], method)(*args, **kwargs)
|
||||||
|
|
||||||
|
raise xmlrpclib.Fault({
|
||||||
|
'model': model,
|
||||||
|
'method': method,
|
||||||
|
'args': args,
|
||||||
|
'kwargs': kwargs
|
||||||
|
}, '')
|
||||||
|
|
||||||
|
def send(self, service_name, method, *args):
|
||||||
|
method_name = '%s_%s' % (service_name, method)
|
||||||
|
if hasattr(self, method_name):
|
||||||
|
return getattr(self, method_name)(*args)
|
||||||
|
|
||||||
|
raise xmlrpclib.Fault({
|
||||||
|
'service': service_name,
|
||||||
|
'method': method,
|
||||||
|
'args': args
|
||||||
|
}, '')
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from ..common import http, nonliterals
|
||||||
|
from ..controllers.main import Session
|
||||||
|
|
||||||
|
UID = 87539319
|
||||||
|
DB = 'test_db'
|
||||||
|
LOGIN = 'test_login'
|
||||||
|
PASSWORD = 'test_password'
|
||||||
|
CONTEXT = {'lang': 'en_US', 'tz': 'UTC', 'uid': UID}
|
||||||
|
|
||||||
|
def bind(session):
|
||||||
|
session.bind(DB, UID, LOGIN, PASSWORD)
|
||||||
|
session.context = CONTEXT
|
||||||
|
session.build_connection().set_login_info(DB, LOGIN, PASSWORD, UID)
|
||||||
|
|
||||||
|
class TestController(http.Controller):
|
||||||
|
_cp_path = '/tests'
|
||||||
|
|
||||||
|
@http.jsonrequest
|
||||||
|
def add_nonliterals(self, req, domains, contexts):
|
||||||
|
return {
|
||||||
|
'domains': [nonliterals.Domain(req.session, domain)
|
||||||
|
for domain in domains],
|
||||||
|
'contexts': [nonliterals.Context(req.session, context)
|
||||||
|
for context in contexts]
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestSession(Session):
|
||||||
|
_cp_path = '/web/session'
|
||||||
|
|
||||||
|
def session_info(self, req):
|
||||||
|
if not req.session._uid:
|
||||||
|
bind(req.session)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"session_id": req.session_id,
|
||||||
|
"uid": req.session._uid,
|
||||||
|
"context": CONTEXT,
|
||||||
|
"db": req.session._db,
|
||||||
|
"login": req.session._login,
|
||||||
|
"openerp_entreprise": False,
|
||||||
|
}
|
|
@ -107,7 +107,7 @@ openerp.web_process = function (instance) {
|
||||||
if(this.process_id)
|
if(this.process_id)
|
||||||
return def.resolve().promise();
|
return def.resolve().promise();
|
||||||
|
|
||||||
this.process_dataset = new instance.web.DataSetStatic(this, "process.process", this.session.context);
|
this.process_dataset = new instance.web.DataSet(this, "process.process", this.session.context);
|
||||||
this.process_dataset
|
this.process_dataset
|
||||||
.call("search_by_model", [self.process_model,self.session.context])
|
.call("search_by_model", [self.process_model,self.session.context])
|
||||||
.done(function(res) {
|
.done(function(res) {
|
||||||
|
@ -237,7 +237,7 @@ openerp.web_process = function (instance) {
|
||||||
},
|
},
|
||||||
jump_to_view: function(model, id) {
|
jump_to_view: function(model, id) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var dataset = new instance.web.DataSetStatic(this, 'ir.values', this.session.context);
|
var dataset = new instance.web.DataSet(this, 'ir.values', this.session.context);
|
||||||
dataset.call('get',
|
dataset.call('get',
|
||||||
['action', 'tree_but_open',[['ir.ui.menu', id]], dataset.context],
|
['action', 'tree_but_open',[['ir.ui.menu', id]], dataset.context],
|
||||||
function(res) {
|
function(res) {
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
Don't stop the world now: asynchronous development and Javascript
|
||||||
|
=================================================================
|
||||||
|
|
||||||
|
As a language (and runtime), javascript is fundamentally
|
||||||
|
single-threaded. This means any blocking request or computation will
|
||||||
|
blocks the whole page (and, in older browsers, the software itself
|
||||||
|
even preventing users from switching to an other tab): a javascript
|
||||||
|
environment can be seen as an event-based runloop where application
|
||||||
|
developers have no control over the runloop itself.
|
||||||
|
|
||||||
|
As a result, performing long-running synchronous network requests or
|
||||||
|
other types of complex and expensive accesses is frowned upon and
|
||||||
|
asynchronous APIs are used instead.
|
||||||
|
|
||||||
|
Asynchronous code rarely comes naturally, especially for developers
|
||||||
|
used to synchronous server-side code (in Python, Java or C#) where the
|
||||||
|
code will just block until the deed is gone. This is increased further
|
||||||
|
when asynchronous programming is not a first-class concept and is
|
||||||
|
instead implemented on top of callbacks-based programming, which is
|
||||||
|
the case in javascript.
|
||||||
|
|
||||||
|
The goal of this guide is to provide some tools to deal with
|
||||||
|
asynchronous systems, and warn against systematic issues or dangers.
|
||||||
|
|
||||||
|
Deferreds
|
||||||
|
---------
|
||||||
|
|
||||||
|
Deferreds are a form of `promises`_. OpenERP Web currently uses
|
||||||
|
`jQuery's deferred`_, but any `CommonJS Promises/A`_ implementation
|
||||||
|
should work.
|
||||||
|
|
||||||
|
The core idea of deferreds is that potentially asynchronous methods
|
||||||
|
will return a :js:class:`Deferred` object instead of an arbitrary
|
||||||
|
value or (most commonly) nothing.
|
||||||
|
|
||||||
|
This object can then be used to track the end of the asynchronous
|
||||||
|
operation by adding callbacks onto it, either success callbacks or
|
||||||
|
error callbacks.
|
||||||
|
|
||||||
|
A great advantage of deferreds over simply passing callback functions
|
||||||
|
directly to asynchronous methods is the ability to :ref:`compose them
|
||||||
|
<deferred-composition>`.
|
||||||
|
|
||||||
|
Using deferreds
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
`CommonJS Promises/A`_ deferreds have only one method of importance:
|
||||||
|
:js:func:`Deferred.then`. This method is used to attach new callbacks
|
||||||
|
to the deferred object.
|
||||||
|
|
||||||
|
* the first parameter attaches a success callback, called when the
|
||||||
|
deferred object is successfully resolved and provided with the
|
||||||
|
resolved value(s) for the asynchronous operation.
|
||||||
|
|
||||||
|
* the second parameter attaches a failure callback, called when the
|
||||||
|
deferred object is rejected and provided with rejection values
|
||||||
|
(often some sort of error message).
|
||||||
|
|
||||||
|
Callbacks attached to deferreds are never "lost": if a callback is
|
||||||
|
attached to an already resolved or rejected deferred, the callback
|
||||||
|
will be called (or ignored) immediately. A deferred is also only ever
|
||||||
|
resolved or rejected once, and is either resolved or rejected: a given
|
||||||
|
deferred can not call a single success callback twice, or call both a
|
||||||
|
success and a failure callbacks.
|
||||||
|
|
||||||
|
:js:func:`~Deferred.then` should be the method you'll use most often
|
||||||
|
when interacting with deferred objects (and thus asynchronous APIs).
|
||||||
|
|
||||||
|
Building deferreds
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
After using asynchronous APIs may come the time to build them: for
|
||||||
|
`mocks`_, to compose deferreds from multiple source in a complex
|
||||||
|
manner, in order to let the current operations repaint the screen or
|
||||||
|
give other events the time to unfold, ...
|
||||||
|
|
||||||
|
This is easy using jQuery's deferred objects.
|
||||||
|
|
||||||
|
.. note:: this section is an implementation detail of jQuery Deferred
|
||||||
|
objects, the creation of promises is not part of any
|
||||||
|
standard (even tentative) that I know of. If you are using
|
||||||
|
deferred objects which are not jQuery's, their API may (and
|
||||||
|
often will) be completely different.
|
||||||
|
|
||||||
|
Deferreds are created by invoking their constructor [#]_ without any
|
||||||
|
argument. This creates a :js:class:`Deferred` instance object with the
|
||||||
|
following methods:
|
||||||
|
|
||||||
|
:js:func:`Deferred.resolve`
|
||||||
|
|
||||||
|
As its name indicates, this method moves the deferred to the
|
||||||
|
"Resolved" state. It can be provided as many arguments as
|
||||||
|
necessary, these arguments will be provided to any pending success
|
||||||
|
callback.
|
||||||
|
|
||||||
|
:js:func:`Deferred.reject`
|
||||||
|
|
||||||
|
Similar to :js:func:`~Deferred.resolve`, but moves the deferred to
|
||||||
|
the "Rejected" state and calls pending failure handlers.
|
||||||
|
|
||||||
|
:js:func:`Deferred.promise`
|
||||||
|
|
||||||
|
Creates a readonly view of the deferred object. It is generally a
|
||||||
|
good idea to return a promise view of the deferred to prevent
|
||||||
|
callers from resolving or rejecting the deferred in your stead.
|
||||||
|
|
||||||
|
:js:func:`~Deferred.reject` and :js:func:`~Deferred.resolve` are used
|
||||||
|
to inform callers that the asynchronous operation has failed (or
|
||||||
|
succeeded). These methods should simply be called when the
|
||||||
|
asynchronous operation has ended, to notify anybody interested in its
|
||||||
|
result(s).
|
||||||
|
|
||||||
|
.. _deferred-composition:
|
||||||
|
|
||||||
|
Composing deferreds
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
What we've seen so far is pretty nice, but mostly doable by passing
|
||||||
|
functions to other functions (well adding functions post-facto would
|
||||||
|
probably be a chore... still, doable).
|
||||||
|
|
||||||
|
Deferreds truly shine when code needs to compose asynchronous
|
||||||
|
operations in some way or other, as they can be used as a basis for
|
||||||
|
such composition.
|
||||||
|
|
||||||
|
There are two main forms of compositions over deferred: multiplexing
|
||||||
|
and piping/cascading.
|
||||||
|
|
||||||
|
Deferred multiplexing
|
||||||
|
`````````````````````
|
||||||
|
|
||||||
|
The most common reason for multiplexing deferred is simply performing
|
||||||
|
2+ asynchronous operations and wanting to wait until all of them are
|
||||||
|
done before moving on (and executing more stuff).
|
||||||
|
|
||||||
|
The jQuery multiplexing function for promises is :js:func:`when`.
|
||||||
|
|
||||||
|
.. note:: the multiplexing behavior of jQuery's :js:func:`when` is an
|
||||||
|
(incompatible, mostly) extension of the behavior defined in
|
||||||
|
`CommonJS Promises/B`_.
|
||||||
|
|
||||||
|
This function can take any number of promises [#]_ and will return a
|
||||||
|
promise.
|
||||||
|
|
||||||
|
This returned promise will be resolved when *all* multiplexed promises
|
||||||
|
are resolved, and will be rejected as soon as one of the multiplexed
|
||||||
|
promises is rejected (it behaves like Python's ``all()``, but with
|
||||||
|
promise objects instead of boolean-ish).
|
||||||
|
|
||||||
|
The resolved values of the various promises multiplexed via
|
||||||
|
:js:func:`when` are mapped to the arguments of :js:func:`when`'s
|
||||||
|
success callback, if they are needed. The resolved values of a promise
|
||||||
|
are at the same index in the callback's arguments as the promise in
|
||||||
|
the :js:func:`when` call so you will have:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
$.when(p0, p1, p2, p3).then(
|
||||||
|
function (results0, results1, results2, results3) {
|
||||||
|
// code
|
||||||
|
});
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
in a normal mapping, each parameter to the callback would be an
|
||||||
|
array: each promise is conceptually resolved with an array of 0..n
|
||||||
|
values and these values are passed to :js:func:`when`'s
|
||||||
|
callback. But jQuery treats deferreds resolving a single value
|
||||||
|
specially, and "unwraps" that value.
|
||||||
|
|
||||||
|
For instance, in the code block above if the index of each promise
|
||||||
|
is the number of values it resolves (0 to 3), ``results0`` is an
|
||||||
|
empty array, ``results2`` is an array of 2 elements (a pair) but
|
||||||
|
``results1`` is the actual value resolved by ``p1``, not an array.
|
||||||
|
|
||||||
|
Deferred chaining
|
||||||
|
`````````````````
|
||||||
|
|
||||||
|
A second useful composition is starting an asynchronous operation as
|
||||||
|
the result of an other asynchronous operation, and wanting the result
|
||||||
|
of both: :js:func:`Deferred.then` returns the deferred on which it was
|
||||||
|
called, so handle e.g. OpenERP's search/read sequence with this would
|
||||||
|
require something along the lines of:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
var result = $.Deferred();
|
||||||
|
Model.search(condition).then(function (ids) {
|
||||||
|
Model.read(ids, fields).then(function (records) {
|
||||||
|
result.resolve(records);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result.promise();
|
||||||
|
|
||||||
|
While it doesn't look too bad for trivial code, this quickly gets
|
||||||
|
unwieldy.
|
||||||
|
|
||||||
|
Instead, jQuery provides a tool to handle this kind of chains:
|
||||||
|
:js:func:`Deferred.pipe`.
|
||||||
|
|
||||||
|
:js:func:`~Deferred.pipe` has the same signature as
|
||||||
|
:js:func:`~Deferred.then` and could be used in the same manner
|
||||||
|
provided its return value was not used.
|
||||||
|
|
||||||
|
It differs from :js:func:`~Deferred.then` in two ways: it returns a
|
||||||
|
new promise object, not the one it was called with, and the return
|
||||||
|
values of the callbacks is actually important to it: whichever
|
||||||
|
callback is called,
|
||||||
|
|
||||||
|
* If the callback is not set (not provided or left to null), the
|
||||||
|
resolution or rejection value(s) is simply forwarded to
|
||||||
|
:js:func:`~Deferred.pipe`'s promise (it's essentially a noop)
|
||||||
|
|
||||||
|
* If the callback is set and does not return an observable object (a
|
||||||
|
deferred or a promise), the value it returns (``undefined`` if it
|
||||||
|
does not return anything) will replace the value it was given, e.g.
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
promise.pipe(function () {
|
||||||
|
console.log('called');
|
||||||
|
});
|
||||||
|
|
||||||
|
will resolve with the sole value ``undefined``.
|
||||||
|
|
||||||
|
* If the callback is set and returns an observable object, that object
|
||||||
|
will be the actual resolution (and result) of the pipe. This means a
|
||||||
|
resolved promise from the failure callback will resolve the pipe,
|
||||||
|
and a failure promise from the success callback will reject the
|
||||||
|
pipe.
|
||||||
|
|
||||||
|
This provides an easy way to chain operation successes, and the
|
||||||
|
previous piece of code can now be rewritten:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
return Model.search(condition).pipe(function (ids) {
|
||||||
|
return Model.read(ids, fields);
|
||||||
|
});
|
||||||
|
|
||||||
|
the result of the whole expression will encode failure if either
|
||||||
|
``search`` or ``read`` fails (with the right rejection values), and
|
||||||
|
will be resolved with ``read``'s resolution values if the chain
|
||||||
|
executes correctly.
|
||||||
|
|
||||||
|
:js:func:`~Deferred.pipe` is also useful to adapt third-party
|
||||||
|
promise-based APIs, in order to filter their resolution value counts
|
||||||
|
for instance (to take advantage of :js:func:`when` 's special treatment
|
||||||
|
of single-value promises).
|
||||||
|
|
||||||
|
jQuery.Deferred API
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. js:function:: when(deferreds…)
|
||||||
|
|
||||||
|
:param deferreds: deferred objects to multiplex
|
||||||
|
:returns: a multiplexed deferred
|
||||||
|
:rtype: :js:class:`Deferred`
|
||||||
|
|
||||||
|
.. js:class:: Deferred
|
||||||
|
|
||||||
|
.. js:function:: Deferred.then(doneCallback[, failCallback])
|
||||||
|
|
||||||
|
Attaches new callbacks to the resolution or rejection of the
|
||||||
|
deferred object. Callbacks are executed in the order they are
|
||||||
|
attached to the deferred.
|
||||||
|
|
||||||
|
To provide only a failure callback, pass ``null`` as the
|
||||||
|
``doneCallback``, to provide only a success callback the
|
||||||
|
second argument can just be ignored (and not passed at all).
|
||||||
|
|
||||||
|
:param doneCallback: function called when the deferred is resolved
|
||||||
|
:type doneCallback: Function
|
||||||
|
:param failCallback: function called when the deferred is rejected
|
||||||
|
:type failCallback: Function
|
||||||
|
:returns: the deferred object on which it was called
|
||||||
|
:rtype: :js:class:`Deferred`
|
||||||
|
|
||||||
|
.. js:function:: Deferred.done(doneCallback)
|
||||||
|
|
||||||
|
Attaches a new success callback to the deferred, shortcut for
|
||||||
|
``deferred.then(doneCallback)``.
|
||||||
|
|
||||||
|
This is a jQuery extension to `CommonJS Promises/A`_ providing
|
||||||
|
little value over calling :js:func:`~Deferred.then` directly,
|
||||||
|
it should be avoided.
|
||||||
|
|
||||||
|
:param doneCallback: function called when the deferred is resolved
|
||||||
|
:type doneCallback: Function
|
||||||
|
:returns: the deferred object on which it was called
|
||||||
|
:rtype: :js:class:`Deferred`
|
||||||
|
|
||||||
|
.. js:function:: Deferred.fail(failCallback)
|
||||||
|
|
||||||
|
Attaches a new failure callback to the deferred, shortcut for
|
||||||
|
``deferred.then(null, failCallback)``.
|
||||||
|
|
||||||
|
A second jQuery extension to `Promises/A <CommonJS
|
||||||
|
Promises/A>`_. Although it provides more value than
|
||||||
|
:js:func:`~Deferred.done`, it still is not much and should be
|
||||||
|
avoided as well.
|
||||||
|
|
||||||
|
:param failCallback: function called when the deferred is rejected
|
||||||
|
:type failCallback: Function
|
||||||
|
:returns: the deferred object on which it was called
|
||||||
|
:rtype: :js:class:`Deferred`
|
||||||
|
|
||||||
|
.. js:function:: Deferred.promise()
|
||||||
|
|
||||||
|
Returns a read-only view of the deferred object, with all
|
||||||
|
mutators (resolve and reject) methods removed.
|
||||||
|
|
||||||
|
.. js:function:: Deferred.resolve(value…)
|
||||||
|
|
||||||
|
Called to resolve a deferred, any value provided will be
|
||||||
|
passed onto the success handlers of the deferred object.
|
||||||
|
|
||||||
|
Resolving a deferred which has already been resolved or
|
||||||
|
rejected has no effect.
|
||||||
|
|
||||||
|
.. js:function:: Deferred.reject(value…)
|
||||||
|
|
||||||
|
Called to reject (fail) a deferred, any value provided will be
|
||||||
|
passed onto the failure handler of the deferred object.
|
||||||
|
|
||||||
|
Rejecting a deferred which has already been resolved or
|
||||||
|
rejected has no effect.
|
||||||
|
|
||||||
|
.. js:function:: Deferred.pipe(doneFilter[, failFilter])
|
||||||
|
|
||||||
|
Filters the result of a deferred, able to transform a success
|
||||||
|
into failure and a failure into success, or to delay
|
||||||
|
resolution further.
|
||||||
|
|
||||||
|
.. [#] or simply calling :js:class:`Deferred` as a function, the
|
||||||
|
result is the same
|
||||||
|
|
||||||
|
.. [#] or not-promises, the `CommonJS Promises/B`_ role of
|
||||||
|
:js:func:`when` is to be able to treat values and promises
|
||||||
|
uniformly: :js:func:`when` will pass promises through directly,
|
||||||
|
but non-promise values and objects will be transformed into a
|
||||||
|
resolved promise (resolving themselves with the value itself).
|
||||||
|
|
||||||
|
jQuery's :js:func:`when` keeps this behavior making deferreds
|
||||||
|
easy to build from "static" values, or allowing defensive code
|
||||||
|
where expected promises are wrapped in :js:func:`when` just in
|
||||||
|
case.
|
||||||
|
|
||||||
|
.. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
|
||||||
|
.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
|
||||||
|
.. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A
|
||||||
|
.. _CommonJS Promises/B: http://wiki.commonjs.org/wiki/Promises/B
|
||||||
|
.. _mocks: http://en.wikipedia.org/wiki/Mock_object
|
|
@ -0,0 +1,108 @@
|
||||||
|
API changes from OpenERP Web 6.1 to 6.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
DataSet -> Model
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The 6.1 ``DataSet`` API has been deprecated in favor of the smaller
|
||||||
|
and more orthogonal :doc:`Model </rpc>` API, which more closely
|
||||||
|
matches the API in OpenERP Web's Python side and in OpenObject addons
|
||||||
|
and removes most stateful behavior of DataSet.
|
||||||
|
|
||||||
|
Migration guide
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Actual arbitrary RPC calls can just be remapped on a
|
||||||
|
:js:class:`~openerp.web.Model` instance:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
dataset.call(method, args)
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
dataset.call_and_eval(method, args)
|
||||||
|
|
||||||
|
can be replaced by calls to :js:func:`openerp.web.Model.call`:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
model.call(method, args)
|
||||||
|
|
||||||
|
If callbacks are passed directly to the older methods, they need to
|
||||||
|
be added to the new one via ``.then()``.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``context_index`` and ``domain_index`` features were not
|
||||||
|
ported, context and domain now need to be passed in "in full",
|
||||||
|
they won't be automatically filled with the user's current
|
||||||
|
context.
|
||||||
|
|
||||||
|
* Shorcut methods (``name_get``, ``name_search``, ``unlink``,
|
||||||
|
``write``, ...) should be ported to
|
||||||
|
:js:func:`openerp.web.Model.call`, using the server's original
|
||||||
|
signature. On the other hand, the non-shortcut equivalents can now
|
||||||
|
use keyword arguments (see :js:func:`~openerp.web.Model.call`'s
|
||||||
|
signature for details)
|
||||||
|
|
||||||
|
* ``read_slice``, which allowed a single round-trip to perform a
|
||||||
|
search and a read, should be reimplemented via
|
||||||
|
:js:class:`~openerp.web.Query` objects (see:
|
||||||
|
:js:func:`~openerp.web.Model.query`) for clearer and simpler
|
||||||
|
code. ``read_index`` should be replaced by a
|
||||||
|
:js:class:`~openerp.web.Query` as well, combining
|
||||||
|
:js:func:`~openerp.web.Query.offset` and
|
||||||
|
:js:func:`~openerp.web.Query.first`.
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Renaming
|
||||||
|
|
||||||
|
The name *DataSet* exists in the CS community consciousness, and
|
||||||
|
(as its name implies) it's a set of data (often fetched from a
|
||||||
|
database, maybe lazily). OpenERP Web's dataset behaves very
|
||||||
|
differently as it does not store (much) data (only a bunch of ids
|
||||||
|
and just enough state to break things). The name "Model" matches
|
||||||
|
the one used on the Python side for the task of building an RPC
|
||||||
|
proxy to OpenERP objects.
|
||||||
|
|
||||||
|
API simplification
|
||||||
|
|
||||||
|
``DataSet`` has a number of methods which serve as little more
|
||||||
|
than shortcuts, or are there due to domain and context evaluation
|
||||||
|
issues in 6.1.
|
||||||
|
|
||||||
|
The shortcuts really add little value, and OpenERP Web 6.2 embeds
|
||||||
|
a restricted Python evaluator (in javascript) meaning most of the
|
||||||
|
context and domain parsing & evaluation can be moved to the
|
||||||
|
javascript code and does not require cooperative RPC bridging.
|
||||||
|
|
||||||
|
DataGroup -> also Model
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Alongside the deprecation of ``DataSet`` for
|
||||||
|
:js:class:`~openerp.web.Model`, OpenERP Web 6.2 also deprecates
|
||||||
|
``DataGroup`` and its subtypes in favor of a single method on
|
||||||
|
:js:class:`~openerp.web.Query`:
|
||||||
|
:js:func:`~openerp.web.Query.group_by`.
|
||||||
|
|
||||||
|
Migration guide
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
While the ``DataGroup`` API worked (mostly), it is quite odd and
|
||||||
|
alien-looking, a bit too Smalltalk-inspired (behaves like a
|
||||||
|
self-contained flow-control structure for reasons which may or may not
|
||||||
|
have been good).
|
||||||
|
|
||||||
|
Because it is heavily related to ``DataSet`` (as it *yields*
|
||||||
|
``DataSet`` objects), deprecating ``DataSet`` automatically deprecates
|
||||||
|
``DataGroup`` (if we want to stay consistent), which is a good time to
|
||||||
|
make the API more imperative and look more like what most developers
|
||||||
|
are used to.
|
|
@ -8,6 +8,17 @@ Welcome to OpenERP Web's documentation!
|
||||||
|
|
||||||
Contents:
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
changelog-6.2
|
||||||
|
|
||||||
|
async
|
||||||
|
rpc
|
||||||
|
|
||||||
|
Older stuff
|
||||||
|
-----------
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,345 @@
|
||||||
|
Outside the box: network interactions
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Building static displays is all nice and good and allows for neat
|
||||||
|
effects (and sometimes you're given data to display from third parties
|
||||||
|
so you don't have to make any effort), but a point generally comes
|
||||||
|
where you'll want to talk to the world and make some network requests.
|
||||||
|
|
||||||
|
OpenERP Web provides two primary APIs to handle this, a low-level
|
||||||
|
JSON-RPC based API communicating with the Python section of OpenERP
|
||||||
|
Web (and of your addon, if you have a Python part) and a high-level
|
||||||
|
API above that allowing your code to talk directly to the OpenERP
|
||||||
|
server, using familiar-looking calls.
|
||||||
|
|
||||||
|
All networking APIs are :doc:`asynchronous </async>`. As a result, all
|
||||||
|
of them will return :js:class:`Deferred` objects (whether they resolve
|
||||||
|
those with values or not). Understanding how those work before before
|
||||||
|
moving on is probably necessary.
|
||||||
|
|
||||||
|
High-level API: calling into OpenERP models
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Access to OpenERP object methods (made available through XML-RPC from
|
||||||
|
the server) is done via the :js:class:`openerp.web.Model` class. This
|
||||||
|
class maps ontwo the OpenERP server objects via two primary methods,
|
||||||
|
:js:func:`~openerp.web.Model.call` and
|
||||||
|
:js:func:`~openerp.web.Model.query`.
|
||||||
|
|
||||||
|
:js:func:`~openerp.web.Model.call` is a direct mapping to the
|
||||||
|
corresponding method of the OpenERP server object. Its usage is
|
||||||
|
similar to that of the OpenERP Model API, with three differences:
|
||||||
|
|
||||||
|
* The interface is :doc:`asynchronous </async>`, so instead of
|
||||||
|
returning results directly RPC method calls will return
|
||||||
|
:js:class:`Deferred` instances, which will themselves resolve to the
|
||||||
|
result of the matching RPC call.
|
||||||
|
|
||||||
|
* Because ECMAScript 3/Javascript 1.5 doesnt feature any equivalent to
|
||||||
|
``__getattr__`` or ``method_missing``, there needs to be an explicit
|
||||||
|
method to dispatch RPC methods.
|
||||||
|
|
||||||
|
* No notion of pooler, the model proxy is instantiated where needed,
|
||||||
|
not fetched from an other (somewhat global) object
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
var Users = new Model('res.users');
|
||||||
|
|
||||||
|
Users.call('change_password', ['oldpassword', 'newpassword'],
|
||||||
|
{context: some_context}).then(function (result) {
|
||||||
|
// do something with change_password result
|
||||||
|
});
|
||||||
|
|
||||||
|
:js:func:`~openerp.web.Model.query` is a shortcut for a builder-style
|
||||||
|
iterface to searches (``search`` + ``read`` in OpenERP RPC terms). It
|
||||||
|
returns a :js:class:`~openerp.web.Query` object which is immutable but
|
||||||
|
allows building new :js:class:`~openerp.web.Query` instances from the
|
||||||
|
first one, adding new properties or modifiying the parent object's:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
Users.query(['name', 'login', 'user_email', 'signature'])
|
||||||
|
.filter([['active', '=', true], ['company_id', '=', main_company]])
|
||||||
|
.limit(15)
|
||||||
|
.all().then(function (users) {
|
||||||
|
// do work with users records
|
||||||
|
});
|
||||||
|
|
||||||
|
The query is only actually performed when calling one of the query
|
||||||
|
serialization methods, :js:func:`~openerp.web.Query.all` and
|
||||||
|
:js:func:`~openerp.web.Query.first`. These methods will perform a new
|
||||||
|
RPC query every time they are called.
|
||||||
|
|
||||||
|
For that reason, it's actually possible to keep "intermediate" queries
|
||||||
|
around and use them differently/add new specifications on them.
|
||||||
|
|
||||||
|
.. js:class:: openerp.web.Model(name)
|
||||||
|
|
||||||
|
.. js:attribute:: openerp.web.Model.name
|
||||||
|
|
||||||
|
name of the OpenERP model this object is bound to
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Model.call(method[, args][, kwargs])
|
||||||
|
|
||||||
|
Calls the ``method`` method of the current model, with the
|
||||||
|
provided positional and keyword arguments.
|
||||||
|
|
||||||
|
:param String method: method to call over rpc on the
|
||||||
|
:js:attr:`~openerp.web.Model.name`
|
||||||
|
:param Array<> args: positional arguments to pass to the
|
||||||
|
method, optional
|
||||||
|
:param Object<> kwargs: keyword arguments to pass to the
|
||||||
|
method, optional
|
||||||
|
:rtype: Deferred<>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Model.query(fields)
|
||||||
|
|
||||||
|
:param Array<String> fields: list of fields to fetch during
|
||||||
|
the search
|
||||||
|
:returns: a :js:class:`~openerp.web.Query` object
|
||||||
|
representing the search to perform
|
||||||
|
|
||||||
|
.. js:class:: openerp.web.Query(fields)
|
||||||
|
|
||||||
|
The first set of methods is the "fetching" methods. They perform
|
||||||
|
RPC queries using the internal data of the object they're called
|
||||||
|
on.
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.all()
|
||||||
|
|
||||||
|
Fetches the result of the current
|
||||||
|
:js:class:`~openerp.web.Query` object's search.
|
||||||
|
|
||||||
|
:rtype: Deferred<Array<>>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.first()
|
||||||
|
|
||||||
|
Fetches the **first** result of the current
|
||||||
|
:js:class:`~openerp.web.Query`, or ``null`` if the current
|
||||||
|
:js:class:`~openerp.web.Query` does have any result.
|
||||||
|
|
||||||
|
:rtype: Deferred<Object | null>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.count()
|
||||||
|
|
||||||
|
Fetches the number of records the current
|
||||||
|
:js:class:`~openerp.web.Query` would retrieve.
|
||||||
|
|
||||||
|
:rtype: Deferred<Number>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.group_by(grouping...)
|
||||||
|
|
||||||
|
Fetches the groups for the query, using the first specified
|
||||||
|
grouping parameter
|
||||||
|
|
||||||
|
:param Array<String> grouping: Lists the levels of grouping
|
||||||
|
asked of the server. Grouping
|
||||||
|
can actually be an array or
|
||||||
|
varargs.
|
||||||
|
:rtype: Deferred<Array<openerp.web.Group>> | null
|
||||||
|
|
||||||
|
The second set of methods is the "mutator" methods, they create a
|
||||||
|
**new** :js:class:`~openerp.web.Query` object with the relevant
|
||||||
|
(internal) attribute either augmented or replaced.
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.context(ctx)
|
||||||
|
|
||||||
|
Adds the provided ``ctx`` to the query, on top of any existing
|
||||||
|
context
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.filter(domain)
|
||||||
|
|
||||||
|
Adds the provided domain to the query, this domain is
|
||||||
|
``AND``-ed to the existing query domain.
|
||||||
|
|
||||||
|
.. js:function:: opeenrp.web.Query.offset(offset)
|
||||||
|
|
||||||
|
Sets the provided offset on the query. The new offset
|
||||||
|
*replaces* the old one.
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.limit(limit)
|
||||||
|
|
||||||
|
Sets the provided limit on the query. The new limit *replaces*
|
||||||
|
the old one.
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Query.order_by(fields…)
|
||||||
|
|
||||||
|
Overrides the model's natural order with the provided field
|
||||||
|
specifications. Behaves much like Django's `QuerySet.order_by
|
||||||
|
<https://docs.djangoproject.com/en/dev/ref/models/querysets/#order-by>`_:
|
||||||
|
|
||||||
|
* Takes 1..n field names, in order of most to least importance
|
||||||
|
(the first field is the first sorting key). Fields are
|
||||||
|
provided as strings.
|
||||||
|
|
||||||
|
* A field specifies an ascending order, unless it is prefixed
|
||||||
|
with the minus sign "``-``" in which case the field is used
|
||||||
|
in the descending order
|
||||||
|
|
||||||
|
Divergences from Django's sorting include a lack of random sort
|
||||||
|
(``?`` field) and the inability to "drill down" into relations
|
||||||
|
for sorting.
|
||||||
|
|
||||||
|
Aggregation (grouping)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
OpenERP has powerful grouping capacities, but they are kind-of strange
|
||||||
|
in that they're recursive, and level n+1 relies on data provided
|
||||||
|
directly by the grouping at level n. As a result, while ``read_group``
|
||||||
|
works it's not a very intuitive API.
|
||||||
|
|
||||||
|
OpenERP Web 6.2 eschews direct calls to ``read_group`` in favor of
|
||||||
|
calling a method of :js:class:`~openerp.web.Query`, `much in the way
|
||||||
|
it is one in SQLAlchemy
|
||||||
|
<http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.group_by>`_ [#]_:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
some_query.group_by(['field1', 'field2']).then(function (groups) {
|
||||||
|
// do things with the fetched groups
|
||||||
|
});
|
||||||
|
|
||||||
|
This method is asynchronous when provided with 1..n fields (to group
|
||||||
|
on) as argument, but it can also be called without any field (empty
|
||||||
|
fields collection or nothing at all). In this case, instead of
|
||||||
|
returning a Deferred object it will return ``null``.
|
||||||
|
|
||||||
|
When grouping criterion come from a third-party and may or may not
|
||||||
|
list fields (e.g. could be an empty list), this provides two ways to
|
||||||
|
test the presence of actual subgroups (versus the need to perform a
|
||||||
|
regular query for records):
|
||||||
|
|
||||||
|
* A check on ``group_by``'s result and two completely separate code
|
||||||
|
paths
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
var groups;
|
||||||
|
if (groups = some_query.group_by(gby)) {
|
||||||
|
groups.then(function (gs) {
|
||||||
|
// groups
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// no groups
|
||||||
|
|
||||||
|
* Or a more coherent code path using :js:func:`when`'s ability to
|
||||||
|
coerce values into deferreds:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
$.when(some_query.group_by(gby)).then(function (groups) {
|
||||||
|
if (!groups) {
|
||||||
|
// No grouping
|
||||||
|
} else {
|
||||||
|
// grouping, even if there are no groups (groups
|
||||||
|
// itself could be an empty array)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
The result of a (successful) :js:func:`~openerp.web.Query.group_by` is
|
||||||
|
an array of :js:class:`~openerp.web.data.Group`.
|
||||||
|
|
||||||
|
Synchronizing views (provisional)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. note:: this API may not be final, and may not even remain
|
||||||
|
|
||||||
|
While the high-level RPC API is mostly stateless, some objects in
|
||||||
|
OpenERP Web need to share state information. One of those is OpenERP
|
||||||
|
views, especially between "collection-based" views (lists, graphs) and
|
||||||
|
"record-based" views (forms, diagrams), which gets its very own API
|
||||||
|
for traversing collections of records, the aptly-named
|
||||||
|
:js:class:`~openerp.web.Traverser`.
|
||||||
|
|
||||||
|
A :js:class:`~openerp.web.Traverser` is linked to a
|
||||||
|
:js:class:`~openerp.web.Model` and is used to iterate over it
|
||||||
|
asynchronously (and using indexes).
|
||||||
|
|
||||||
|
.. js:class:: openerp.web.Traverser(model)
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Traverser.model()
|
||||||
|
|
||||||
|
:returns: the :js:class:`~openerp.web.Model` this traverser
|
||||||
|
instance is bound to
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Traverser.index([idx])
|
||||||
|
|
||||||
|
If provided with an index parameter, sets that as the new
|
||||||
|
index for the traverser.
|
||||||
|
|
||||||
|
:param Number idx: the new index for the traverser
|
||||||
|
:returns: the current index for the traverser
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Traverser.current([fields])
|
||||||
|
|
||||||
|
Fetches the traverser's "current" record (that is, the record
|
||||||
|
at the current index of the traverser)
|
||||||
|
|
||||||
|
:param Array<String> fields: fields to return in the record
|
||||||
|
:rtype: Deferred<>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Traverser.next([fields])
|
||||||
|
|
||||||
|
Increases the traverser's internal index by one, the fetches
|
||||||
|
the corresponding record. Roughly equivalent to:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
var idx = traverser.index();
|
||||||
|
traverser.index(idx+1);
|
||||||
|
traverser.current();
|
||||||
|
|
||||||
|
:param Array<String> fields: fields to return in the record
|
||||||
|
:rtype: Deferred<>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Traverser.previous([fields])
|
||||||
|
|
||||||
|
Similar to :js:func:`~openerp.web.Traverser.next` but iterates
|
||||||
|
the traverser backwards rather than forward.
|
||||||
|
|
||||||
|
:param Array<String> fields: fields to return in the record
|
||||||
|
:rtype: Deferred<>
|
||||||
|
|
||||||
|
.. js:function:: openerp.web.Traverser.size()
|
||||||
|
|
||||||
|
Shortcut to checking the size of the backing model, calling
|
||||||
|
``traverser.size()`` is equivalent to calling
|
||||||
|
``traverser.model().query([]).count()``
|
||||||
|
|
||||||
|
:rtype: Deferred<Number>
|
||||||
|
|
||||||
|
Low-level API: RPC calls to Python side
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
While the previous section is great for calling core OpenERP code
|
||||||
|
(models code), it does not work if you want to call the Python side of
|
||||||
|
openerp-web.
|
||||||
|
|
||||||
|
For this. a lower-level API is available on
|
||||||
|
:js:class:`openerp.web.Connection` objects (usually available through
|
||||||
|
``openerp.connection``): the ``rpc`` method.
|
||||||
|
|
||||||
|
This method simply takes an absolute path (which is the combination of
|
||||||
|
the Python controller's ``_cp_path`` attribute and the name of the
|
||||||
|
method yo want to call) and a mapping of attributes to values (applied
|
||||||
|
as keyword arguments on the Python method [#]_). This function fetches
|
||||||
|
the return value of the Python methods, converted to JSON.
|
||||||
|
|
||||||
|
For instance, to call the ``eval_domain_and_context`` of the
|
||||||
|
:class:`~web.controllers.main.Session` controller:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
openerp.connection.rpc('/web/session/eval_domain_and_context', {
|
||||||
|
domains: ds,
|
||||||
|
contexts: cs
|
||||||
|
}).then(function (result) {
|
||||||
|
// handle result
|
||||||
|
});
|
||||||
|
|
||||||
|
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
|
||||||
|
is not terminal, it returns a query which can still be altered.
|
||||||
|
|
||||||
|
.. [#] except for ``context``, which is extracted and stored in the
|
||||||
|
request object itself.
|
19
openerp-web
19
openerp-web
|
@ -50,6 +50,19 @@ logging_opts.add_option("--log-config", dest="log_config", default=os.path.join(
|
||||||
help="Logging configuration file", metavar="FILE")
|
help="Logging configuration file", metavar="FILE")
|
||||||
optparser.add_option_group(logging_opts)
|
optparser.add_option_group(logging_opts)
|
||||||
|
|
||||||
|
testing_opts = optparse.OptionGroup(optparser, "Testing")
|
||||||
|
testing_opts.add_option('--test-mode', dest='test_mode',
|
||||||
|
action='store_true', default=False,
|
||||||
|
help="Starts test mode, which provides a few"
|
||||||
|
" (utterly unsafe) APIs for testing purposes and"
|
||||||
|
" sets up a special connector which always raises"
|
||||||
|
" errors on tentative server access. These errors"
|
||||||
|
" serialize RPC query information (service,"
|
||||||
|
" method, arguments list) in the fault_code"
|
||||||
|
" attribute of the error object returned to the"
|
||||||
|
" client. This lets javascript code assert the" \
|
||||||
|
" XMLRPC consequences of its queries.")
|
||||||
|
optparser.add_option_group(testing_opts)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
(options, args) = optparser.parse_args(sys.argv[1:])
|
(options, args) = optparser.parse_args(sys.argv[1:])
|
||||||
|
@ -78,6 +91,12 @@ if __name__ == "__main__":
|
||||||
options.backend = 'xmlrpc'
|
options.backend = 'xmlrpc'
|
||||||
os.environ["TZ"] = "UTC"
|
os.environ["TZ"] = "UTC"
|
||||||
|
|
||||||
|
if options.test_mode:
|
||||||
|
import web.test_support
|
||||||
|
import web.test_support.controllers
|
||||||
|
options.connector = web.test_support.TestConnector()
|
||||||
|
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||||
|
|
||||||
if sys.version_info >= (2, 7) and os.path.exists(options.log_config):
|
if sys.version_info >= (2, 7) and os.path.exists(options.log_config):
|
||||||
with open(options.log_config) as file:
|
with open(options.log_config) as file:
|
||||||
dct = json.load(file)
|
dct = json.load(file)
|
||||||
|
|
Loading…
Reference in New Issue