From 9f6af7c9747d8f85e3296d3044f13b735834dc70 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 21 Feb 2012 18:04:55 +0100 Subject: [PATCH 001/238] [ADD] asynchronous javascript guide, as it'll be needed by the networking/RPC guide bzr revid: xmo@openerp.com-20120221170455-rcrbyvz1ozj7mfib --- doc/source/async.rst | 353 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 doc/source/async.rst diff --git a/doc/source/async.rst b/doc/source/async.rst new file mode 100644 index 00000000000..23b3409bd8f --- /dev/null +++ b/doc/source/async.rst @@ -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 +`. + +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 `_. 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 From f926427406a8fa6cd60af52476119bd1d49691dc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 23 Feb 2012 13:28:29 +0100 Subject: [PATCH 002/238] [FIX] use an array for controller classes, so they are iterated in the correct order (otherwise it might not be possible to override an extended controller) bzr revid: xmo@openerp.com-20120223122829-mtz2w120qq8rbjrx --- addons/web/common/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/web/common/http.py b/addons/web/common/http.py index 71a280f27eb..26807f65b69 100644 --- a/addons/web/common/http.py +++ b/addons/web/common/http.py @@ -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 @@ -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 From b58012f32ab31b9a1b55297de915fe2257dea655 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 23 Feb 2012 13:31:51 +0100 Subject: [PATCH 003/238] [IMP] re-set session id on session reload bzr revid: xmo@openerp.com-20120223123151-w2s4g0y38z7xjoeu --- addons/web/static/src/js/core.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index c4e37a8a3b1..10f3167b330 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -639,6 +639,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. // 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, From 5ac6e532bdfeef0f3bf8ad45ba8bc17183ff423c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 23 Feb 2012 13:32:13 +0100 Subject: [PATCH 004/238] [FIX] if a connector was setup in the configuration object, don't re-set one instead bzr revid: xmo@openerp.com-20120223123213-aftgz7h0zxkbzsju --- addons/web/common/http.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/web/common/http.py b/addons/web/common/http.py index 26807f65b69..9294808fda4 100644 --- a/addons/web/common/http.py +++ b/addons/web/common/http.py @@ -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 = {} From 0ff97e49295c9836bebc730049bafd23b9936978 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 23 Feb 2012 13:34:16 +0100 Subject: [PATCH 005/238] [ADD] testing scaffold which does not hit the OpenERP server bzr revid: xmo@openerp.com-20120223123416-2zvxy6nqbe9gdhhw --- addons/web/static/src/js/test_support.js | 64 ++++++++++++++++++++++ addons/web/static/test/fulltest.html | 49 +++++++++++++++++ addons/web/static/test/fulltest/dataset.js | 11 ++++ addons/web/test_support/__init__.py | 37 +++++++++++++ addons/web/test_support/controllers.py | 31 +++++++++++ openerp-web | 19 +++++++ 6 files changed, 211 insertions(+) create mode 100644 addons/web/static/src/js/test_support.js create mode 100644 addons/web/static/test/fulltest.html create mode 100644 addons/web/static/test/fulltest/dataset.js create mode 100644 addons/web/test_support/__init__.py create mode 100644 addons/web/test_support/controllers.py diff --git a/addons/web/static/src/js/test_support.js b/addons/web/static/src/js/test_support.js new file mode 100644 index 00000000000..5fcfb94c6a2 --- /dev/null +++ b/addons/web/static/src/js/test_support.js @@ -0,0 +1,64 @@ +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, fn) { + 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); + openerp.test_support.setup_connection(oe.connection) + .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'); + } + fn(e.data.fault_code); + }) + } +}; diff --git a/addons/web/static/test/fulltest.html b/addons/web/static/test/fulltest.html new file mode 100644 index 00000000000..f27adfa7df9 --- /dev/null +++ b/addons/web/static/test/fulltest.html @@ -0,0 +1,49 @@ + + + + + OpenERP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ OpenERP Web Test Suite: javascript to XML-RPC (excluded) +

+

+
+

+
    +
    + + + diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js new file mode 100644 index 00000000000..4b948261a3b --- /dev/null +++ b/addons/web/static/test/fulltest/dataset.js @@ -0,0 +1,11 @@ +$(document).ready(function () { + var t = window.openerp.test_support; + + t.module('check', 'data'); + t.test('check1', function (openerp) { + var ds = new openerp.web.DataSet({session: openerp.connection}, 'res.users', {}); + t.expect(ds.create({name: 'foo'}), function (result) { + ok(false, 'ha ha ha') + }); + }); +}); diff --git a/addons/web/test_support/__init__.py b/addons/web/test_support/__init__.py new file mode 100644 index 00000000000..59f6cc67ca2 --- /dev/null +++ b/addons/web/test_support/__init__.py @@ -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 + }, '') diff --git a/addons/web/test_support/controllers.py b/addons/web/test_support/controllers.py new file mode 100644 index 00000000000..f8e3c7a0739 --- /dev/null +++ b/addons/web/test_support/controllers.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from ..common.http import Controller, jsonrequest +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 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, + } diff --git a/openerp-web b/openerp-web index ec72085db2c..3340b95646e 100755 --- a/openerp-web +++ b/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") 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) From 27dfa4d12b2acc53176962f3cb0753e7349176ee Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 24 Feb 2012 11:57:53 +0100 Subject: [PATCH 006/238] [ADD] a bunch of tests of the dataset API (translation of javascript dataset calls to XMLRPC ones) bzr revid: xmo@openerp.com-20120224105753-hqf7o9xwwkc0xcok --- addons/web/static/test/fulltest/dataset.js | 144 ++++++++++++++++++++- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index 4b948261a3b..21b6b80cb80 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -1,11 +1,145 @@ $(document).ready(function () { var t = window.openerp.test_support; - t.module('check', 'data'); - t.test('check1', function (openerp) { - var ds = new openerp.web.DataSet({session: openerp.connection}, 'res.users', {}); - t.expect(ds.create({name: 'foo'}), function (result) { - ok(false, 'ha ha ha') + 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, 3); + deepEqual(result.args[0], [30]); + deepEqual(result.args[1], ['a', 'b', 'c']); + + ok(_.isEmpty(result.kwargs)); }); }); + 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, 2); + deepEqual(result.args[0], ['a', 'b', 'c']); + // FIXME: args[1] is context w/ user context, where to get? Hardcode? + strictEqual(result.args[1].foo, 'bar'); + + ok(_.isEmpty(result.kwargs)); + }); + }); + 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, 2); + deepEqual(r.args[0], {foo: 1, bar: 2}); + + ok(_.isEmpty(r.kwargs)); + }); + }); + 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, 3); + deepEqual(r.args[0], [42]); + deepEqual(r.args[1], {foo: 1}); + + ok(_.isEmpty(r.kwargs)); + }); + // 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, 2); + deepEqual(r.args[0], [42]); + + ok(_.isEmpty(r.kwargs)); + }); + }); + 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, 2); + deepEqual(r.args[0], [1, 2]); + + ok(_.isEmpty(r.kwargs)); + }); + }); + 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, 5); + strictEqual(r.args[0], 'bob'); + // domain + deepEqual(r.args[1], []); + strictEqual(r.args[2], 'ilike'); + strictEqual(r.args[4], 0); + + ok(_.isEmpty(r.kwargs)); + }); + }); + 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, 5); + strictEqual(r.args[0], ''); + // domain + deepEqual(r.args[1], [['foo', '=', 3]]); + strictEqual(r.args[2], 'someop'); + // limit + strictEqual(r.args[4], 0); + + ok(_.isEmpty(r.kwargs)); + }); + }); + 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); + }); + }); + // TODO: SearchDataSet#read_slice? + // TODO: non-literal domains and contexts basics + // TODO: call_and_eval + // TODO: name_search, non-literal domains + }); From f67ddedd2a8e802a309ace02511795366099b776 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 24 Feb 2012 13:26:20 +0100 Subject: [PATCH 007/238] [ADD] read_slice spec tests bzr revid: xmo@openerp.com-20120224122620-ji9h2sckpxx3kask --- addons/web/static/test/fulltest/dataset.js | 39 +++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index 21b6b80cb80..d42f1b363d3 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -137,7 +137,44 @@ $(document).ready(function () { strictEqual(r.args[5], 42); }); }); - // TODO: SearchDataSet#read_slice? + + 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'); + + ok(_.isEmpty(r.kwargs)); + }); + }); // TODO: non-literal domains and contexts basics // TODO: call_and_eval // TODO: name_search, non-literal domains From 859991dda7922151644719416f83eb8475e7665a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 27 Feb 2012 09:51:28 +0100 Subject: [PATCH 008/238] [ADD] nonliterals in dataset tests bzr revid: xmo@openerp.com-20120227085128-6ymxpa93pr59vjmb --- addons/web/static/src/js/test_support.js | 21 +++++++++-- addons/web/static/test/fulltest/dataset.js | 41 ++++++++++++++++++++-- addons/web/test_support/controllers.py | 14 +++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/test_support.js b/addons/web/static/src/js/test_support.js index 5fcfb94c6a2..d46366326b7 100644 --- a/addons/web/static/src/js/test_support.js +++ b/addons/web/static/src/js/test_support.js @@ -24,15 +24,26 @@ openerp.test_support = { }); return connection.session_reload(); }, - module: function (title, tested_core, fn) { + 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); - openerp.test_support.setup_connection(oe.connection) - .always(QUnit.start) + 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) { @@ -57,6 +68,10 @@ openerp.test_support = { .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); }) diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index d42f1b363d3..14556e9f212 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -1,5 +1,8 @@ $(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) { @@ -14,6 +17,7 @@ $(document).ready(function () { strictEqual(result.args.length, 3); deepEqual(result.args[0], [30]); deepEqual(result.args[1], ['a', 'b', 'c']); + deepEqual(result.args[2], context_()); ok(_.isEmpty(result.kwargs)); }); @@ -27,8 +31,9 @@ $(document).ready(function () { strictEqual(result.args.length, 2); deepEqual(result.args[0], ['a', 'b', 'c']); - // FIXME: args[1] is context w/ user context, where to get? Hardcode? - strictEqual(result.args[1].foo, 'bar'); + console.log(result.args[1]); + console.log(context_({foo: 'bar'})); + deepEqual(result.args[1], context_({foo: 'bar'})); ok(_.isEmpty(result.kwargs)); }); @@ -40,6 +45,7 @@ $(document).ready(function () { strictEqual(r.args.length, 2); deepEqual(r.args[0], {foo: 1, bar: 2}); + deepEqual(r.args[1], context_()); ok(_.isEmpty(r.kwargs)); }); @@ -52,6 +58,7 @@ $(document).ready(function () { strictEqual(r.args.length, 3); deepEqual(r.args[0], [42]); deepEqual(r.args[1], {foo: 1}); + deepEqual(r.args[2], context_()); ok(_.isEmpty(r.kwargs)); }); @@ -68,6 +75,7 @@ $(document).ready(function () { strictEqual(r.args.length, 2); deepEqual(r.args[0], [42]); + deepEqual(r.args[1], context_()); ok(_.isEmpty(r.kwargs)); }); @@ -90,6 +98,7 @@ $(document).ready(function () { strictEqual(r.args.length, 2); deepEqual(r.args[0], [1, 2]); + deepEqual(r.args[1], context_()); ok(_.isEmpty(r.kwargs)); }); @@ -104,6 +113,7 @@ $(document).ready(function () { // domain deepEqual(r.args[1], []); strictEqual(r.args[2], 'ilike'); + deepEqual(r.args[3], context_()); strictEqual(r.args[4], 0); ok(_.isEmpty(r.kwargs)); @@ -119,6 +129,7 @@ $(document).ready(function () { // domain deepEqual(r.args[1], [['foo', '=', 3]]); strictEqual(r.args[2], 'someop'); + deepEqual(r.args[3], context_()); // limit strictEqual(r.args[4], 0); @@ -171,6 +182,32 @@ $(document).ready(function () { 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: ['mary had a little lamb', 'bob the builder'], + 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)); }); diff --git a/addons/web/test_support/controllers.py b/addons/web/test_support/controllers.py index f8e3c7a0739..a3719070730 100644 --- a/addons/web/test_support/controllers.py +++ b/addons/web/test_support/controllers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from ..common.http import Controller, jsonrequest +from ..common import http, nonliterals from ..controllers.main import Session UID = 87539319 @@ -14,6 +14,18 @@ def bind(session): 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' From 910f217da1c60b2d28854c44b5306e21741f769e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 27 Feb 2012 10:41:32 +0100 Subject: [PATCH 009/238] [IMP] update qunit to 1.3 bzr revid: xmo@openerp.com-20120227094132-dk41nad2mx854s4p --- addons/web/static/lib/qunit/qunit.css | 10 ++- addons/web/static/lib/qunit/qunit.js | 119 +++++++++++++++++--------- 2 files changed, 85 insertions(+), 44 deletions(-) mode change 100755 => 100644 addons/web/static/lib/qunit/qunit.css mode change 100755 => 100644 addons/web/static/lib/qunit/qunit.js diff --git a/addons/web/static/lib/qunit/qunit.css b/addons/web/static/lib/qunit/qunit.css old mode 100755 new mode 100644 index bcecc4c0daf..58101ea34ce --- a/addons/web/static/lib/qunit/qunit.css +++ b/addons/web/static/lib/qunit/qunit.css @@ -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; } diff --git a/addons/web/static/lib/qunit/qunit.js b/addons/web/static/lib/qunit/qunit.js old mode 100755 new mode 100644 index 6d2a8a7b8ab..b71381313c7 --- a/addons/web/static/lib/qunit/qunit.js +++ b/addons/web/static/lib/qunit/qunit.js @@ -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 = '' + testName + '', testEnvironmentArg; + var name = '' + escapeInnerText(testName) + ''; 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 = '' + config.currentModule + ": " + 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 = + '

    ' + escapeInnerText( document.title ) + '

    ' + + '

    ' + + '
    ' + + '

    ' + + '
      '; + } + 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 = '' + message + ""; - expected = escapeInnerText(QUnit.jsDump.parse(expected)); - actual = escapeInnerText(QUnit.jsDump.parse(actual)); - var output = message + ''; - if (actual != expected) { - output += ''; - output += ''; - } + var output = message; if (!result) { + expected = escapeInnerText(QUnit.jsDump.parse(expected)); + actual = escapeInnerText(QUnit.jsDump.parse(actual)); + output += '
      Expected:
      ' + expected + '
      Result:
      ' + actual + '
      Diff:
      ' + QUnit.diff(expected, actual) +'
      '; + if (actual != expected) { + output += ''; + output += ''; + } var source = sourceFromStacktrace(); if (source) { details.source = source; output += ''; } + output += "
      Expected:
      ' + expected + '
      Result:
      ' + actual + '
      Diff:
      ' + QUnit.diff(expected, actual) +'
      Source:
      ' + escapeInnerText(source) + '
      "; } - output += ""; 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() ); From db670cff7aa3f946fb0fd6c1b2cdeaf16c667f55 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 27 Feb 2012 11:26:05 +0100 Subject: [PATCH 010/238] Nonliteral domains, name_search w/ nonliteral domains bzr revid: xmo@openerp.com-20120227102605-wvn1osn1wxgbdiio --- addons/web/static/test/fulltest/dataset.js | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index 14556e9f212..4924e3b9c78 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -189,7 +189,10 @@ $(document).ready(function () { }); t.module('Nonliterals', 'data', { - domains: ['mary had a little lamb', 'bob the builder'], + domains: [ + "[('model_id', '=', parent.model)]", + "[('product_id','=',product_id)]" + ], contexts: ['{a: b > c}'] }); t.test('Dataset', function (openerp) { @@ -212,6 +215,32 @@ $(document).ready(function () { 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, 5); + strictEqual(r.args[0], 'foo'); + deepEqual(r.args[1], [['model_id', '=', 'qux']]); + strictEqual(r.args[2], 'ilike'); + deepEqual(r.args[3], context_()); + strictEqual(r.args[4], 0); + + ok(_.isEmpty(r.kwargs)); + }); + }); // TODO: non-literal domains and contexts basics // TODO: call_and_eval // TODO: name_search, non-literal domains From b8efd17bbb552f5516746a6edf27c509e89313e5 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 27 Feb 2012 11:28:20 +0100 Subject: [PATCH 011/238] [REM] todos bzr revid: xmo@openerp.com-20120227102820-t5hmrrsbcd5pcw61 --- addons/web/static/test/fulltest/dataset.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index 4924e3b9c78..e9513b00b74 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -241,8 +241,4 @@ $(document).ready(function () { ok(_.isEmpty(r.kwargs)); }); }); - // TODO: non-literal domains and contexts basics - // TODO: call_and_eval - // TODO: name_search, non-literal domains - }); From e6f5d4c211e00ad8ce5f6cae24a65875c2bccf7c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 27 Feb 2012 14:56:26 +0100 Subject: [PATCH 012/238] [ADD] Model API, reimplement DataSet/DataSetSearch on top of it (as much as possible) TODO: traversal state API, removing even more method (e.g. completely remove DataSet.call in the Python API) bzr revid: xmo@openerp.com-20120227135626-yxqh0gc6jwrdkshs --- addons/web/controllers/main.py | 73 +---- addons/web/static/src/js/core.js | 2 +- addons/web/static/src/js/data.js | 349 +++++++++++++-------- addons/web/static/test/fulltest/dataset.js | 103 +++--- doc/source/changelog-6.2.rst | 37 +++ doc/source/index.rst | 11 + doc/source/rpc.rst | 141 +++++++++ 7 files changed, 468 insertions(+), 248 deletions(-) create mode 100644 doc/source/changelog-6.2.rst create mode 100644 doc/source/rpc.rst diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index eaf3d671874..6a143d58231 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -819,11 +819,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) @@ -859,7 +854,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] } @@ -867,46 +861,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) @@ -916,23 +874,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) @@ -1008,19 +949,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" diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index 10f3167b330..b07800203b2 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -452,7 +452,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. * setting the correct session id and session context in the parameter * objects * - * @param {String} url RPC endpoint + * @param {Object} url RPC endpoint * @param {Object} params call parameters * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 8e200951eaa..58e491cc52c 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -9,15 +9,167 @@ openerp.web.data = function(openerp) { * @returns {String} SQL-like sorting string (``ORDER BY``) clause */ openerp.web.serialize_sort = function (criterion) { - return _.map(criterion, - function (criteria) { - if (criteria[0] === '-') { - return criteria.slice(1) + ' DESC'; - } - return criteria + ' ASC'; - }).join(', '); + return _.map(criterion, + function (criteria) { + if (criteria[0] === '-') { + return criteria.slice(1) + ' DESC'; + } + return criteria + ' ASC'; + }).join(', '); }; +openerp.web.Query = openerp.web.Class.extend({ + init: function (model, fields) { + this._model = model; + this._fields = fields; + this._filter = []; + this._context = {}; + this._limit = false; + this._offset = 0; + this._order_by = []; + }, + clone: function (to_set) { + to_set = to_set || {}; + var q = new openerp.web.Query(this._model, this._fields); + q._context = this._context; + q._filter = this._filter; + q._limit = this._limit; + q._offset = this._offset; + q._order_by = this._order_by; + + for(var key in to_set) { + if (!to_set.hasOwnProperty(key)) { continue; } + switch(key) { + case 'filter': + q._filter = new openerp.web.CompoundDomain( + q._filter, to_set.filter); + break; + case 'context': + q._context = new openerp.web.CompoundContext( + q._context, to_set.context); + break; + case 'limit': + case 'offset': + case 'order_by': + q['_' + key] = to_set[key]; + } + } + return q; + }, + _execute: function () { + var self = this; + return openerp.connection.rpc('/web/dataset/search_read', { + model: this._model.name, + fields: this._fields || false, + domain: this._model.domain(this._filter), + context: this._model.context(this._context), + offset: this._offset, + limit: this._limit, + sort: openerp.web.serialize_sort(this._order_by) + }).pipe(function (results) { + self._count = results.length; + return results.records; + }, null); + }, + first: function () { + var self = this; + return this.clone({limit: 1})._execute().pipe(function (records) { + delete self._count; + if (records.length) { return records[0]; } + return null; + }); + }, + all: function () { + return this._execute(); + }, + context: function (context) { + if (!context) { return this; } + return this.clone({context: context}); + }, + count: function () { + if (this._count) { return $.when(this._count); } + return this.model.call( + 'search_count', [this._filter], { + context: this._model.context(this._context)}); + }, + filter: function (domain) { + if (!domain) { return this; } + return this.clone({filter: domain}); + }, + limit: function (limit) { + return this.clone({limit: limit}); + }, + offset: function (offset) { + return this.clone({offset: offset}); + }, + order_by: function () { + if (arguments.length === 0) { return this; } + return this.clone({order_by: _.toArray(arguments)}); + } +}); + +openerp.web.Model = openerp.web.CallbackEnabled.extend({ + init: function (model_name, context, domain) { + this._super(); + this.name = model_name; + this._context = context || {}; + this._domain = domain || []; + }, + /* + * @deprecated does not allow to specify kwargs, directly use call() instead + */ + get_func: function (method_name) { + var self = this; + return function () { + return self.call(method_name, _.toArray(arguments)); + }; + }, + call: function (method, args, kwargs) { + args = args || []; + kwargs = kwargs || {}; + return openerp.connection.rpc('/web/dataset/call_kw', { + model: this.name, + method: method, + args: args, + kwargs: kwargs + }); + }, + exec_workflow: function (id, signal) { + return openerp.connection.rpc('/web/dataset/exec_workflow', { + model: this.name, + id: id, + signal: signal + }); + }, + query: function (fields) { + return new openerp.web.Query(this, fields); + }, + domain: function (domain) { + return new openerp.web.CompoundDomain( + this._domain, domain || []); + }, + context: function (context) { + return new openerp.web.CompoundContext( + openerp.connection.user_context, this._context, context || {}); + }, + /** + * Button action caller, needs to perform cleanup if an action is returned + * from the button (parsing of context and domain, and fixup of the views + * collection for act_window actions) + * + * FIXME: remove when evaluator integrated + */ + call_button: function (method, args) { + return this.rpc('/web/dataset/call_button', { + model: this.model, + method: method, + domain_id: null, + context_id: args.length - 1, + args: args || [] + }); + }, +}); + openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP @@ -249,6 +401,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data this.context = context || {}; this.index = null; this._sort = []; + this._model = new openerp.web.Model(model, context); }, previous: function () { this.index -= 1; @@ -296,13 +449,10 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ read_ids: function (ids, fields, options) { - var options = options || {}; - return this.rpc('/web/dataset/get', { - model: this.model, - ids: ids, - fields: fields, - context: this.get_context(options.context) - }); + // TODO: reorder results to match ids list + return this._model.call('read', + [ids, fields || false], + {context: this._model.context(options.context)}); }, /** * Read a slice of the records represented by this DataSet, based on its @@ -315,7 +465,14 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ read_slice: function (fields, options) { - return null; + var self = this; + options = options || {}; + return this._model.query(fields) + .limit(options.limit || false) + .offset(options.offset || 0) + .all().then(function (records) { + self.ids = _(records).pluck('id'); + }); }, /** * Reads the current dataset record (from its index) @@ -325,18 +482,13 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ read_index: function (fields, options) { - var def = $.Deferred(); - if (_.isEmpty(this.ids)) { - def.reject(); - } else { - fields = fields || false; - this.read_ids([this.ids[this.index]], fields, options).then(function(records) { - def.resolve(records[0]); - }, function() { - def.reject.apply(def, arguments); - }); - } - return def.promise(); + options = options || {}; + // not very good + return this._model.query(fields) + .offset(this.index).first().pipe(function (record) { + if (!record) { return $.Deferred().reject().promise(); } + return record; + }); }, /** * Reads default values for the current model @@ -346,12 +498,9 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ default_get: function(fields, options) { - var options = options || {}; - return this.rpc('/web/dataset/default_get', { - model: this.model, - fields: fields, - context: this.get_context(options.context) - }); + options = options || {}; + return this._model.call('default_get', + [fields], {context: this._model.context(options.context)}); }, /** * Creates a new record in db @@ -362,11 +511,10 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ create: function(data, callback, error_callback) { - return this.rpc('/web/dataset/create', { - model: this.model, - data: data, - context: this.get_context() - }, callback, error_callback); + return this._model.call('create', + [data], {context: this._model.context()}) + .pipe(function (r) { return {result: r}; }) + .then(callback, error_callback); }, /** * Saves the provided data in an existing db record @@ -379,12 +527,10 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data */ write: function (id, data, options, callback, error_callback) { options = options || {}; - return this.rpc('/web/dataset/save', { - model: this.model, - id: id, - data: data, - context: this.get_context(options.context) - }, callback, error_callback); + return this._model.call('write', + [[id], data], {context: this._model.context(options.context)}) + .pipe(function (r) { return {result: r}}) + .then(callback, error_callback); }, /** * Deletes an existing record from the database @@ -394,9 +540,9 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @param {Function} error_callback function called in case of deletion error */ unlink: function(ids, callback, error_callback) { - var self = this; - return this.call_and_eval("unlink", [ids, this.get_context()], null, 1, - callback, error_callback); + return this._model.call('unlink', + [ids], {context: this._model.context()}) + .then(callback, error_callback); }, /** * Calls an arbitrary RPC method @@ -408,11 +554,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ call: function (method, args, callback, error_callback) { - return this.rpc('/web/dataset/call', { - model: this.model, - method: method, - args: args || [] - }, callback, error_callback); + return this._model.call(method, args).then(callback, error_callback); }, /** * Calls an arbitrary method, with more crazy @@ -446,13 +588,8 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ call_button: function (method, args, callback, error_callback) { - return this.rpc('/web/dataset/call_button', { - model: this.model, - method: method, - domain_id: null, - context_id: args.length - 1, - args: args || [] - }, callback, error_callback); + return this._model.call_button(method, args) + .then(callback, error_callback); }, /** * Fetches the "readable name" for records, based on intrinsic rules @@ -462,7 +599,9 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ name_get: function(ids, callback) { - return this.call_and_eval('name_get', [ids, this.get_context()], null, 1, callback); + return this._model.call('name_get', + [ids], {context: this._model.context()}) + .then(callback); }, /** * @@ -474,29 +613,30 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ name_search: function (name, domain, operator, limit, callback) { - return this.call_and_eval('name_search', - [name || '', domain || false, operator || 'ilike', this.get_context(), limit || 0], - 1, 3, callback); + return this._model.call('name_search', [], { + name: name || '', + args: domain || false, + operator: operator || 'ilike', + context: this._model.context(), + limit: limit || 0 + }).then(callback); }, /** * @param name * @param callback */ name_create: function(name, callback) { - return this.call_and_eval('name_create', [name, this.get_context()], null, 1, callback); + return this._model.call('name_create', + [name], {context: this._model.context()}) + .then(callback); }, exec_workflow: function (id, signal, callback) { - return this.rpc('/web/dataset/exec_workflow', { - model: this.model, - id: id, - signal: signal - }, callback); + return this._model.exec_workflow(id, signal) + .pipe(function (result) { return { result: result }; }) + .then(callback); }, get_context: function(request_context) { - if (request_context) { - return new openerp.web.CompoundContext(this.context, request_context); - } - return this.context; + return this._model.context(request_context); }, /** * Reads or changes sort criteria on the dataset. @@ -573,11 +713,9 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D init: function(parent, model, context, domain) { this._super(parent, model, context); this.domain = domain || []; - this.offset = 0; - this._length; - // subset records[offset:offset+limit] - // is it necessary ? + this._length = null; this.ids = []; + this._model = new openerp.web.Model(model, context, domain); }, /** * Read a slice of the records represented by this DataSet, based on its @@ -594,27 +732,20 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D read_slice: function (fields, options) { options = options || {}; var self = this; - var offset = options.offset || 0; - return this.rpc('/web/dataset/search_read', { - model: this.model, - fields: fields || false, - domain: this.get_domain(options.domain), - context: this.get_context(options.context), - sort: this.sort(), - offset: offset, - limit: options.limit || false - }).pipe(function (result) { - self.ids = result.ids; - self.offset = offset; - self._length = result.length; - return result.records; + var q = this._model.query(fields || false) + .filter(options.domain) + .context(options.context) + .offset(options.offset || 0) + .limit(options.limit || false); + q = q.order_by.apply(q, this._sort); + return q.all().then(function (records) { + // FIXME: not sure about that one, *could* have discarded count + q.count().then(function (count) { this._length = count; }); + self.ids = _(records).pluck('id'); }); }, get_domain: function (other_domain) { - if (other_domain) { - return new openerp.web.CompoundDomain(this.domain, other_domain); - } - return this.domain; + this._model.domain(other_domain); }, unlink: function(ids, callback, error_callback) { var self = this; @@ -841,34 +972,6 @@ openerp.web.ProxyDataSet = openerp.web.DataSetSearch.extend({ on_unlink: function(ids) {} }); -openerp.web.Model = openerp.web.CallbackEnabled.extend({ - init: function(model_name) { - this._super(); - this.model_name = model_name; - }, - rpc: function() { - var c = openerp.connection; - return c.rpc.apply(c, arguments); - }, - /* - * deprecated because it does not allow to specify kwargs, directly use call() instead - */ - get_func: function(method_name) { - var self = this; - return function() { - return self.call(method_name, _.toArray(arguments), {}); - }; - }, - call: function (method, args, kwargs) { - return this.rpc('/web/dataset/call_kw', { - model: this.model_name, - method: method, - args: args, - kwargs: kwargs - }); - } -}); - openerp.web.CompoundContext = openerp.web.Class.extend({ init: function () { this.__ref = "compound_context"; diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index e9513b00b74..1b09bb7dc79 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -11,13 +11,15 @@ $(document).ready(function () { 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.method, 'search'); strictEqual(result.model, 'some.model'); - strictEqual(result.args.length, 3); - deepEqual(result.args[0], [30]); - deepEqual(result.args[1], ['a', 'b', 'c']); - deepEqual(result.args[2], context_()); + strictEqual(result.args.length, 5); + deepEqual(result.args[0], []); + strictEqual(result.args[1], 2); + strictEqual(result.args[2], 1); + strictEqual(result.args[3], false); + deepEqual(result.args[4], context_()); ok(_.isEmpty(result.kwargs)); }); @@ -29,13 +31,12 @@ $(document).ready(function () { strictEqual(result.method, 'default_get'); strictEqual(result.model, 'some.model'); - strictEqual(result.args.length, 2); + strictEqual(result.args.length, 1); deepEqual(result.args[0], ['a', 'b', 'c']); - console.log(result.args[1]); - console.log(context_({foo: 'bar'})); - deepEqual(result.args[1], context_({foo: 'bar'})); - ok(_.isEmpty(result.kwargs)); + deepEqual(result.kwargs, { + context: context_({foo: 'bar'}) + }); }); }); t.test('create', function (openerp) { @@ -43,11 +44,12 @@ $(document).ready(function () { t.expect(ds.create({foo: 1, bar: 2}), function (r) { strictEqual(r.method, 'create'); - strictEqual(r.args.length, 2); + strictEqual(r.args.length, 1); deepEqual(r.args[0], {foo: 1, bar: 2}); - deepEqual(r.args[1], context_()); - ok(_.isEmpty(r.kwargs)); + deepEqual(r.kwargs, { + context: context_() + }); }); }); t.test('write', function (openerp) { @@ -55,12 +57,12 @@ $(document).ready(function () { t.expect(ds.write(42, {foo: 1}), function (r) { strictEqual(r.method, 'write'); - strictEqual(r.args.length, 3); + strictEqual(r.args.length, 2); deepEqual(r.args[0], [42]); deepEqual(r.args[1], {foo: 1}); - deepEqual(r.args[2], context_()); - - ok(_.isEmpty(r.kwargs)); + 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) { @@ -73,11 +75,11 @@ $(document).ready(function () { t.expect(ds.unlink([42]), function (r) { strictEqual(r.method, 'unlink'); - strictEqual(r.args.length, 2); + strictEqual(r.args.length, 1); deepEqual(r.args[0], [42]); - deepEqual(r.args[1], context_()); - - ok(_.isEmpty(r.kwargs)); + deepEqual(r.kwargs, { + context: context_() + }); }); }); t.test('call', function (openerp) { @@ -96,11 +98,11 @@ $(document).ready(function () { t.expect(ds.name_get([1, 2], null), function (r) { strictEqual(r.method, 'name_get'); - strictEqual(r.args.length, 2); + strictEqual(r.args.length, 1); deepEqual(r.args[0], [1, 2]); - deepEqual(r.args[1], context_()); - - ok(_.isEmpty(r.kwargs)); + deepEqual(r.kwargs, { + context: context_() + }); }); }); t.test('name_search, name', function (openerp) { @@ -108,15 +110,14 @@ $(document).ready(function () { t.expect(ds.name_search('bob'), function (r) { strictEqual(r.method, 'name_search'); - strictEqual(r.args.length, 5); - strictEqual(r.args[0], 'bob'); - // domain - deepEqual(r.args[1], []); - strictEqual(r.args[2], 'ilike'); - deepEqual(r.args[3], context_()); - strictEqual(r.args[4], 0); - - ok(_.isEmpty(r.kwargs)); + 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) { @@ -124,16 +125,14 @@ $(document).ready(function () { t.expect(ds.name_search(0, [['foo', '=', 3]], 'someop'), function (r) { strictEqual(r.method, 'name_search'); - strictEqual(r.args.length, 5); - strictEqual(r.args[0], ''); - // domain - deepEqual(r.args[1], [['foo', '=', 3]]); - strictEqual(r.args[2], 'someop'); - deepEqual(r.args[3], context_()); - // limit - strictEqual(r.args[4], 0); - - ok(_.isEmpty(r.kwargs)); + strictEqual(r.args.length, 0); + deepEqual(r.kwargs, { + name: '', + args: [['foo', '=', 3]], + operator: 'someop', + context: context_(), + limit: 0 + }); }); }); t.test('exec_workflow', function (openerp) { @@ -231,14 +230,14 @@ $(document).ready(function () { t.expect(ds.name_search('foo', domain, 'ilike', 0), function (r) { strictEqual(r.method, 'name_search'); - strictEqual(r.args.length, 5); - strictEqual(r.args[0], 'foo'); - deepEqual(r.args[1], [['model_id', '=', 'qux']]); - strictEqual(r.args[2], 'ilike'); - deepEqual(r.args[3], context_()); - strictEqual(r.args[4], 0); - - ok(_.isEmpty(r.kwargs)); + strictEqual(r.args.length, 0); + deepEqual(r.kwargs, { + name: 'foo', + args: [['model_id', '=', 'qux']], + operator: 'ilike', + context: context_(), + limit: 0 + }); }); }); }); diff --git a/doc/source/changelog-6.2.rst b/doc/source/changelog-6.2.rst new file mode 100644 index 00000000000..2b492472f0f --- /dev/null +++ b/doc/source/changelog-6.2.rst @@ -0,0 +1,37 @@ +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 ` 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 +~~~~~~~~~~~~~~~ + +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 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. diff --git a/doc/source/index.rst b/doc/source/index.rst index efa4d1ce812..99756633b43 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -8,6 +8,17 @@ Welcome to OpenERP Web's documentation! Contents: +.. toctree:: + :maxdepth: 1 + + changelog-6.2 + + async + rpc + +Older stuff +----------- + .. toctree:: :maxdepth: 2 diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst new file mode 100644 index 00000000000..8ed72e8520b --- /dev/null +++ b/doc/source/rpc.rst @@ -0,0 +1,141 @@ +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 `. 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. + +: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. + +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. + +.. 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 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> + + .. 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 + + .. js:function:: openerp.web.Query.count() + + Fetches the number of records the current + :js:class:`~openerp.web.Query` would retrieve. + + :rtype: Deferred + + 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 + `_: + + * 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. + +Low-level API: RPC calls to Python side +--------------------------------------- + From f26eff28a9a0af862eadcea90bf97bfae6c108f2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 28 Feb 2012 14:03:21 +0100 Subject: [PATCH 013/238] [FIX] typos bzr revid: xmo@openerp.com-20120228130321-rzxy8axbqtnjr1ze --- addons/web/static/src/js/data.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 58e491cc52c..aa760e7df44 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -88,7 +88,7 @@ openerp.web.Query = openerp.web.Class.extend({ }, count: function () { if (this._count) { return $.when(this._count); } - return this.model.call( + return this._model.call( 'search_count', [this._filter], { context: this._model.context(this._context)}); }, @@ -449,6 +449,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ read_ids: function (ids, fields, options) { + options = options || {}; // TODO: reorder results to match ids list return this._model.call('read', [ids, fields || false], @@ -485,6 +486,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data options = options || {}; // not very good return this._model.query(fields) + .context(options.context) .offset(this.index).first().pipe(function (record) { if (!record) { return $.Deferred().reject().promise(); } return record; @@ -738,9 +740,10 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D .offset(options.offset || 0) .limit(options.limit || false); q = q.order_by.apply(q, this._sort); + return q.all().then(function (records) { // FIXME: not sure about that one, *could* have discarded count - q.count().then(function (count) { this._length = count; }); + q.count().then(function (count) { self._length = count; }); self.ids = _(records).pluck('id'); }); }, From 739d1c73637ce9afe8bb732422ed88066568dc38 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 28 Feb 2012 14:28:32 +0100 Subject: [PATCH 014/238] [ADD] basic docstrings to Query and Model bzr revid: xmo@openerp.com-20120228132832-pf16do6611fcwrj1 --- addons/web/static/src/js/data.js | 113 ++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 11 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index aa760e7df44..969f3542908 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -71,6 +71,11 @@ openerp.web.Query = openerp.web.Class.extend({ return results.records; }, null); }, + /** + * Fetches the first record matching the query, or null + * + * @returns {jQuery.Deferred} + */ first: function () { var self = this; return this.clone({limit: 1})._execute().pipe(function (records) { @@ -79,43 +84,95 @@ openerp.web.Query = openerp.web.Class.extend({ return null; }); }, + /** + * Fetches all records matching the query + * + * @returns {jQuery.Deferred>} + */ all: function () { return this._execute(); }, - context: function (context) { - if (!context) { return this; } - return this.clone({context: context}); - }, + /** + * Fetches the number of records matching the query in the database + * + * @returns {jQuery.Deferred} + */ count: function () { if (this._count) { return $.when(this._count); } return this._model.call( 'search_count', [this._filter], { context: this._model.context(this._context)}); }, + /** + * Creates a new query with the union of the current query's context and + * the new context. + * + * @param context context data to add to the query + * @returns {openerp.web.Query} + */ + context: function (context) { + if (!context) { return this; } + return this.clone({context: context}); + }, + /** + * Creates a new query with the union of the current query's filter and + * the new domain. + * + * @param domain domain data to AND with the current query filter + * @returns {openerp.web.Query} + */ filter: function (domain) { if (!domain) { return this; } return this.clone({filter: domain}); }, + /** + * Creates a new query with the provided limit replacing the current + * query's own limit + * + * @param {Number} limit maximum number of records the query should retrieve + * @returns {openerp.web.Query} + */ limit: function (limit) { return this.clone({limit: limit}); }, + /** + * Creates a new query with the provided offset replacing the current + * query's own offset + * + * @param {Number} offset number of records the query should skip before starting its retrieval + * @returns {openerp.web.Query} + */ offset: function (offset) { return this.clone({offset: offset}); }, + /** + * Creates a new query with the provided ordering parameters replacing + * those of the current query + * + * @param {String...} fields ordering clauses + * @returns {openerp.web.Query} + */ order_by: function () { if (arguments.length === 0) { return this; } return this.clone({order_by: _.toArray(arguments)}); } }); -openerp.web.Model = openerp.web.CallbackEnabled.extend({ +openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ + /** + * @constructs openerp.web.Model + * @extends openerp.web.Class + * + * @param {String} model_name name of the OpenERP model this object is bound to + * @param {Object} [context] + * @param {Array} [domain] + */ init: function (model_name, context, domain) { - this._super(); this.name = model_name; this._context = context || {}; this._domain = domain || []; }, - /* + /** * @deprecated does not allow to specify kwargs, directly use call() instead */ get_func: function (method_name) { @@ -124,6 +181,14 @@ openerp.web.Model = openerp.web.CallbackEnabled.extend({ return self.call(method_name, _.toArray(arguments)); }; }, + /** + * Call a method (over RPC) on the bound OpenERP model. + * + * @param {String} method name of the method to call + * @param {Array} [args] positional arguments + * @param {Object} [kwargs] keyword arguments + * @returns {jQuery.Deferred<>} call result + */ call: function (method, args, kwargs) { args = args || []; kwargs = kwargs || {}; @@ -134,6 +199,21 @@ openerp.web.Model = openerp.web.CallbackEnabled.extend({ kwargs: kwargs }); }, + /** + * Fetches a Query instance bound to this model, for searching + * + * @param {Array} [fields] fields to ultimately fetch during the search + * @returns {openerp.web.Query} + */ + query: function (fields) { + return new openerp.web.Query(this, fields); + }, + /** + * Executes a signal on the designated workflow, on the bound OpenERP model + * + * @param {Number} id workflow identifier + * @param {String} signal signal to trigger on the workflow + */ exec_workflow: function (id, signal) { return openerp.connection.rpc('/web/dataset/exec_workflow', { model: this.name, @@ -141,13 +221,24 @@ openerp.web.Model = openerp.web.CallbackEnabled.extend({ signal: signal }); }, - query: function (fields) { - return new openerp.web.Query(this, fields); - }, + /** + * Fetches the model's domain, combined with the provided domain if any + * + * @param {Array} [domain] to combine with the model's internal domain + * @returns The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain + */ domain: function (domain) { + if (!domain) { return this._domain; } return new openerp.web.CompoundDomain( - this._domain, domain || []); + this._domain, domain); }, + /** + * Fetches the combination of the user's context and the domain context, + * combined with the provided context if any + * + * @param {Object} [context] to combine with the model's internal context + * @returns The union of the user's context and the model's internal context, as well as the provided context if any. In that order. + */ context: function (context) { return new openerp.web.CompoundContext( openerp.connection.user_context, this._context, context || {}); From 923e947917c7082b9e583633a45e179af84cb1d2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 29 Feb 2012 15:17:54 +0100 Subject: [PATCH 015/238] [ADD] small Model examples bzr revid: xmo@openerp.com-20120229141754-2mr8b6sm6es2j4wb --- doc/source/changelog-6.2.rst | 4 ++-- doc/source/rpc.rst | 38 ++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/doc/source/changelog-6.2.rst b/doc/source/changelog-6.2.rst index 2b492472f0f..5facb47ac14 100644 --- a/doc/source/changelog-6.2.rst +++ b/doc/source/changelog-6.2.rst @@ -31,7 +31,7 @@ API simplification than shortcuts, or are there due to domain and context evaluation issues in 6.1. - The shortcuts really add little value, and OpenERP Web embeds a - restricted Python evaluator (in javascript) meaning most of the + 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. diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst index 8ed72e8520b..ea426916932 100644 --- a/doc/source/rpc.rst +++ b/doc/source/rpc.rst @@ -27,19 +27,53 @@ class maps ontwo the OpenERP server objects via two primary methods, :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. +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 `, 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. +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 From 9022aae8cb1122df0c9a108b2905e37949bd73b1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 1 Mar 2012 11:21:17 +0100 Subject: [PATCH 016/238] [ADD] core Traverser API There are still questions over how it'll work, and if it can work at all bzr revid: xmo@openerp.com-20120301102117-zxd89ffcvo2n32nw --- addons/web/static/src/js/data.js | 79 ++++++++++++++++++++++++++++++++ doc/source/rpc.rst | 69 ++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 969f3542908..27d4562aafb 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -261,6 +261,85 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ }, }); +openerp.web.Traverser = openerp.web.Class.extend(/** @lends openerp.web.Traverser# */{ + /** + * @constructs openerp.web.Traverser + * @extends openerp.web.Class + * + * @param {openerp.web.Model} model instance this traverser is bound to + */ + init: function (model) { + this._model = model; + this._index = 0; + }, + + /** + * Gets and sets the current index + * + * @param {Number} [idx] + * @returns {Number} current index + */ + index: function (idx) { + if (idx) { this._index = idx; } + return this._index; + }, + /** + * Returns the model this traverser is currently bound to + * + * @returns {openerp.web.Model} + */ + model: function () { + return this._model; + }, + /** + * Fetches the size of the backing model's match + * + * @returns {Deferred} deferred count + */ + size: function () { + return this._model.query().count(); + }, + + /** + * Record at the current index for the collection, fails if there is no + * record at the current index. + * + * @returns {Deferred<>} + */ + current: function (fields) { + return this._model.query(fields).first().pipe(function (record) { + if (record == null) { + return $.Deferred() + .reject('No record at index' + this._index) + .promise(); + } + return record; + }); + }, + next: function (fields) { + var self = this; + this._index++; + return this.size().pipe(function (s) { + if (self._index >= s) { + self._index = 0; + } + return self.current(fields); + }); + }, + previous: function (fields) { + var self = this; + this._index--; + if (this._index < 0) { + return this.size().pipe(function (s) { + self._index = s-1; + return self.current(fields); + }); + } + return this.current(fields); + } + +}); + openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst index ea426916932..1192b28971f 100644 --- a/doc/source/rpc.rst +++ b/doc/source/rpc.rst @@ -170,6 +170,75 @@ around and use them differently/add new specifications on them. (``?`` field) and the inability to "drill down" into relations for sorting. +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 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 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 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 + Low-level API: RPC calls to Python side --------------------------------------- From 8225ef6c995ae302b61020d27c14e7bfbb11109d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 1 Mar 2012 11:30:49 +0100 Subject: [PATCH 017/238] [ADD] small doc for Connection#rpc bzr revid: xmo@openerp.com-20120301103049-u2b208awbv6e67xb --- doc/source/rpc.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst index 1192b28971f..672173f6469 100644 --- a/doc/source/rpc.rst +++ b/doc/source/rpc.rst @@ -242,3 +242,31 @@ asynchronously (and using indexes). 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 + }); + +.. [#] except for ``context``, which is extracted and stored in the + request object itself. From 190eb339faeed6755de7b77b3f0152a5e3c39c8e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 1 Mar 2012 16:23:15 +0100 Subject: [PATCH 018/238] [ADD] replacement for current aborter/abort_last hack: UDP-style handler of mis-ordered requests * By default, ignores (refuses to resolve) mis-ordered request when they come back * Optionally, fails them bzr revid: xmo@openerp.com-20120301152315-67kbkdwpy7cdh25y --- addons/web/static/src/js/data.js | 40 ++++++++++ addons/web/static/test/rpc.js | 130 +++++++++++++++++++++++++++++++ addons/web/static/test/test.html | 1 + 3 files changed, 171 insertions(+) create mode 100644 addons/web/static/test/rpc.js diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 27d4562aafb..55e200a205f 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -1190,6 +1190,46 @@ openerp.web.CompoundDomain = openerp.web.Class.extend({ return this.__eval_context; } }); + +openerp.web.DropMisordered = openerp.web.Class.extend(/** @lends openerp.web.DropMisordered# */{ + /** + * @constructs openerp.web.DropMisordered + * @extends openerp.web.Class + * + * @param {Boolean} [failMisordered=false] whether mis-ordered responses should be failed or just ignored + */ + init: function (failMisordered) { + // local sequence number, for requests sent + this.lsn = 0; + // remote sequence number, seqnum of last received request + this.rsn = -1; + this.failMisordered = failMisordered || false; + }, + /** + * Adds a deferred (usually an async request) to the sequencer + * + * @param {$.Deferred} deferred to ensure add + * @returns {$.Deferred} + */ + add: function (deferred) { + var res = $.Deferred(); + + var self = this, seq = this.lsn++; + deferred.then(function () { + if (seq > self.rsn) { + self.rsn = seq; + res.resolve.apply(res, arguments); + } else if (self.failMisordered) { + res.reject(); + } + }, function () { + res.reject.apply(res, arguments); + }); + + return res.promise(); + } +}); + }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/test/rpc.js b/addons/web/static/test/rpc.js new file mode 100644 index 00000000000..7dbd3239abd --- /dev/null +++ b/addons/web/static/test/rpc.js @@ -0,0 +1,130 @@ +$(document).ready(function () { + var openerp; + + module('Misordered resolution management', { + setup: function () { + openerp = window.openerp.init(); + window.openerp.web.core(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); + }); +}); diff --git a/addons/web/static/test/test.html b/addons/web/static/test/test.html index 10c2a2c0ed2..004ba0479e1 100644 --- a/addons/web/static/test/test.html +++ b/addons/web/static/test/test.html @@ -51,4 +51,5 @@ + From aa7a8ed36a1b923cd1df09a01c43e22c57549572 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 1 Mar 2012 17:04:52 +0100 Subject: [PATCH 019/238] [REN] pointless usages of DataSetStatic (which is all of them) to using DataSet bzr revid: xmo@openerp.com-20120301160452-epmjxma3pslo38ek --- addons/web/static/src/js/view_form.js | 34 ++++++++++----------- addons/web/static/src/js/view_page.js | 2 +- addons/web_process/static/src/js/process.js | 4 +-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 640fff67293..5e9e311bb2d 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1901,7 +1901,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ // context menu var init_context_menu_def = $.Deferred().then(function(e) { - var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context()); + var rdataset = new openerp.web.DataSet(self, "ir.values", self.build_context()); rdataset.call("get", ['action', 'client_action_relate', [[self.field.relation, false]], false, rdataset.get_context()], false, 0) .then(function(result) { @@ -2036,7 +2036,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ this.abort_last(); delete this.abort_last; } - var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context()); + var dataset = new openerp.web.DataSet(this, this.field.relation, self.build_context()); dataset.name_search(search_val, self.build_domain(), 'ilike', this.limit + 1, function(data) { @@ -2089,13 +2089,13 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ 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 openerp.web.DataSetStatic(this, this.field.relation, self.build_context()); - dataset.name_create(name, function(data) { - self._change_int_ext_value(data); - }).fail(function(error, event) { - event.preventDefault(); - slow_create(); - }); + new openerp.web.DataSet(this, this.field.relation, self.build_context()) + .name_create(name, function(data) { + self._change_int_ext_value(data); + }).fail(function(error, event) { + event.preventDefault(); + slow_create(); + }); } else slow_create(); }, @@ -2115,10 +2115,10 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ new openerp.web.CompoundContext(self.build_context(), context || {}) ); pop.on_select_elements.add(function(element_ids) { - var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context()); - dataset.name_get([element_ids[0]], function(data) { - self._change_int_ext_value(data[0]); - }); + new openerp.web.DataSet(self, self.field.relation, self.build_context()) + .name_get([element_ids[0]], function(data) { + self._change_int_ext_value(data[0]); + }); }); }, _change_int_ext_value: function(value) { @@ -2154,10 +2154,10 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ }; if (value && !(value instanceof Array)) { // name_get in a m2o does not use the context of the field - var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); - dataset.name_get([value], function(data) { - real_set_value(data[0]); - }).fail(function() {self.tmp_value = undefined;}); + new openerp.web.DataSet(this, this.field.relation, self.view.dataset.get_context()) + .name_get([value], function(data) { + real_set_value(data[0]); + }).fail(function() {self.tmp_value = undefined;}); } else { $.async_when().then(function() {real_set_value(value);}); } diff --git a/addons/web/static/src/js/view_page.js b/addons/web/static/src/js/view_page.js index 3b436841579..33eabc63b3c 100644 --- a/addons/web/static/src/js/view_page.js +++ b/addons/web/static/src/js/view_page.js @@ -168,7 +168,7 @@ openerp.web.page = function (openerp) { }); }; if (value && !(value instanceof Array)) { - new openerp.web.DataSetStatic( + new openerp.web.DataSet( this, this.field.relation, self.build_context()) .name_get([value], function(data) { real_set_value(data[0]); diff --git a/addons/web_process/static/src/js/process.js b/addons/web_process/static/src/js/process.js index cd30dad1f13..923f9e664f7 100644 --- a/addons/web_process/static/src/js/process.js +++ b/addons/web_process/static/src/js/process.js @@ -107,7 +107,7 @@ openerp.web_process = function (openerp) { if(this.process_id) return def.resolve().promise(); - this.process_dataset = new openerp.web.DataSetStatic(this, "process.process", this.session.context); + this.process_dataset = new openerp.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 (openerp) { }, jump_to_view: function(model, id) { var self = this; - var dataset = new openerp.web.DataSetStatic(this, 'ir.values', this.session.context); + var dataset = new openerp.web.DataSet(this, 'ir.values', this.session.context); dataset.call('get', ['action', 'tree_but_open',[['ir.ui.menu', id]], dataset.context], function(res) { From 1aed1963c0f62c9308d9ad0161f619cd96389bc9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Mar 2012 09:39:41 +0100 Subject: [PATCH 020/238] [REM] horrible hack of a query-aborting API, use DropMisordered instead DropMisordered does not abort late requests, but provides a better and simpler API if 'late' requests (resolved after those following them) should just be ignored bzr revid: xmo@openerp.com-20120302083941-pm43lag22bfac8g4 --- addons/web/static/src/js/core.js | 14 +------------- addons/web/static/src/js/data.js | 4 +--- addons/web/static/src/js/search.js | 28 +++++++++++++-------------- addons/web/static/src/js/view_form.js | 10 +++------- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index b07800203b2..a1d2d04f697 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -476,9 +476,7 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }; var deferred = $.Deferred(); this.on_rpc_request(); - var aborter = params.aborter; - delete params.aborter; - var request = this.rpc_function(url, payload).then( + this.rpc_function(url, payload).then( function (response, textStatus, jqXHR) { self.on_rpc_response(); if (!response.error) { @@ -504,16 +502,6 @@ openerp.web.Connection = openerp.web.CallbackEnabled.extend( /** @lends openerp. }; deferred.reject(error, $.Event()); }); - if (aborter) { - aborter.abort_last = function () { - if (!(request.isResolved() || request.isRejected())) { - deferred.fail(function (error, event) { - event.preventDefault(); - }); - request.abort(); - } - }; - } // Allow deferred user to disable on_rpc_error in fail deferred.fail(function() { deferred.fail(function(error, event) { diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 55e200a205f..2c91d6e4e19 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -745,9 +745,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data method: method, domain_id: domain_index == undefined ? null : domain_index, context_id: context_index == undefined ? null : context_index, - args: args || [], - // FIXME: API which does not suck for aborting requests in-flight - aborter: this + args: args || [] }, callback, error_callback); }, /** diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 74ae65785f9..4871e9d95bb 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1005,8 +1005,9 @@ openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ this.got_name = $.Deferred().then(function () { self.$element.val(self.name); }); - this.dataset = new openerp.web.DataSet( - this.view, this.attrs['relation']); + this.model = new openerp.web.Model(this.attrs['relation']); + + this.orderer = new openerp.web.DropMisordered(); }, start: function () { this._super(); @@ -1020,23 +1021,21 @@ openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ var self = this; this.$element.autocomplete({ source: function (req, resp) { - if (self.abort_last) { - self.abort_last(); - delete self.abort_last; - } - self.dataset.name_search( - req.term, self.attrs.domain, 'ilike', 8, function (data) { - resp(_.map(data, function (result) { - return {id: result[0], label: result[1]} - })); + self.orderer.add(self.model.call('name_search', null, { + name: req.term, + args: self.attrs.domain, + limit: 8 + })).then(function (data) { + resp(_.map(data, function (result) { + return {id: result[0], label: result[1]} + })); }); - self.abort_last = self.dataset.abort_last; }, select: function (event, ui) { self.id = ui.item.id; self.name = ui.item.label; }, - delay: 0 + delay: 100 }) }, on_name_get: function (name_get) { @@ -1055,7 +1054,8 @@ openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ this.id = this.id[0]; // TODO: maybe this should not be completely removed delete defaults[this.attrs.name]; - this.dataset.name_get([this.id], $.proxy(this, 'on_name_get')); + this.model.call('name_get', [[this.id]]).then( + this.proxy('name_get')); } else { this.got_name.reject(); } diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 5e9e311bb2d..2a678bee393 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1891,6 +1891,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ this.cm_id = _.uniqueId('m2o_cm_'); this.last_search = []; this.tmp_value = undefined; + this.orderer = new openerp.web.DropMisordered(); }, start: function() { this._super(); @@ -2032,14 +2033,10 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ var search_val = request.term; var self = this; - if (this.abort_last) { - this.abort_last(); - delete this.abort_last; - } var dataset = new openerp.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) { @@ -2080,7 +2077,6 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ response(values); }); - this.abort_last = dataset.abort_last; }, _quick_create: function(name) { var self = this; From 2c2df2b325020098d5173ec38114096d35aa588b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Mar 2012 11:07:42 +0100 Subject: [PATCH 021/238] [FIX] Model#call not working correctly if the second argument (after method) is the kwargs bzr revid: xmo@openerp.com-20120302100742-p1wrg44ght8d3cdy --- addons/web/static/src/js/data.js | 7 ++++++- doc/source/rpc.rst | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 2c91d6e4e19..544469c3f9c 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -192,6 +192,11 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ call: function (method, args, kwargs) { args = args || []; kwargs = kwargs || {}; + if (!_.isArray(args)) { + // call(method, kwargs) + kwargs = args; + args = []; + } return openerp.connection.rpc('/web/dataset/call_kw', { model: this.name, method: method, @@ -783,7 +788,7 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data * @returns {$.Deferred} */ name_search: function (name, domain, operator, limit, callback) { - return this._model.call('name_search', [], { + return this._model.call('name_search', { name: name || '', args: domain || false, operator: operator || 'ilike', diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst index 672173f6469..5dea140c798 100644 --- a/doc/source/rpc.rst +++ b/doc/source/rpc.rst @@ -80,7 +80,7 @@ around and use them differently/add new specifications on them. name of the OpenERP model this object is bound to - .. js:function:: openerp.web.Model.call(method, args, kwargs) + .. js:function:: openerp.web.Model.call(method[, args][, kwargs]) Calls the ``method`` method of the current model, with the provided positional and keyword arguments. From 9b0cc92b66189ed5c21e12c830dae89e29e8f51e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Mar 2012 16:47:59 +0100 Subject: [PATCH 022/238] [ADD] grouping to the Model/Query API, but filtering looks completely broken bzr revid: xmo@openerp.com-20120302154759-8ihi5p1ffygiyhw3 --- addons/web/static/src/js/data.js | 303 ++++++++++++++----------------- doc/source/changelog-6.2.rst | 71 ++++++++ doc/source/rpc.rst | 73 ++++++++ 3 files changed, 277 insertions(+), 170 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 544469c3f9c..cfe5a312c03 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -9,13 +9,13 @@ openerp.web.data = function(openerp) { * @returns {String} SQL-like sorting string (``ORDER BY``) clause */ openerp.web.serialize_sort = function (criterion) { - return _.map(criterion, - function (criteria) { - if (criteria[0] === '-') { - return criteria.slice(1) + ' DESC'; - } - return criteria + ' ASC'; - }).join(', '); + return _.map(criterion, + function (criteria) { + if (criteria[0] === '-') { + return criteria.slice(1) + ' DESC'; + } + return criteria + ' ASC'; + }).join(', '); }; openerp.web.Query = openerp.web.Class.extend({ @@ -103,6 +103,38 @@ openerp.web.Query = openerp.web.Class.extend({ 'search_count', [this._filter], { context: this._model.context(this._context)}); }, + /** + * Performs a groups read according to the provided grouping criterion + * + * @param {String|Array} grouping + * @returns {jQuery.Deferred> | null} + */ + group_by: function (grouping) { + if (grouping === undefined) { + return null; + } + + if (!(grouping instanceof Array)) { + grouping = _.toArray(arguments); + } + if (_.isEmpty(grouping)) { return null; } + + var self = this; + return this._model.call('read_group', { + groupby: grouping, + fields: _.uniq(grouping.concat(this._fields || [])), + domain: this._model.domain(this._filter), + context: this._model.context(this._context), + offset: this._offset, + limit: this._limit, + orderby: openerp.web.serialize_sort(this._order_by) || false + }).pipe(function (results) { + return _(results).map(function (result) { + return new openerp.web.data.Group( + self._model.name, grouping[0], result); + }); + }); + }, /** * Creates a new query with the union of the current query's context and * the new context. @@ -152,9 +184,12 @@ openerp.web.Query = openerp.web.Class.extend({ * @param {String...} fields ordering clauses * @returns {openerp.web.Query} */ - order_by: function () { - if (arguments.length === 0) { return this; } - return this.clone({order_by: _.toArray(arguments)}); + order_by: function (fields) { + if (!fields instanceof Array) { + fields = _.toArray(arguments); + } + if (_.isEmpty(fields)) { return this; } + return this.clone({order_by: fields}); } }); @@ -345,6 +380,64 @@ openerp.web.Traverser = openerp.web.Class.extend(/** @lends openerp.web.Traverse }); +/** + * Utility objects, should never need to be instantiated from outside of this + * module + * + * @namespace + */ +openerp.web.data = { + Group: openerp.web.Class.extend(/** @lends openerp.web.data.Group# */{ + /** + * @constructs openerp.web.data.Group + * @extends openerp.web.Class + */ + init: function (model, grouping_field, read_group_group) { + // In cases where group_by_no_leaf and no group_by, the result of + // read_group has aggregate fields but no __context or __domain. + // Create default (empty) values for those so that things don't break + var fixed_group = _.extend( + {__context: {group_by: []}, __domain: []}, + read_group_group); + + var aggregates = {}; + _(fixed_group).each(function (value, key) { + if (key.indexOf('__') === 0 + || key === grouping_field + || key === grouping_field + '_count') { + return; + } + aggregates[key] = value || 0; + }); + + this.model = new openerp.web.Model( + model, fixed_group.__context, fixed_group.__domain); + + var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0; + var leaf_group = fixed_group.__context.group_by.length === 0; + this.attributes = { + grouped_on: grouping_field, + // if terminal group (or no group) and group_by_no_leaf => use group.__count + length: group_size, + value: fixed_group[grouping_field], + // A group is open-able if it's not a leaf in group_by_no_leaf mode + has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']), + + aggregates: aggregates + }; + }, + get: function (key) { + return this.attributes[key]; + }, + subgroups: function () { + return this.model.query().group_by(this.model.context().group_by); + }, + query: function () { + return this.model.query.apply(this.model, arguments); + } + }) +}; + openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ /** * Management interface between views and grouped collections of OpenERP @@ -368,181 +461,51 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da */ init: function(parent, model, domain, context, group_by, level) { this._super(parent, null); - if (group_by) { - if (group_by.length || context['group_by_no_leaf']) { - return new openerp.web.ContainerDataGroup( this, model, domain, context, group_by, level); - } else { - return new openerp.web.GrouplessDataGroup( this, model, domain, context, level); - } - } - - this.model = model; + this.model = new openerp.web.Model(model, context, domain); + this.group_by = group_by; this.context = context; this.domain = domain; this.level = level || 0; }, - cls: 'DataGroup' -}); -openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.ContainerDataGroup# */ { - /** - * - * @constructs openerp.web.ContainerDataGroup - * @extends openerp.web.DataGroup - * - * @param session - * @param model - * @param domain - * @param context - * @param group_by - * @param level - */ - init: function (parent, model, domain, context, group_by, level) { - this._super(parent, model, domain, context, null, level); - - this.group_by = group_by; - }, - /** - * The format returned by ``read_group`` is absolutely dreadful: - * - * * A ``__context`` key provides future grouping levels - * * A ``__domain`` key provides the domain for the next search - * * The current grouping value is provided through the name of the - * current grouping name e.g. if currently grouping on ``user_id``, then - * the ``user_id`` value for this group will be provided through the - * ``user_id`` key. - * * Similarly, the number of items in the group (not necessarily direct) - * is provided via ``${current_field}_count`` - * * Other aggregate fields are just dumped there - * - * This function slightly improves the grouping records by: - * - * * Adding a ``grouped_on`` property providing the current grouping field - * * Adding a ``value`` and a ``length`` properties which replace the - * ``$current_field`` and ``${current_field}_count`` ones - * * Moving aggregate values into an ``aggregates`` property object - * - * Context and domain keys remain as-is, they should not be used externally - * but in case they're needed... - * - * @param {Object} group ``read_group`` record - */ - transform_group: function (group) { - var field_name = this.group_by[0]; - // In cases where group_by_no_leaf and no group_by, the result of - // read_group has aggregate fields but no __context or __domain. - // Create default (empty) values for those so that things don't break - var fixed_group = _.extend( - {__context: {group_by: []}, __domain: []}, - group); - - var aggregates = {}; - _(fixed_group).each(function (value, key) { - if (key.indexOf('__') === 0 - || key === field_name - || key === field_name + '_count') { - return; - } - aggregates[key] = value || 0; - }); - - var group_size = fixed_group[field_name + '_count'] || fixed_group.__count || 0; - var leaf_group = fixed_group.__context.group_by.length === 0; - return { - __context: fixed_group.__context, - __domain: fixed_group.__domain, - - grouped_on: field_name, - // if terminal group (or no group) and group_by_no_leaf => use group.__count - length: group_size, - value: fixed_group[field_name], - // A group is openable if it's not a leaf in group_by_no_leaf mode - openable: !(leaf_group && this.context['group_by_no_leaf']), - - aggregates: aggregates - }; - }, - fetch: function (fields) { - // internal method - var d = new $.Deferred(); - var self = this; - - this.rpc('/web/group/read', { - model: this.model, - context: this.context, - domain: this.domain, - fields: _.uniq(this.group_by.concat(fields)), - group_by_fields: this.group_by, - sort: openerp.web.serialize_sort(this.sort) - }, function () { }).then(function (response) { - var data_groups = _(response).map( - _.bind(self.transform_group, self)); - self.groups = data_groups; - d.resolveWith(self, [data_groups]); - }, function () { - d.rejectWith.apply(d, [self, arguments]); - }); - return d.promise(); - }, - /** - * The items of a list have the following properties: - * - * ``length`` - * the number of records contained in the group (and all of its - * sub-groups). This does *not* provide the size of the "next level" - * of the group, unless the group is terminal (no more groups within - * it). - * ``grouped_on`` - * the name of the field this level was grouped on, this is mostly - * used for display purposes, in order to know the name of the current - * level of grouping. The ``grouped_on`` should be the same for all - * objects of the list. - * ``value`` - * the value which led to this group (this is the value all contained - * records have for the current ``grouped_on`` field name). - * ``aggregates`` - * a mapping of other aggregation fields provided by ``read_group`` - * - * @param {Array} fields the list of fields to aggregate in each group, can be empty - * @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters. - * @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter - */ list: function (fields, ifGroups, ifRecords) { var self = this; - this.fetch(fields).then(function (group_records) { - ifGroups(_(group_records).map(function (group) { - var child_context = _.extend({}, self.context, group.__context); + $.when(this.model.query(fields) + .order_by(this.sort) + .group_by(this.group_by)).then(function (groups) { + if (!groups) { + console.log(self.domain); + console.log(self.model.domain()); + ifRecords(_.extend( + new openerp.web.DataSetSearch(self, self.model.name), + {domain: self.model.domain(), context: self.model.context(), + _sort: self.sort})); + return; + } + ifGroups(_(groups).map(function (group) { + var child_context = _.extend( + {}, self.model.context(), group.model.context()); return _.extend( new openerp.web.DataGroup( - self, self.model, group.__domain, + self, self.model.name, group.model.domain(), child_context, child_context.group_by, self.level + 1), - group, {sort: self.sort}); + { + __context: child_context, + __domain: group.model.domain(), + grouped_on: group.get('grouped_on'), + length: group.get('length'), + value: group.get('value'), + openable: group.get('has_children'), + aggregates: group.get('aggregates') + }, {sort: self.sort}); })); }); } }); -openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.GrouplessDataGroup# */ { - /** - * - * @constructs openerp.web.GrouplessDataGroup - * @extends openerp.web.DataGroup - * - * @param session - * @param model - * @param domain - * @param context - * @param level - */ - init: function (parent, model, domain, context, level) { - this._super(parent, model, domain, context, null, level); - }, - list: function (fields, ifGroups, ifRecords) { - ifRecords(_.extend( - new openerp.web.DataSetSearch(this, this.model), - {domain: this.domain, context: this.context, _sort: this.sort})); - } -}); +openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend({ }); +openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend({ }); + openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ { /** * A specialization of groupless data groups, relying on a single static diff --git a/doc/source/changelog-6.2.rst b/doc/source/changelog-6.2.rst index 5facb47ac14..48d79bf6398 100644 --- a/doc/source/changelog-6.2.rst +++ b/doc/source/changelog-6.2.rst @@ -12,6 +12,51 @@ 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 ~~~~~~~~~ @@ -35,3 +80,29 @@ API simplification 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. diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst index 5dea140c798..9f8f4424ce6 100644 --- a/doc/source/rpc.rst +++ b/doc/source/rpc.rst @@ -128,6 +128,17 @@ around and use them differently/add new specifications on them. :rtype: Deferred + .. js:function:: openerp.web.Query.group_by(grouping...) + + Fetches the groups for the query, using the first specified + grouping parameter + + :param Array grouping: Lists the levels of grouping + asked of the server. Grouping + can actually be an array or + varargs. + :rtype: Deferred> | 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. @@ -170,6 +181,65 @@ around and use them differently/add new specifications on them. (``?`` 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 +`_ [#]_: + +.. 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) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -268,5 +338,8 @@ For instance, to call the ``eval_domain_and_context`` of the // 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. From c1ec7b1938fbd1a6b09d0172752d97197031bae1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Mar 2012 16:53:24 +0100 Subject: [PATCH 023/238] [REM] debug logging bzr revid: xmo@openerp.com-20120302155324-v3ju1lcioiw2s9el --- addons/web/static/src/js/data.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index cfe5a312c03..783eb979013 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -474,8 +474,6 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da .order_by(this.sort) .group_by(this.group_by)).then(function (groups) { if (!groups) { - console.log(self.domain); - console.log(self.model.domain()); ifRecords(_.extend( new openerp.web.DataSetSearch(self, self.model.name), {domain: self.model.domain(), context: self.model.context(), From 79f79deec19fabba3400b64161b6e49c6548c14e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 15:50:29 +0100 Subject: [PATCH 024/238] [FIX] "bzr cdiff" operator binds tighter than "instanceof", remember to parenthesize "instanceof" calls before negating them bzr revid: xmo@openerp.com-20120306145029-pckye3yoig2xlhh8 --- addons/web/static/src/js/data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 783eb979013..0c99e05717f 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -185,7 +185,7 @@ openerp.web.Query = openerp.web.Class.extend({ * @returns {openerp.web.Query} */ order_by: function (fields) { - if (!fields instanceof Array) { + if (!(fields instanceof Array)) { fields = _.toArray(arguments); } if (_.isEmpty(fields)) { return this; } From 14657305314d964df4f96b28b9dd806f4c4681c2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 15:57:41 +0100 Subject: [PATCH 025/238] [FIX] call_button implementation in model bzr revid: xmo@openerp.com-20120306145741-wi9xk2c4e18fab2z --- addons/web/static/src/js/data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 0c99e05717f..ac8f5fc5665 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -291,8 +291,8 @@ openerp.web.Model = openerp.web.Class.extend(/** @lends openerp.web.Model# */{ * FIXME: remove when evaluator integrated */ call_button: function (method, args) { - return this.rpc('/web/dataset/call_button', { - model: this.model, + return openerp.connection.rpc('/web/dataset/call_button', { + model: this.name, method: method, domain_id: null, context_id: args.length - 1, From b48fddcc8114b01cd1950d02e599c5db0892809b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 16:09:45 +0100 Subject: [PATCH 026/238] [IMP] make Query#order_by more resilient to somebody passing 'undefined' bzr revid: xmo@openerp.com-20120306150945-86358u7atgyevb52 --- addons/web/static/src/js/data.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index ac8f5fc5665..4cb4a8b6d4b 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -185,6 +185,7 @@ openerp.web.Query = openerp.web.Class.extend({ * @returns {openerp.web.Query} */ order_by: function (fields) { + if (fields === undefined) { return this; } if (!(fields instanceof Array)) { fields = _.toArray(arguments); } From 2864ffb5ae23fb3adaf4a24f04f4bf17f21cbc7c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 16:55:50 +0100 Subject: [PATCH 027/238] [FIX] DataSet created in/from datagroups bzr revid: xmo@openerp.com-20120306155550-be04x4q21pt58t5h --- addons/web/static/src/js/data.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 4cb4a8b6d4b..0b57ac26440 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -476,9 +476,11 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da .group_by(this.group_by)).then(function (groups) { if (!groups) { ifRecords(_.extend( - new openerp.web.DataSetSearch(self, self.model.name), - {domain: self.model.domain(), context: self.model.context(), - _sort: self.sort})); + new openerp.web.DataSetSearch( + self, self.model.name, + self.model.context(), + self.model.domain()), + {_sort: self.sort})); return; } ifGroups(_(groups).map(function (group) { From 075ac184f8989c4abcdd36851fe89881e92508cc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 17:05:58 +0100 Subject: [PATCH 028/238] [FIX] recursive grouping bzr revid: xmo@openerp.com-20120306160558-sq5r7a4d7ks4g33b --- addons/web/static/src/js/data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 0b57ac26440..8e9ea1d3088 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -489,7 +489,7 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da return _.extend( new openerp.web.DataGroup( self, self.model.name, group.model.domain(), - child_context, child_context.group_by, + child_context, group.model._context.group_by, self.level + 1), { __context: child_context, From 8b13a999675d053d6bb902ab0880eec05bbc9a8d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 17:13:58 +0100 Subject: [PATCH 029/238] [FIX] handling of _count in queries: it can be 0, which is valid but falsy bzr revid: xmo@openerp.com-20120306161358-py6zz2bts9b3cb1x --- addons/web/static/src/js/data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 8e9ea1d3088..c517dd53394 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -98,7 +98,7 @@ openerp.web.Query = openerp.web.Class.extend({ * @returns {jQuery.Deferred} */ count: function () { - if (this._count) { return $.when(this._count); } + if (this._count != undefined) { return $.when(this._count); } return this._model.call( 'search_count', [this._filter], { context: this._model.context(this._context)}); From d05add0c7dc467e56b7fc7b6f8001a1b1f5e0676 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 17:38:41 +0100 Subject: [PATCH 030/238] [FIX] length management in dataset when items are removed from list view bzr revid: xmo@openerp.com-20120306163841-4ko7el1t54xwguxr --- addons/web/static/src/js/data.js | 5 ++++- addons/web/static/src/js/view_list.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index c517dd53394..0dc60c0e031 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -890,7 +890,10 @@ openerp.web.DataSetSearch = openerp.web.DataSet.extend(/** @lends openerp.web.D unlink: function(ids, callback, error_callback) { var self = this; return this._super(ids, function(result) { - self.ids = _.without.apply(_, [self.ids].concat(ids)); + self.ids = _(self.ids).difference(ids); + if (self._length) { + self._length -= 1; + } if (this.index !== null) { self.index = self.index <= self.ids.length - 1 ? self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index eb13bbd927b..8d8e342370e 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -291,6 +291,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# */ configure_pager: function (dataset) { this.dataset.ids = dataset.ids; + this.dataset._length = dataset._length; var limit = this.limit(), total = dataset.size(), From 83221bd1f1be436fa5cddaac6edf4e3ebfa64dfa Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 6 Mar 2012 17:49:28 +0100 Subject: [PATCH 031/238] [FIX] typo in search view's m2o field bzr revid: xmo@openerp.com-20120306164928-j50gshmmui6fn01t --- addons/web/static/src/js/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 4871e9d95bb..b1ee33c4125 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1055,7 +1055,7 @@ openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({ // TODO: maybe this should not be completely removed delete defaults[this.attrs.name]; this.model.call('name_get', [[this.id]]).then( - this.proxy('name_get')); + this.proxy('on_name_get')); } else { this.got_name.reject(); } From 514c63288c7d5d88d764b44696c5e3a5fc148dfb Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 22 Mar 2012 15:25:40 +0100 Subject: [PATCH 032/238] [FIX] change dataset.read_index to be implemented in terms of dataset.read_ids, not in terms of model.first() as not all model data is kept/transferred (filters are not bubbled?) bzr revid: xmo@openerp.com-20120322142540-x3j9p7kub2px6p6v --- addons/web/static/src/js/data.js | 9 +++------ addons/web/static/test/fulltest/dataset.js | 14 ++++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 0dc60c0e031..b3ed5b08280 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -623,12 +623,9 @@ openerp.web.DataSet = openerp.web.OldWidget.extend( /** @lends openerp.web.Data */ read_index: function (fields, options) { options = options || {}; - // not very good - return this._model.query(fields) - .context(options.context) - .offset(this.index).first().pipe(function (record) { - if (!record) { return $.Deferred().reject().promise(); } - return record; + return this.read_ids([this.ids[this.index]], fields, options).pipe(function (records) { + if (_.isEmpty(records)) { return $.Deferred().reject().promise(); } + return records[0]; }); }, /** diff --git a/addons/web/static/test/fulltest/dataset.js b/addons/web/static/test/fulltest/dataset.js index 1b09bb7dc79..9f7ab101869 100644 --- a/addons/web/static/test/fulltest/dataset.js +++ b/addons/web/static/test/fulltest/dataset.js @@ -11,17 +11,15 @@ $(document).ready(function () { ds.ids = [10, 20, 30, 40, 50]; ds.index = 2; t.expect(ds.read_index(['a', 'b', 'c']), function (result) { - strictEqual(result.method, 'search'); + strictEqual(result.method, 'read'); strictEqual(result.model, 'some.model'); - strictEqual(result.args.length, 5); - deepEqual(result.args[0], []); - strictEqual(result.args[1], 2); - strictEqual(result.args[2], 1); - strictEqual(result.args[3], false); - deepEqual(result.args[4], context_()); + strictEqual(result.args.length, 2); + deepEqual(result.args[0], [30]); - ok(_.isEmpty(result.kwargs)); + deepEqual(result.kwargs, { + context: context_() + }); }); }); t.test('default_get', function (openerp) { From 12381be01045d64498616f3f2697f0162edbe0c1 Mon Sep 17 00:00:00 2001 From: "Turkesh Patel (Open ERP)" Date: Fri, 23 Mar 2012 15:00:13 +0530 Subject: [PATCH 033/238] [FIX] account_voucher: remove pay button if state is posted. bzr revid: tpa@tinyerp.com-20120323093013-3x546uunrbe5t0nc --- addons/account_voucher/voucher_sales_purchase_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account_voucher/voucher_sales_purchase_view.xml b/addons/account_voucher/voucher_sales_purchase_view.xml index 0cb3104f92e..5b3965ad59b 100644 --- a/addons/account_voucher/voucher_sales_purchase_view.xml +++ b/addons/account_voucher/voucher_sales_purchase_view.xml @@ -153,7 +153,7 @@