[MERGE] model-backed datasets

bzr revid: xmo@openerp.com-20120419081433-6tmpdfum79lixxah
This commit is contained in:
Xavier Morel 2012-04-19 10:14:33 +02:00
commit cafb491afc
28 changed files with 2146 additions and 668 deletions

View File

@ -405,14 +405,14 @@ def session_context(request, storage_path, session_cookie='sessionid'):
#----------------------------------------------------------
addons_module = {}
addons_manifest = {}
controllers_class = {}
controllers_class = []
controllers_object = {}
controllers_path = {}
class ControllerType(type):
def __init__(cls, 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):
__metaclass__ = ControllerType
@ -440,12 +440,12 @@ class Root(object):
self.root = '/web/webclient/home'
self.config = options
if self.config.backend == 'local':
conn = LocalConnector()
else:
conn = openerplib.get_connector(hostname=self.config.server_host,
port=self.config.server_port)
self.config.connector = conn
if not hasattr(self.config, 'connector'):
if self.config.backend == 'local':
self.config.connector = LocalConnector()
else:
self.config.connector = openerplib.get_connector(
hostname=self.config.server_host, port=self.config.server_port)
self.session_cookie = 'sessionid'
self.addons = {}
@ -526,7 +526,7 @@ class Root(object):
addons_module[module] = m
addons_manifest[module] = manifest
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:
o = v()
controllers_object[k] = o

View File

@ -909,11 +909,6 @@ class Menu(openerpweb.Controller):
class DataSet(openerpweb.Controller):
_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
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)
@ -949,7 +944,6 @@ class DataSet(openerpweb.Controller):
if fields and fields == ['id']:
# shortcut read if we only want the ids
return {
'ids': ids,
'length': length,
'records': [{'id': id} for id in ids]
}
@ -957,46 +951,10 @@ class DataSet(openerpweb.Controller):
records = Model.read(ids, fields or False, context)
records.sort(key=lambda obj: ids.index(obj['id']))
return {
'ids': ids,
'length': length,
'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
def load(self, req, model, id, fields):
m = req.session.model(model)
@ -1006,23 +964,6 @@ class DataSet(openerpweb.Controller):
value = r[0]
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):
has_domain = domain_id is not None and domain_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
def exec_workflow(self, req, model, id, signal):
r = 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}
return req.session.exec_workflow(model, id, signal)
class DataGroup(openerpweb.Controller):
_cp_path = "/web/group"

10
addons/web/static/lib/qunit/qunit.css Executable file → Normal file
View File

@ -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
*
* Copyright (c) 2011 John Resig, Jörn Zaefferer
* Copyright (c) 2012 John Resig, Jörn Zaefferer
* Dual licensed under the MIT (MIT-LICENSE.txt)
* or GPL (GPL-LICENSE.txt) licenses.
*/
@ -54,6 +54,10 @@
color: #fff;
}
#qunit-header label {
display: inline-block;
}
#qunit-banner {
height: 5px;
}
@ -223,4 +227,6 @@
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

119
addons/web/static/lib/qunit/qunit.js Executable file → Normal file
View File

@ -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
*
* Copyright (c) 2011 John Resig, Jörn Zaefferer
* Copyright (c) 2012 John Resig, Jörn Zaefferer
* Dual licensed under the MIT (MIT-LICENSE.txt)
* or GPL (GPL-LICENSE.txt) licenses.
*/
@ -13,8 +13,11 @@
var defined = {
setTimeout: typeof window.setTimeout !== "undefined",
sessionStorage: (function() {
var x = "qunit-test-string";
try {
return !!sessionStorage.getItem;
sessionStorage.setItem(x, x);
sessionStorage.removeItem(x);
return true;
} catch(e) {
return false;
}
@ -25,11 +28,10 @@ var testId = 0,
toString = Object.prototype.toString,
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.testName = testName;
this.expected = expected;
this.testEnvironmentArg = testEnvironmentArg;
this.async = async;
this.callback = callback;
this.assertions = [];
@ -62,6 +64,10 @@ Test.prototype = {
runLoggingCallbacks( 'moduleStart', QUnit, {
name: this.module
} );
} else if (config.autorun) {
runLoggingCallbacks( 'moduleStart', QUnit, {
name: this.module
} );
}
config.current = this;
@ -69,9 +75,6 @@ Test.prototype = {
setup: function() {},
teardown: function() {}
}, this.moduleTestEnvironment);
if (this.testEnvironmentArg) {
extend(this.testEnvironment, this.testEnvironmentArg);
}
runLoggingCallbacks( 'testStart', QUnit, {
name: this.testName,
@ -274,17 +277,12 @@ var QUnit = {
},
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 ) {
callback = expected;
expected = null;
}
// is 2nd argument a testEnvironment?
if ( expected && typeof expected === 'object') {
testEnvironmentArg = expected;
expected = null;
}
if ( config.currentModule ) {
name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
@ -294,7 +292,7 @@ var QUnit = {
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.moduleTestEnvironment = config.currentModuleTestEnviroment;
test.queue();
@ -312,6 +310,9 @@ var QUnit = {
* @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
*/
ok: function(a, msg) {
if (!config.current) {
throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2));
}
a = !!a;
var details = {
result: a,
@ -447,9 +448,14 @@ var QUnit = {
QUnit.constructor = F;
})();
// Backwards compatibility, deprecated
QUnit.equals = QUnit.equal;
QUnit.same = QUnit.deepEqual;
// deprecated; still export them to window to provide clear error messages
// next step: remove entirely
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
var config = {
@ -513,8 +519,7 @@ if ( typeof exports === "undefined" || typeof require === "undefined" ) {
extend(window, QUnit);
window.QUnit = QUnit;
} else {
extend(exports, QUnit);
exports.QUnit = QUnit;
module.exports = QUnit;
}
// define these after exposing globals to keep them in these QUnit namespace only
@ -536,6 +541,16 @@ extend(QUnit, {
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" ),
banner = id( "qunit-banner" ),
result = id( "qunit-testresult" );
@ -564,15 +579,15 @@ extend(QUnit, {
/**
* 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() {
if ( window.jQuery ) {
jQuery( "#qunit-fixture" ).html( config.fixture );
} else {
var main = id( 'qunit-fixture' );
if ( main ) {
main.innerHTML = config.fixture;
var main = id( 'qunit-fixture' );
if ( main ) {
if ( window.jQuery ) {
jQuery( main ).replaceWith( config.fixture.cloneNode(true) );
} else {
main.parentNode.replaceChild(config.fixture.cloneNode(true), main);
}
}
},
@ -636,6 +651,9 @@ extend(QUnit, {
},
push: function(result, actual, expected, message) {
if (!config.current) {
throw new Error("assertion outside test context, was " + sourceFromStacktrace());
}
var details = {
result: result,
message: message,
@ -645,21 +663,22 @@ extend(QUnit, {
message = escapeInnerText(message) || (result ? "okay" : "failed");
message = '<span class="test-message">' + message + "</span>";
expected = escapeInnerText(QUnit.jsDump.parse(expected));
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>';
}
var output = message;
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();
if (source) {
details.source = source;
output += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeInnerText(source) + '</pre></td></tr>';
}
output += "</table>";
}
output += "</table>";
runLoggingCallbacks( 'log', QUnit, details );
@ -779,7 +798,7 @@ QUnit.load = function() {
var main = id('qunit-fixture');
if ( main ) {
config.fixture = main.innerHTML;
config.fixture = main.cloneNode(true);
}
if (config.autostart) {
@ -847,6 +866,15 @@ function done() {
].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, {
failed: config.stats.bad,
passed: passed,
@ -881,16 +909,21 @@ function validTest( name ) {
// so far supports only Firefox, Chrome and Opera (buggy)
// could be extended in the future to use something like https://github.com/csnover/TraceKit
function sourceFromStacktrace() {
function sourceFromStacktrace(offset) {
offset = offset || 3;
try {
throw new Error();
} catch ( e ) {
if (e.stacktrace) {
// Opera
return e.stacktrace.split("\n")[6];
return e.stacktrace.split("\n")[offset + 3];
} else if (e.stack) {
// 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) {
// Safari, PhantomJS
// 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 ) {
console.error(message);
console.error(exception);
console.error(exception.stack);
console.warn(callback.toString());
} else if ( window.opera && opera.postError ) {
@ -1368,9 +1402,9 @@ QUnit.jsDump = (function() {
var ret = [ ];
QUnit.jsDump.up();
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));
}
}
QUnit.jsDump.down();
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() );

View File

@ -1390,6 +1390,7 @@ instance.web.Connection = instance.web.CallbackEnabled.extend( /** @lends instan
// an invalid session or no session at all), refresh session data
// (should not change, but just in case...)
_.extend(self, {
session_id: result.session_id,
db: result.db,
username: result.login,
uid: result.uid,

File diff suppressed because it is too large Load Diff

View File

@ -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);
})
}
};

View File

@ -2291,6 +2291,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
this.last_search = [];
this.floating = false;
this.inhibit_on_change = false;
this.orderer = new openerp.web.DropMisordered();
},
start: function() {
this._super();
@ -2439,14 +2440,10 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
var search_val = request.term;
var self = this;
if (this.abort_last) {
this.abort_last();
delete this.abort_last;
}
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
dataset.name_search(search_val, self.build_domain(), 'ilike',
this.limit + 1, function(data) {
this.orderer.add(dataset.name_search(
search_val, self.build_domain(), 'ilike', this.limit + 1)).then(function(data) {
self.last_search = data;
// possible selections for the m2o
var values = _.map(data, function(x) {
@ -2485,7 +2482,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten
response(values);
});
this.abort_last = dataset.abort_last;
},
_quick_create: function(name) {
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});
};
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());
dataset.name_create(name, function(data) {
self.display_value = {};
self.display_value["" + data[0]] = data[1];
self.set({value: data[0]});
}).fail(function(error, event) {
event.preventDefault();
slow_create();
});
new instance.web.DataSet(this, this.field.relation, self.build_context())
.name_create(name, function(data) {
self.display_value = {};
self.display_value["" + data[0]] = data[1];
self.set({value: data[0]});
}).fail(function(error, event) {
event.preventDefault();
slow_create();
});
} else
slow_create();
},

View File

@ -2,8 +2,8 @@ $(document).ready(function () {
var openerp;
module('web-class', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
}
});
test('Basic class creation', function () {

View File

@ -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);
});

View File

@ -3,8 +3,9 @@ $(document).ready(function () {
module("eval.contexts", {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
}
});
test('context_sequences', function () {

View File

@ -2,8 +2,9 @@ $(document).ready(function () {
var openerp;
module("form.widget", {
setup: function () {
openerp = window.openerp.init(true);
window.openerp.web.core(openerp);
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);

View File

@ -3,8 +3,9 @@ $(document).ready(function () {
module('server-formats', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
}
});
@ -40,8 +41,9 @@ $(document).ready(function () {
module('web-formats', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
window.openerp.web.formats(openerp);
}
@ -206,8 +208,9 @@ $(document).ready(function () {
});
module('custom-date-formats', {
setup: function () {
openerp = window.openerp.init();
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
window.openerp.web.coresetup(openerp);
window.openerp.web.dates(openerp);
window.openerp.web.formats(openerp);
}

View File

@ -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>

View File

@ -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
});
});
});
});

View File

@ -10,7 +10,13 @@ $(document).ready(function () {
};
module('list-events', {
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);
}
});
@ -90,7 +96,13 @@ $(document).ready(function () {
module('list-records', {
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);
}
});
@ -123,7 +135,13 @@ $(document).ready(function () {
module('list-collections-degenerate', {
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);
}
});
@ -245,7 +263,13 @@ $(document).ready(function () {
module('list-hofs', {
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);
}
});

View File

@ -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]);
});
});

View File

@ -2,8 +2,8 @@ $(document).ready(function () {
var openerp;
module('Registry', {
setup: function () {
openerp = window.openerp.init(true);
window.openerp.web.core(openerp);
openerp = window.openerp.init([]);
window.openerp.web.corelib(openerp);
openerp.web.Foo = {};
openerp.web.Bar = {};
openerp.web.Foo2 = {};

View File

@ -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);
});
});

View File

@ -26,10 +26,9 @@
<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/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/formats.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/list-utils.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>
</html>

View File

@ -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
}, '')

View File

@ -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,
}

View File

@ -107,7 +107,7 @@ openerp.web_process = function (instance) {
if(this.process_id)
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
.call("search_by_model", [self.process_model,self.session.context])
.done(function(res) {
@ -237,7 +237,7 @@ openerp.web_process = function (instance) {
},
jump_to_view: function(model, id) {
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',
['action', 'tree_but_open',[['ir.ui.menu', id]], dataset.context],
function(res) {

353
doc/async.rst Normal file
View File

@ -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

108
doc/changelog-6.2.rst Normal file
View File

@ -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.

View File

@ -8,6 +8,17 @@ Welcome to OpenERP Web's documentation!
Contents:
.. toctree::
:maxdepth: 1
changelog-6.2
async
rpc
Older stuff
-----------
.. toctree::
:maxdepth: 2

345
doc/rpc.rst Normal file
View File

@ -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.

View File

@ -50,6 +50,19 @@ logging_opts.add_option("--log-config", dest="log_config", default=os.path.join(
help="Logging configuration file", metavar="FILE")
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__":
(options, args) = optparser.parse_args(sys.argv[1:])
@ -78,6 +91,12 @@ if __name__ == "__main__":
options.backend = 'xmlrpc'
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):
with open(options.log_config) as file:
dct = json.load(file)