From 9f6af7c9747d8f85e3296d3044f13b735834dc70 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 21 Feb 2012 18:04:55 +0100 Subject: [PATCH 01/83] [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 02/83] [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 03/83] [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 04/83] [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 05/83] [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 06/83] [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 07/83] [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 08/83] [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 09/83] [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 10/83] 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 11/83] [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 12/83] [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 13/83] [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 14/83] [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 15/83] [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 16/83] [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 17/83] [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 18/83] [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 19/83] [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 20/83] [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 21/83] [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 22/83] [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 23/83] [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 24/83] [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 25/83] [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 26/83] [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 27/83] [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 28/83] [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 29/83] [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 30/83] [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 31/83] [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 32/83] [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 e9f4fc98cc98dab4ca1fdab2f6d9697485934beb Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 3 Apr 2012 16:15:53 +0200 Subject: [PATCH 33/83] [ADD] display unhelpful message when a list view search yields no result bzr revid: xmo@openerp.com-20120403141553-hrv006vdkv70zyfu --- addons/web/static/src/css/base.css | 9 +++++++++ addons/web/static/src/css/base.sass | 14 ++++++++++++++ addons/web/static/src/img/empty-list-arrow.png | Bin 0 -> 2308 bytes addons/web/static/src/js/view_list.js | 15 +++++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 addons/web/static/src/img/empty-list-arrow.png diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index b0a2b7945b9..d4a1d743d35 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -344,3 +344,12 @@ -webkit-box-shadow: none; -box-shadow: none; } +.openerp2 .oe-listview .oe_listview_nocontent > img { + float: left; + margin-right: 1.5em; +} +.openerp2 .oe-listview .oe_listview_nocontent > div { + overflow: hidden; + font-size: 150%; + width: 60ex; +} diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index c31f501c499..ecc45f5f538 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -337,6 +337,20 @@ $colour4: #8a89ba @include box-shadow(none) // }}} + + // ListView {{{ + .oe-listview + .oe_listview_nocontent + > img + float: left + margin-right: 1.5em + > div + // don't encroach on my arrow + overflow: hidden + font-size: 150% + width: 60ex + // }}} + .openerp // Transitional overrides for old styles {{{ // }}} diff --git a/addons/web/static/src/img/empty-list-arrow.png b/addons/web/static/src/img/empty-list-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b1372adfd3fc393ecc1dac3934c423822970cd GIT binary patch literal 2308 zcmV+f3H$bmP)X)YT08QE{zMm=*yzjy#>qO{b%( zI}c{Mx03c9H98A){3#yqKb--N032OnY{IHxe`j=U0dQ71X*fr_-rMdfhO?Be=W<_8 zj{qD+a-Q}nrt_NceY#zQ=DCovmL8(x;bg|Xh=WW7;Q#!dE6lRP7`PA`cgO>*0&zVl zW>;>57TRm70J;%?W6+jWT|J)*-v@DPq@u$gCQSQwHuVTV9}uaueWRY;EWlDthqccq zRtZs=&cEQ#VimEr2atn@VpxUD3W~h!`$m-_`A0=}o?^e*)&Md9s%|~v(U#IU;EAZL zX9cbK)i4Im3^T0>O!E>6djCen5f$R7~r| zu;s~((H7HQoPiGm0Mg*G8K^Fz{hq2?ms8*% z%^*ilIV?CJj@#Tc{%#aN;(JUPmYaT(pSG^!dl39mAj+ zAHlTx(Q*3(n%EEd80VWP8sl0cAnt>FPbnRKXBP^yBiOJ4zH_629t%h1&;d9RC$4GM zdBg{~8Y9C2sTIKPVw}`5{5+5}l7*dnGCed$7C>?f=-yFDYc6XUgGT4A&IJQ$V=gxa zE&~`xBsH*%sRiRSt%+bfTuE+Y7ES8wP@^LOD%{2&qG@l3Jp?zkm(G~ z4z>b-Y+dY5;=P4aA1u@VaLXjcPfkd_^Lg!lAy6#wj$jU&!>t_PGZeN3SZKfv< zxWLjR0IdZSO$8Y8JrUBvJm1U|6%t}}54F)>(e3N8rXGlJFN$ejS<4cF9W3wLb27AO zuVhBTQW9=Jcb=<)*mrVsG|d&7=GSs=*8;dFsFX%U4zeh32#T6$Hv|u0tri+~mza~$ zrTic-)TM~7r0jY0ozxo%!Zb0HEqq#Sg@*_{Qpx+V=J)aYzf;4^OA5GcTc-6aH>_Km z4`9l^4;mz^fh;As0y5=4*lg&=Y2g!1G5s1gT4s|<4V?>D^Dx%Ain1V@?;+p-6m!|4 z&lQP*P^{%{zSpLW`1T@NRSOGS0s#>u`!GGQ1GJ#3Hpa63|i!0A-$!bCx2rXs~bW2oOmls+(bVccccdm}nF1 z9c}PpH8Zr3m;p8FA^T`Uur6Vq9_cTQ1{#a^|4aA`k^CHgzbuWJR0E1TYitJcVwLGT z;8(>BBi&n#K=qiCw&!W-!J9NQe;fXO02S@$DIdqbUx&|q3pot46o65R>CI%5TPW56 zjRoL3;`2O)>`5R*x_%SNv_FJ-@Pge&g*Sx2U=2c)6y^t@s5?jzQ(Ob(z_q*pU(p$` z$+#1Tp6#RWCcpQB->*ufa&oybk>UURh*Jq_0Ezq1zGqM>Y--%6_DoaeH!B1lHlEIZ zLz`i~@1A1v#RU1xWTntTrZlZBMKamypu9DtiCD#Dj*7%;`zFRa5b;^r_d&oe>c63#sisT!WhY_tYNHW)WAYO5cw(<3g|5%FjIxduIUVGc-3yU9m*M zo;{XZ@Kvj0ZT0#)3Fj^}i~yq}Z9w$?z%jsNVUy$*XY>Nb=mm_CL;ywrMgT?tMiK!S e0T=;z9LPV+S$p2eoZCSF0000') + .append($('', { + src: '/web/static/src/img/empty-list-arrow.png', + alt: _t("This list is empty")})) + .append($('
      ').html(help))); } }); openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.ListView.List# */{ @@ -1344,6 +1356,9 @@ openerp.web.ListView.Groups = openerp.web.Class.extend( /** @lends openerp.web.L self.records.add(records, {silent: true}); list.render(); d.resolve(list); + if (_.isEmpty(records)) { + view.no_result(); + } });}); return d.promise(); }, From da864266c0c374abecad3295f74a57b0d7a394b8 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 3 Apr 2012 16:17:26 +0200 Subject: [PATCH 34/83] [FIX] o2m/m2m lists: only display help when there's help on the action... or an action at all bzr revid: xmo@openerp.com-20120403141726-a6o4sfvifzzjy9r4 --- addons/web/static/src/js/view_list.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index c9a65ac6c4b..85943732c68 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -749,8 +749,9 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# } }, no_result: function () { - var help = this.options.action.help; - if (this.groups.group_by || !help) { + if (this.groups.group_by + || !this.options.action + || !this.options.action.help) { return; } this.$element.children('table').replaceWith( @@ -758,7 +759,7 @@ openerp.web.ListView = openerp.web.View.extend( /** @lends openerp.web.ListView# .append($('', { src: '/web/static/src/img/empty-list-arrow.png', alt: _t("This list is empty")})) - .append($('
      ').html(help))); + .append($('
      ').html(this.options.action.help))); } }); openerp.web.ListView.List = openerp.web.Class.extend( /** @lends openerp.web.ListView.List# */{ From 522de0125cd5764220957637ee8f647dc910ef8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 10:47:37 +0200 Subject: [PATCH 35/83] [REVIEW] needaction: remove unnecessary bzr revid: tde@openerp.com-20120418084737-jxvoe5jf02xalq8r --- openerp/addons/base/ir/ir_needaction.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 3fcd56affae..6dd0b73e82c 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -49,15 +49,11 @@ class ir_needaction_users(osv.osv): def _get_users(self, cr, uid, res_ids, res_model, context=None): """Given res_ids of res_model, get user_ids present in table""" - if context is None: - context = {} needact_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) return map(itemgetter('res_id'), self.read(cr, uid, needact_ids, context=context)) def create_users(self, cr, uid, res_ids, res_model, user_ids, context=None): """Given res_ids of res_model, add user_ids to the relationship table""" - if context is None: - context = {} for res_id in res_ids: for user_id in user_ids: self.create(cr, uid, {'res_model': res_model, 'res_id': res_id, 'user_id': user_id}, context=context) @@ -65,8 +61,6 @@ class ir_needaction_users(osv.osv): def unlink_users(self, cr, uid, res_ids, res_model, context=None): """Given res_ids of res_model, delete all entries in the relationship table""" - if context is None: - context = {} to_del_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) return self.unlink(cr, uid, to_del_ids, context=context) @@ -131,8 +125,6 @@ class ir_needaction_mixin(osv.osv): return dict.fromkeys(ids, []) def create(self, cr, uid, values, context=None): - if context is None: - context = {} needact_table_obj = self.pool.get('ir.needaction_users') # perform create obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context) @@ -142,8 +134,6 @@ class ir_needaction_mixin(osv.osv): return obj_id def write(self, cr, uid, ids, values, context=None): - if context is None: - context = {} needact_table_obj = self.pool.get('ir.needaction_users') # perform write write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context) @@ -154,8 +144,6 @@ class ir_needaction_mixin(osv.osv): return write_res def unlink(self, cr, uid, ids, context=None): - if context is None: - context = {} # unlink user_ids needact_table_obj = self.pool.get('ir.needaction_users') needact_table_obj.unlink_users(cr, uid, ids, self._name, context=context) @@ -169,8 +157,6 @@ class ir_needaction_mixin(osv.osv): def needaction_get_record_ids(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id get the number of actions it has to perform""" - if context is None: - context = {} needact_table_obj = self.pool.get('ir.needaction_users') needact_table_ids = needact_table_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) return map(itemgetter('res_id'), needact_table_obj.read(cr, uid, needact_table_ids, context=context)) @@ -178,8 +164,6 @@ class ir_needaction_mixin(osv.osv): def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id get the number of actions it has to perform""" - if context is None: - context = {} needact_table_obj = self.pool.get('ir.needaction_users') return needact_table_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context) @@ -188,8 +172,6 @@ class ir_needaction_mixin(osv.osv): perform an action. Records are given as references, a list of tuples (model_name, record_id). This method is trans-model.""" - if context is None: - context = {} needact_table_obj = self.pool.get('ir.needaction_users') needact_table_ids = needact_table_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) needact_records = needact_table_obj.read(cr, uid, needact_table_ids, context=context) From 3930d41621dddd023f62ad0695528367a9773127 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 18 Apr 2012 10:47:42 +0200 Subject: [PATCH 36/83] [IMP] hr.config.settings: small fix in view bzr revid: rco@openerp.com-20120418084742-ruboxpqeggpzu2sh --- addons/hr/res_config_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr/res_config_view.xml b/addons/hr/res_config_view.xml index 3377ad20c37..a34783772c9 100644 --- a/addons/hr/res_config_view.xml +++ b/addons/hr/res_config_view.xml @@ -42,7 +42,7 @@ - Configure Human Resources Application + Configure Human Resources hr.config.settings form From 7ef99096b0b84b0eb338ba55fcce14b3f9b5142c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 10:59:45 +0200 Subject: [PATCH 37/83] [RFEVIEW] Cleaned code, based on ODO/VMT review. bzr revid: tde@openerp.com-20120418085945-jlr7hbzrpju3yv2m --- openerp/addons/base/ir/ir_needaction.py | 52 +++++++++---------- .../addons/base/security/ir.model.access.csv | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 6dd0b73e82c..7f246dc282a 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -24,17 +24,18 @@ from operator import itemgetter from osv import osv, fields from tools.translate import _ -class ir_needaction_users(osv.osv): +class ir_needaction_users_rel(osv.osv): ''' - ir_needaction_users holds data related to the needaction - mechanism inside OpenERP. A needaction is characterized by: + ir_needaction_users_rel holds data related to the needaction + mechanism inside OpenERP. A row in this model is characterized by: - res_model: model of the record requiring an action - res_id: ID of the record requiring an action - user_id: foreign key to the res.users table, to the user that has to perform the action - ''' + This model can be seen as a many2many, linking (res_model, res_id) to + users (those whose attention is required on the record).''' - _name = 'ir.needaction_users' + _name = 'ir.ir_needaction_users_rel' _description = 'Needaction relationship table' _rec_name = 'id' _order = 'id desc' @@ -49,8 +50,8 @@ class ir_needaction_users(osv.osv): def _get_users(self, cr, uid, res_ids, res_model, context=None): """Given res_ids of res_model, get user_ids present in table""" - needact_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) - return map(itemgetter('res_id'), self.read(cr, uid, needact_ids, context=context)) + rel_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) + return map(itemgetter('res_id'), self.read(cr, uid, rel_ids, context=context)) def create_users(self, cr, uid, res_ids, res_model, user_ids, context=None): """Given res_ids of res_model, add user_ids to the relationship table""" @@ -88,11 +89,11 @@ class ir_needaction_mixin(osv.osv): This class wraps a class (ir.needaction_users) that behaves like a many2many field. However, no field is added to the model - inheriting from base.needaction. The mixin class manages the low-level + inheriting from this mixin class. This class handles the low-level considerations of updating relationships. Every change made on the record calls a method that updates the relationships. - Objects using the need_action feature should override the + Objects using the 'need_action' feature should override the ``get_needaction_user_ids`` method. This methods returns a dictionary whose keys are record ids, and values a list of user ids, like in a many2many relationship. Therefore by defining only one method, @@ -112,7 +113,7 @@ class ir_needaction_mixin(osv.osv): are given as references, a list of tuples (model_name, record_id) ''' _name = 'ir.needaction_mixin' - _description = 'Need action of users on records API' + _description = '"Need action" mixin' #------------------------------------------------------ # Addon API @@ -125,56 +126,55 @@ class ir_needaction_mixin(osv.osv): return dict.fromkeys(ids, []) def create(self, cr, uid, values, context=None): - needact_table_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.needaction_users') # perform create obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context) # link user_ids needaction_user_ids = self.get_needaction_user_ids(cr, uid, [obj_id], context=context) - needact_table_obj.create_users(cr, uid, [obj_id], self._name, needaction_user_ids[obj_id], context=context) + rel_obj.create_users(cr, uid, [obj_id], self._name, needaction_user_ids[obj_id], context=context) return obj_id def write(self, cr, uid, ids, values, context=None): - needact_table_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.needaction_users') # perform write write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context) # get and update user_ids needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context) for id in ids: - needact_table_obj.update_users(cr, uid, [id], self._name, needaction_user_ids[id], context=context) + rel_obj.update_users(cr, uid, [id], self._name, needaction_user_ids[id], context=context) return write_res def unlink(self, cr, uid, ids, context=None): # unlink user_ids - needact_table_obj = self.pool.get('ir.needaction_users') - needact_table_obj.unlink_users(cr, uid, ids, self._name, context=context) + rel_obj = self.pool.get('ir.needaction_users') + rel_obj.unlink_users(cr, uid, ids, self._name, context=context) # perform unlink return super(ir_needaction_mixin, self).unlink(cr, uid, ids, context=context) #------------------------------------------------------ - # Need action API + # "Need action" API #------------------------------------------------------ def needaction_get_record_ids(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id get the number of actions it has to perform""" - needact_table_obj = self.pool.get('ir.needaction_users') - needact_table_ids = needact_table_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) - return map(itemgetter('res_id'), needact_table_obj.read(cr, uid, needact_table_ids, context=context)) + rel_obj = self.pool.get('ir.needaction_users') + needact_table_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) + return map(itemgetter('res_id'), rel_obj.read(cr, uid, needact_table_ids, context=context)) def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id get the number of actions it has to perform""" - needact_table_obj = self.pool.get('ir.needaction_users') - return needact_table_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context) + rel_obj = self.pool.get('ir.needaction_users') + return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context) def needaction_get_record_references(self, cr, uid, user_id, offset=None, limit=None, order=None, context=None): """For a given user_id, get all the records that asks this user to perform an action. Records are given as references, a list of tuples (model_name, record_id). This method is trans-model.""" - needact_table_obj = self.pool.get('ir.needaction_users') - needact_table_ids = needact_table_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) - needact_records = needact_table_obj.read(cr, uid, needact_table_ids, context=context) - return map(itemgetter('res_model', 'id'), needact_records) + rel_obj = self.pool.get('ir.needaction_users') + needact_table_ids = rel_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) + return map(itemgetter('res_model', 'id'), rel_obj.read(cr, uid, needact_table_ids, context=context)) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index ac05b726341..8b906ba48a5 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -121,6 +121,6 @@ "access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0 "access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1 "access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0 -"access_ir_needaction_users","ir_needaction_users","model_ir_needaction_users",,1,1,1,1 +"access_ir_needaction_users_rel","ir_needaction_users_rel","model_ir_needaction_users_rel",,1,1,1,1 "access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1 From 94210b26a27cb32c0a00bcb578b44bca138eeec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:01:41 +0200 Subject: [PATCH 38/83] [REVIEW] [DOC] Updated documentation. bzr revid: tde@openerp.com-20120418090141-ho1205rt0omyxo4k --- doc/api/need_action_specs.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/api/need_action_specs.rst b/doc/api/need_action_specs.rst index c9f32dd60c4..88787ae4c44 100644 --- a/doc/api/need_action_specs.rst +++ b/doc/api/need_action_specs.rst @@ -1,14 +1,14 @@ Need action mechanism ===================== -ir.needaction mixin class -++++++++++++++++++++++++++ +ir.needaction_mixin mixin class ++++++++++++++++++++++++++++++++ This revision adds a mixin class for objects using the need action feature. Need action feature can be used by objects willing to be able to signal that an action is required on a particular record. If in the business logic an action must be performed by somebody, for instance validation by a manager, this mechanism allows to set a list of users asked to perform an action. -This class wraps a class (ir.needaction_users) that behaves like a many2many field. However, no field is added to the model inheriting from base.needaction. The mixin class manages the low-level considerations of updating relationships. Every change made on the record calls a method that updates the relationships. +This class wraps a class (ir.needaction_users) that behaves like a many2many field. However, no field is added to the model inheriting from ir.needaction_mixin. The mixin class manages the low-level considerations of updating relationships. Every change made on the record calls a method that updates the relationships. Objects using the need_action feature should override the ``get_needaction_user_ids`` method. This methods returns a dictionary whose keys are record ids, and values a list of user ids, like in a many2many relationship. Therefore by defining only one method, you can specify if an action is required by defining the users that have to do it, in every possible situation. @@ -30,12 +30,12 @@ Those fields are functional, because they depend on the user and must therefore Addon implementation example ++++++++++++++++++++++++++++ -In your ``foo`` module, you want to specify that when it is in state ``confirmed``, it has to be validated by a manager, given by the field ``manager_id``. After making ``foo`` inheriting from ``base.needaction``, you override the ``get_needaction_user_ids`` method: +In your ``foo`` module, you want to specify that when it is in state ``confirmed``, it has to be validated by a manager, given by the field ``manager_id``. After making ``foo`` inheriting from ``ir.needaction_mixin``, you override the ``get_needaction_user_ids`` method: :: [...] - _inherit = [base.needaction] + _inherit = [`ir.needaction_mixin] [...] def get_needaction_user_ids(self, cr, uid, ids, context=None): result = dict.fromkeys(ids) From c1a4f1f8db6e3161ae61a8358e023c281dc3fed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:07:35 +0200 Subject: [PATCH 39/83] [IMP] Updated code (see odo/vmt review). Also removed record_ids storage in menus, to avoid passing too much data to the client. bzr revid: tde@openerp.com-20120418090735-e5ewk3wcf7bp0bvu --- openerp/addons/base/ir/ir_needaction.py | 15 ++++++++------- openerp/addons/base/ir/ir_ui_menu.py | 8 +++----- openerp/osv/orm.py | 10 ++++++---- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 7f246dc282a..c408c189939 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -51,7 +51,7 @@ class ir_needaction_users_rel(osv.osv): def _get_users(self, cr, uid, res_ids, res_model, context=None): """Given res_ids of res_model, get user_ids present in table""" rel_ids = self.search(cr, uid, [('res_model', '=', res_model), ('res_id', 'in', res_ids)], context=context) - return map(itemgetter('res_id'), self.read(cr, uid, rel_ids, context=context)) + return list(set(map(itemgetter('user_id'), self.read(cr, uid, rel_ids, ['user_id'], context=context)))) def create_users(self, cr, uid, res_ids, res_model, user_ids, context=None): """Given res_ids of res_model, add user_ids to the relationship table""" @@ -69,7 +69,7 @@ class ir_needaction_users_rel(osv.osv): """Given res_ids of res_model, update their entries in the relationship table to user_ids""" # read current records cur_users = self._get_users(cr, uid, res_ids, res_model, context=context) - if len(cur_users) == len(user_ids) and all([cur_user in user_ids for cur_user in cur_users]): + if len(cur_users) == len(user_ids) and all(cur_user in user_ids for cur_user in cur_users): return True # unlink old records self.unlink_users(cr, uid, res_ids, res_model, context=context) @@ -157,10 +157,11 @@ class ir_needaction_mixin(osv.osv): def needaction_get_record_ids(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id - get the number of actions it has to perform""" + return the record ids that require the user to perform an + action""" rel_obj = self.pool.get('ir.needaction_users') - needact_table_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) - return map(itemgetter('res_id'), rel_obj.read(cr, uid, needact_table_ids, context=context)) + rel_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) + return map(itemgetter('res_id'), rel_obj.read(cr, uid, rel_ids, ['res_id'], context=context)) def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id @@ -174,7 +175,7 @@ class ir_needaction_mixin(osv.osv): tuples (model_name, record_id). This method is trans-model.""" rel_obj = self.pool.get('ir.needaction_users') - needact_table_ids = rel_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) - return map(itemgetter('res_model', 'id'), rel_obj.read(cr, uid, needact_table_ids, context=context)) + rel_ids = rel_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) + return map(itemgetter('res_model', 'res_id'), rel_obj.read(cr, uid, rel_ids, ['res_model', 'res_id'], context=context)) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 1f3f2e0f181..0de4ff74c11 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -258,19 +258,17 @@ class ir_ui_menu(osv.osv): def _get_needaction(self, cr, uid, ids, field_names, args, context=None): if context is None: context = {} - res = dict.fromkeys(ids) + res = {} for menu in self.browse(cr, uid, ids, context=context): res[menu.id] = {} if menu.action and menu.action.type == 'ir.actions.act_window' and menu.action.res_model: - menu_needaction_res = self.pool.get(menu.action.res_model).get_needaction_info(cr, uid, uid, domain=menu.action.domain, context=context) + menu_needaction_res = self.pool.get(menu.action.res_model)._get_needaction_info(cr, uid, uid, domain=menu.action.domain, context=context) # TODO: find the addon that causes a bug on runbot, not on local if not isinstance(menu_needaction_res[1], (int, long)): menu_needaction_res[1] = 0 else: - menu_needaction_res = [False, 0, ()] + menu_needaction_res = [False, 0] res[menu.id]['needaction_enabled'] = menu_needaction_res[0] res[menu.id]['needaction_counter'] = menu_needaction_res[1] - # not used currently, therefore set to a void list - res[menu.id]['needaction_record_ids'] = [] return res _columns = { diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 313c96a9fb4..bdf78fd3567 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -4866,7 +4866,7 @@ class BaseModel(object): get_xml_id = get_external_id _get_xml_ids = _get_external_ids - def get_needaction_info(self, cr, uid, user_id, limit=None, order=None, domain=False, context=None): + def _get_needaction_info(self, cr, uid, user_id, limit=None, order=None, domain=False, context=None): """Base method for needaction mechanism - see ir.needaction for actual implementation - if the model uses the need action mechanism @@ -4883,16 +4883,18 @@ class BaseModel(object): :return: [uses_needaction=True/False, needaction_uid_ctr=%d] """ if hasattr(self, 'needaction_get_record_ids'): + # Arbitrary limit, but still much lower thant infinity, to avoid + # getting too much data. ids = self.needaction_get_record_ids(cr, uid, user_id, limit=8192, context=context) if not ids: - return [True, 0, []] + return [True, 0] if domain: new_domain = eval(domain, locals_dict={'uid': user_id}) + [('id', 'in', ids)] else: new_domain = [('id', 'in', ids)] - return [True, self.search(cr, uid, new_domain, limit=limit, order=order, count=True, context=context), ids] + return [True, self.search(cr, uid, new_domain, limit=limit, order=order, count=True, context=context)] else: - return [False, 0, []] + return [False, 0] # Transience def is_transient(self): From aca2b4874639b5adbe13e6471b8d9a0d5f59651d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:12:59 +0200 Subject: [PATCH 40/83] [FIX] Propagated name change from ir_needaction_users to ir_needaction_users_rel. bzr revid: tde@openerp.com-20120418091259-cnhae6rsc8dqlmkw --- openerp/addons/base/ir/ir_needaction.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index c408c189939..2ea81870c68 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -87,7 +87,7 @@ class ir_needaction_mixin(osv.osv): validation by a manager, this mechanism allows to set a list of users asked to perform an action. - This class wraps a class (ir.needaction_users) that behaves + This class wraps a class (ir.ir_needaction_users_rel) that behaves like a many2many field. However, no field is added to the model inheriting from this mixin class. This class handles the low-level considerations of updating relationships. Every change made on the @@ -126,7 +126,7 @@ class ir_needaction_mixin(osv.osv): return dict.fromkeys(ids, []) def create(self, cr, uid, values, context=None): - rel_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.ir_needaction_users_rel') # perform create obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context) # link user_ids @@ -135,7 +135,7 @@ class ir_needaction_mixin(osv.osv): return obj_id def write(self, cr, uid, ids, values, context=None): - rel_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.ir_needaction_users_rel') # perform write write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context) # get and update user_ids @@ -146,7 +146,7 @@ class ir_needaction_mixin(osv.osv): def unlink(self, cr, uid, ids, context=None): # unlink user_ids - rel_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.ir_needaction_users_rel') rel_obj.unlink_users(cr, uid, ids, self._name, context=context) # perform unlink return super(ir_needaction_mixin, self).unlink(cr, uid, ids, context=context) @@ -159,14 +159,14 @@ class ir_needaction_mixin(osv.osv): """Given the current model and a user_id return the record ids that require the user to perform an action""" - rel_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.ir_needaction_users_rel') rel_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) return map(itemgetter('res_id'), rel_obj.read(cr, uid, rel_ids, ['res_id'], context=context)) def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id get the number of actions it has to perform""" - rel_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.ir_needaction_users_rel') return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context) def needaction_get_record_references(self, cr, uid, user_id, offset=None, limit=None, order=None, context=None): @@ -174,7 +174,7 @@ class ir_needaction_mixin(osv.osv): perform an action. Records are given as references, a list of tuples (model_name, record_id). This method is trans-model.""" - rel_obj = self.pool.get('ir.needaction_users') + rel_obj = self.pool.get('ir.ir_needaction_users_rel') rel_ids = rel_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) return map(itemgetter('res_model', 'res_id'), rel_obj.read(cr, uid, rel_ids, ['res_model', 'res_id'], context=context)) From 7b073bd3861d83fee01e987b151eeef88c8dd314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:13:57 +0200 Subject: [PATCH 41/83] [FIX] Propagated name change from ir_needaction_users to ir_needaction_users_rel. bzr revid: tde@openerp.com-20120418091357-w3364qw5e0fyfjkj --- openerp/addons/base/ir/ir.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index ccec381f1d1..e66182bae39 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -662,9 +662,9 @@ - - ir.needaction_users.tree - ir.needaction_users + + ir.needaction_users_rel.tree + ir.needaction_users_rel tree 10 @@ -676,14 +676,14 @@ - + Need action relationships - ir.needaction_users + ir.needaction_users_rel form tree,form - + Date: Wed, 18 Apr 2012 11:16:13 +0200 Subject: [PATCH 42/83] [imp] removed validate & invalid in form view bzr revid: nicolas.vanhoren@openerp.com-20120418091613-c0gpu6eo3yqndq6l --- addons/web/static/src/js/view_form.js | 65 +++++++------------ addons/web_calendar/static/src/js/calendar.js | 1 - 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 3f9ef8301fd..2ca19687cec 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -250,9 +250,6 @@ instance.web.FormView = instance.web.View.extend({ field.reset(); var result = field.set_value(self.datarecord[f] || false); set_values.push(result); - $.when(result).then(function() { - field.validate(); - }); }); return $.when.apply(null, set_values).pipe(function() { if (!record.id) { @@ -285,9 +282,6 @@ instance.web.FormView = instance.web.View.extend({ on_form_changed: function() { this.trigger("view_content_has_changed"); _.each(this.get_widgets(), function(w) { - if (w.field) { - w.validate(); - } w.update_dom(); }); }, @@ -1265,7 +1259,7 @@ instance.web.form.compute_domain = function(expr, fields) { _t("Unknown field %s in domain %s"), ex[0], JSON.stringify(expr))); } - var field_value = field.get_value(); + var field_value = field.get_value ? field.get_value() : field.value; var op = ex[1]; var val = ex[2]; @@ -1655,7 +1649,7 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins this.set({'value': false}); this.field = this.field_manager.get_field(this.name); this.set({required: this.modifiers['required'] === true}); - this.invalid = this.dirty = false; + this.dirty = false; // some events to make the property "effective_readonly" sync automatically with "readonly" and // "force_readonly" @@ -1695,7 +1689,6 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins this._inhibit_on_change = true; this.set({'value': value_}); this._inhibit_on_change = false; - this.invalid = false; this.update_dom(); }, on_translate: function() { @@ -1705,7 +1698,7 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins return this.get('value'); }, is_valid: function() { - return !this.invalid; + return true; }, is_dirty: function() { return this.dirty && !this.get("effective_readonly"); @@ -1723,7 +1716,6 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins }, _on_ui_change: function() { this.dirty = true; - this.validate(); if (this.is_valid()) { this.view.do_onchange(this); this.view.on_form_changed(true); @@ -1732,9 +1724,6 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins this.update_dom(true); } }, - validate: function() { - this.invalid = false; - }, focus: function($element) { if ($element) { setTimeout(function() { @@ -1821,16 +1810,16 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} this.$element.text(show_value); } }, - validate: function() { - this.invalid = false; + is_valid: function() { if (!this.get("effective_readonly")) { try { var value_ = instance.web.parse_value(this.$element.find('input').val(), this, ''); - this.invalid = this.get("required") && value_ === ''; + return (! this.get("required")) || value_ !== ''; } catch(e) { - this.invalid = true; + return false; } } + return true; }, focus: function($element) { this._super($element || this.$element.find('input:first')); @@ -2031,11 +2020,11 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten this.$element.text(instance.web.format_value(this.get('value'), this, '')); } }, - validate: function() { - this.invalid = false; + is_valid: function() { if (!this.get("effective_readonly")) { - this.invalid = !this.datewidget.is_valid() || (this.get("required") && !this.datewidget.get_value()); + return this.datewidget.is_valid() && (!this.get("required") || this.datewidget.get_value()); } + return true; }, focus: function($element) { this._super($element || (this.datewidget && this.datewidget.$input)); @@ -2076,16 +2065,16 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} this.$element.text(show_value); } }, - validate: function() { - this.invalid = false; + is_valid: function() { if (!this.get("effective_readonly")) { try { var value_ = instance.web.parse_value(this.$textarea.val(), this, ''); - this.invalid = this.get("required") && value_ === ''; + return !this.get("required") || value_ !== ''; } catch(e) { - this.invalid = true; + return false; } } + return true; }, focus: function($element) { this._super($element || this.$textarea); @@ -2220,13 +2209,13 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte this.$element.text(option ? option[1] : this.values[0][1]); } }, - validate: function() { + is_valid: function() { if (this.get("effective_readonly")) { - this.invalid = false; - return; + return true; } var value_ = this.values[this.$element.find('select')[0].selectedIndex]; - this.invalid = !(value_ && !(this.get("required") && value_[0] === false)); + var invalid_ = !(value_ && !(this.get("required") && value_[0] === false)); + return ! invalid_; }, focus: function($element) { this._super($element || this.$element.find('select:first')); @@ -2570,8 +2559,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this._super(value_); this.inhibit_on_change = false; }, - validate: function() { - this.invalid = this.get("required") && ! this.get("value"); + is_valid: function() { + return !this.get("required") || !! this.get("value"); }, focus: function ($element) { this._super($element || this.$input); @@ -2876,23 +2865,18 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }, this)); }, is_valid: function() { - this.validate(); - return this._super(); - }, - validate: function() { - this.invalid = false; if (!this.viewmanager.views[this.viewmanager.active_view]) - return; + return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; if (this.viewmanager.active_view === "form") { for (var f in view.fields) { f = view.fields[f]; if (!f.is_valid()) { - this.invalid = true; - return; + return false; } } } + return true; }, is_dirty: function() { this.save_any_view(); @@ -3066,9 +3050,6 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ self.reload_content(); this.is_setted.resolve(); }, - validate: function() { - this.invalid = false; - }, load_view: function() { var self = this; this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { diff --git a/addons/web_calendar/static/src/js/calendar.js b/addons/web_calendar/static/src/js/calendar.js index fe261ba6fec..d92aac4304e 100644 --- a/addons/web_calendar/static/src/js/calendar.js +++ b/addons/web_calendar/static/src/js/calendar.js @@ -291,7 +291,6 @@ instance.web_calendar.CalendarView = instance.web.View.extend({ var ffield = form.fields[field_name]; ffield.reset(); $.when(ffield.set_value(data[field_name])).then(function() { - ffield.validate(); ffield.dirty = true; form.do_onchange(ffield); }); From 79a4e461a7f666cf71b597d3a733c85d03e8647d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:23:15 +0200 Subject: [PATCH 43/83] [FIX] Fixed wrong model name. bzr revid: tde@openerp.com-20120418092315-c35p0dtazgkgd9ae --- openerp/addons/base/ir/ir_needaction.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 2ea81870c68..6dd9cdfdc54 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -35,7 +35,7 @@ class ir_needaction_users_rel(osv.osv): This model can be seen as a many2many, linking (res_model, res_id) to users (those whose attention is required on the record).''' - _name = 'ir.ir_needaction_users_rel' + _name = 'ir.needaction_users_rel' _description = 'Needaction relationship table' _rec_name = 'id' _order = 'id desc' @@ -126,7 +126,7 @@ class ir_needaction_mixin(osv.osv): return dict.fromkeys(ids, []) def create(self, cr, uid, values, context=None): - rel_obj = self.pool.get('ir.ir_needaction_users_rel') + rel_obj = self.pool.get('ir.needaction_users_rel') # perform create obj_id = super(ir_needaction_mixin, self).create(cr, uid, values, context=context) # link user_ids @@ -135,7 +135,7 @@ class ir_needaction_mixin(osv.osv): return obj_id def write(self, cr, uid, ids, values, context=None): - rel_obj = self.pool.get('ir.ir_needaction_users_rel') + rel_obj = self.pool.get('ir.needaction_users_rel') # perform write write_res = super(ir_needaction_mixin, self).write(cr, uid, ids, values, context=context) # get and update user_ids @@ -146,7 +146,7 @@ class ir_needaction_mixin(osv.osv): def unlink(self, cr, uid, ids, context=None): # unlink user_ids - rel_obj = self.pool.get('ir.ir_needaction_users_rel') + rel_obj = self.pool.get('ir.needaction_users_rel') rel_obj.unlink_users(cr, uid, ids, self._name, context=context) # perform unlink return super(ir_needaction_mixin, self).unlink(cr, uid, ids, context=context) @@ -159,14 +159,14 @@ class ir_needaction_mixin(osv.osv): """Given the current model and a user_id return the record ids that require the user to perform an action""" - rel_obj = self.pool.get('ir.ir_needaction_users_rel') + rel_obj = self.pool.get('ir.needaction_users_rel') rel_ids = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, context=context) return map(itemgetter('res_id'), rel_obj.read(cr, uid, rel_ids, ['res_id'], context=context)) def needaction_get_action_count(self, cr, uid, user_id, limit=80, context=None): """Given the current model and a user_id get the number of actions it has to perform""" - rel_obj = self.pool.get('ir.ir_needaction_users_rel') + rel_obj = self.pool.get('ir.needaction_users_rel') return rel_obj.search(cr, uid, [('res_model', '=', self._name), ('user_id', '=', user_id)], limit=limit, count=True, context=context) def needaction_get_record_references(self, cr, uid, user_id, offset=None, limit=None, order=None, context=None): @@ -174,7 +174,7 @@ class ir_needaction_mixin(osv.osv): perform an action. Records are given as references, a list of tuples (model_name, record_id). This method is trans-model.""" - rel_obj = self.pool.get('ir.ir_needaction_users_rel') + rel_obj = self.pool.get('ir.needaction_users_rel') rel_ids = rel_obj.search(cr, uid, [('user_id', '=', user_id)], offset=offset, limit=limit, order=order, context=context) return map(itemgetter('res_model', 'res_id'), rel_obj.read(cr, uid, rel_ids, ['res_model', 'res_id'], context=context)) From 22adea444c6614243ce280a795931ce91460ddc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:41:01 +0200 Subject: [PATCH 44/83] [ADD] Added a 'needaction_pending' field: this states whether the record has an action pending. bzr revid: tde@openerp.com-20120418094101-o0a9lcpux8b6d75u --- openerp/addons/base/ir/ir_needaction.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 6dd9cdfdc54..7270eb2ce54 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -115,6 +115,21 @@ class ir_needaction_mixin(osv.osv): _name = 'ir.needaction_mixin' _description = '"Need action" mixin' + def get_needaction_pending(self, cr, uid, ids, name, arg, context=None): + res = {} + rel_obj = self.pool.get('ir.needaction_users_rel') + for id in ids: + res[id] = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id), ('user_id', '=', uid)], limit=1, count=True, context=context) > 0 + return res + + _columns = { + 'needaction_pending': fields.function(get_needaction_pending, type='boolean', + string='Need action pending', + help='If True, this field states that users have to perform an action. \ + This field comes from the needaction mechanism. Please refer \ + to the ir.needaction_mixin class.'), + } + #------------------------------------------------------ # Addon API #------------------------------------------------------ From 9012e82d774ce96d0fafaa41b56e39dd3b4ab6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:45:54 +0200 Subject: [PATCH 45/83] [IMP] 'needaction_pending' field: not necessary to read a table as the information is already available in object. bzr revid: tde@openerp.com-20120418094554-n5p2wf599tbmhizi --- openerp/addons/base/ir/ir_needaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 7270eb2ce54..7ed9762f8db 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -117,9 +117,9 @@ class ir_needaction_mixin(osv.osv): def get_needaction_pending(self, cr, uid, ids, name, arg, context=None): res = {} - rel_obj = self.pool.get('ir.needaction_users_rel') + needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context) for id in ids: - res[id] = rel_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', id), ('user_id', '=', uid)], limit=1, count=True, context=context) > 0 + res[id] = uid in needaction_user_ids[id] return res _columns = { From 84dadd0e27e1c6f7686ac97e3cfbdd53d74ef757 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 18 Apr 2012 11:50:43 +0200 Subject: [PATCH 46/83] [imp] separated syntax validity from required validity in form view bzr revid: nicolas.vanhoren@openerp.com-20120418095043-6b3pgd89d984f6o0 --- addons/web/static/src/js/view_form.js | 50 +++++++++++++++++---------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 2ca19687cec..976c844c3db 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1698,8 +1698,14 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins return this.get('value'); }, is_valid: function() { + return this.is_syntax_valid() && (! this.get('required') || ! this.is_false()); + }, + is_syntax_valid: function() { return true; }, + is_false: function() { + return this.get('value') === false; + }, is_dirty: function() { return this.dirty && !this.get("effective_readonly"); }, @@ -1716,7 +1722,7 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins }, _on_ui_change: function() { this.dirty = true; - if (this.is_valid()) { + if (this.is_syntax_valid()) { this.view.do_onchange(this); this.view.on_form_changed(true); this.view.do_notify_change(); @@ -1810,17 +1816,20 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} this.$element.text(show_value); } }, - is_valid: function() { + is_syntax_valid: function() { if (!this.get("effective_readonly")) { try { var value_ = instance.web.parse_value(this.$element.find('input').val(), this, ''); - return (! this.get("required")) || value_ !== ''; + return true; } catch(e) { return false; } } return true; }, + is_false: function() { + return this.get('value') === ''; + }, focus: function($element) { this._super($element || this.$element.find('input:first')); } @@ -1846,7 +1855,7 @@ instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ } }, on_button_clicked: function() { - if (!this.get('value') || !this.is_valid()) { + if (!this.get('value') || !this.is_syntax_valid()) { this.do_warn("E-mail error", "Can't send email to invalid e-mail address"); } else { location.href = 'mailto:' + this.get('value'); @@ -1958,7 +1967,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ this.$input.prop('readonly', this.readonly); this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); }, - is_valid: function() { + is_valid_: function() { var value_ = this.$input.val(); if (value_ === "") { return true; @@ -1978,7 +1987,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ return instance.web.format_value(v, {"widget": this.type_of_date}); }, on_change: function() { - if (this.is_valid()) { + if (this.is_valid_()) { this.set_value_from_ui_(); } } @@ -2020,12 +2029,15 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten this.$element.text(instance.web.format_value(this.get('value'), this, '')); } }, - is_valid: function() { + is_syntax_valid: function() { if (!this.get("effective_readonly")) { - return this.datewidget.is_valid() && (!this.get("required") || this.datewidget.get_value()); + return this.datewidget.is_valid_(); } return true; }, + is_false: function() { + return this.get('value') === ''; + }, focus: function($element) { this._super($element || (this.datewidget && this.datewidget.$input)); } @@ -2065,17 +2077,20 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} this.$element.text(show_value); } }, - is_valid: function() { + is_syntax_valid: function() { if (!this.get("effective_readonly")) { try { var value_ = instance.web.parse_value(this.$textarea.val(), this, ''); - return !this.get("required") || value_ !== ''; + return true; } catch(e) { return false; } } return true; }, + is_false: function() { + return this.get('value') === ''; + }, focus: function($element) { this._super($element || this.$textarea); }, @@ -2209,13 +2224,12 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte this.$element.text(option ? option[1] : this.values[0][1]); } }, - is_valid: function() { + is_syntax_valid: function() { if (this.get("effective_readonly")) { return true; } var value_ = this.values[this.$element.find('select')[0].selectedIndex]; - var invalid_ = !(value_ && !(this.get("required") && value_[0] === false)); - return ! invalid_; + return !! value_; }, focus: function($element) { this._super($element || this.$element.find('select:first')); @@ -2559,8 +2573,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this._super(value_); this.inhibit_on_change = false; }, - is_valid: function() { - return !this.get("required") || !! this.get("value"); + is_false: function() { + return ! this.get("value"); }, focus: function ($element) { this._super($element || this.$input); @@ -2864,7 +2878,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ return false; }, this)); }, - is_valid: function() { + is_syntax_valid: function() { if (!this.viewmanager.views[this.viewmanager.active_view]) return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; @@ -3510,8 +3524,8 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte this.m2o.renderElement(); this.m2o.start(); }, - is_valid: function() { - return this.get("required") === false || typeof(this.get_value()) === 'string'; + is_false: function() { + return typeof(this.get_value()) !== 'string'; }, is_dirty: function() { return this.selection.is_dirty() || this.m2o.is_dirty(); From e4063efdf27324d91ad2d1a33f042e0c2b192af8 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 18 Apr 2012 11:57:58 +0200 Subject: [PATCH 47/83] [imp] changed the way on_changes are received by the form view bzr revid: nicolas.vanhoren@openerp.com-20120418095758-x7du906934jbkp90 --- addons/web/static/src/js/view_form.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 976c844c3db..3cda7bf1b4c 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -853,6 +853,11 @@ instance.web.FormView = instance.web.View.extend({ if (this.get_field(name).translate) { this.translatable_fields.push(field); } + field.on('changed_value', this, function() { + this.do_onchange(field); + this.on_form_changed(true); + this.do_notify_change(); + }); }, get_field: function(field_name) { return this.fields_view.fields[field_name]; @@ -1574,7 +1579,7 @@ instance.web.form.FieldManagerInterface = { * - force_readonly: boolean, When it is true, the field should always appear * in read only mode, no matter what the value of the "readonly" property can be. * Events: - * - ... + * - changed_value: called to trigger a on_change in the view * */ instance.web.form.FieldInterface = { @@ -1662,14 +1667,12 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins _.bind(test_effective_readonly, this)(); this.on("change:value", this, function() { - if (this._field_is_started && ! this._inhibit_on_change) + if (! this._inhibit_on_change) this._on_ui_change(); }); }, start: function() { this._super.apply(this, arguments); - // quick hack, will change later - this._field_is_started = true; if (this.field.translate) { this.$element.addClass('oe_form_field_translatable'); this.$element.find('.oe_field_translate').click(this.on_translate); @@ -1723,9 +1726,7 @@ instance.web.form.AbstractField = instance.web.form.Widget.extend(/** @lends ins _on_ui_change: function() { this.dirty = true; if (this.is_syntax_valid()) { - this.view.do_onchange(this); - this.view.on_form_changed(true); - this.view.do_notify_change(); + this.trigger('changed_value'); } else { this.update_dom(true); } From 49f785c74ec6e6aa5df27439e8bf8860e23df58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 11:58:54 +0200 Subject: [PATCH 48/83] [DOC] Improved doc, adding reference to the needaction_pending field. bzr revid: tde@openerp.com-20120418095854-29o6qupaoi741imr --- doc/api/need_action_specs.rst | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/doc/api/need_action_specs.rst b/doc/api/need_action_specs.rst index 88787ae4c44..de1b75a5fb1 100644 --- a/doc/api/need_action_specs.rst +++ b/doc/api/need_action_specs.rst @@ -1,6 +1,8 @@ Need action mechanism ===================== +.. versionadded:: openobject-server.4124 + ir.needaction_mixin mixin class +++++++++++++++++++++++++++++++ @@ -15,15 +17,37 @@ Objects using the need_action feature should override the ``get_needaction_user_ This class also offers several global services,: - ``needaction_get_record_ids``: for the current model and uid, get all record ids that ask this user to perform an action. This mechanism is used for instance to display the number of pending actions in menus, such as Leads (12) - ``needaction_get_action_count``: as ``needaction_get_record_ids`` but returns only the number of action, not the ids (performs a search with count=True) - - ``needaction_get_user_record_references``: for a given uid, get all the records that ask this user to perform an action. Records are given as references, a list of tuples (model_name, record_id) + - ``needaction_get_user_record_references``: for a given uid, get all the records that ask this user to perform an action. Records are given as references, a list of tuples (model_name, record_id). + +.. versionadded:: openobject-server.XXXX + +This revision of the needaction_mixin mechanism slighty modifies the class behavior. The ``ir_needaction_mixin`` class now adds a function field on model inheriting from the class. This field allows to state whether a given record has a needaction for the current user. This is usefull if you want to customize views according to the needaction feature. For example, you may want to set records in bold in a list view if the current user has an action to perform on the record. This makes the class not a pure abstract class, but allows to easily use the action information. The field definition is:: + + + def get_needaction_pending(self, cr, uid, ids, name, arg, context=None): + res = {} + needaction_user_ids = self.get_needaction_user_ids(cr, uid, ids, context=context) + for id in ids: + res[id] = uid in needaction_user_ids[id] + return res + + _columns = { + 'needaction_pending': fields.function(get_needaction_pending, type='boolean', + string='Need action pending', + help='If True, this field states that users have to perform an action. \ + This field comes from the needaction mechanism. Please refer \ + to the ir.needaction_mixin class.'), + } Menu modification +++++++++++++++++ +.. versionchanged:: openobject-server.XXXX + This revision adds three functional fields to ``ir.ui.menu`` model : - ``uses_needaction``: boolean field. If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false. - ``needaction_uid_ctr``: integer field. If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform. - - ``needaction_record_ids``: many2many field. If the target model uses the need action mechanism, this field holds the ids of the record requesting the user to perform an action. + - ``needaction_record_ids``: many2many field. If the target model uses the need action mechanism, this field holds the ids of the record requesting the user to perform an action. **This field has been removed on version XXXX**. Those fields are functional, because they depend on the user and must therefore be computed at every refresh, each time menus are displayed. The use of the need action mechanism is done by taking into account the action domain in order to display accurate results. When computing the value of the functional fields, the ids of records asking the user to perform an action is concatenated to the action domain. A counting search is then performed on the model, giving back the number of action the users has to perform, limited to the domain of the action. From a787430ba943af3a7e7e71b3ddcb0c76d00d5d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 13:10:16 +0200 Subject: [PATCH 49/83] [DOC] Small doc update. bzr revid: tde@openerp.com-20120418111016-fgmvnq85esyhvw87 --- doc/api/need_action_specs.rst | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/doc/api/need_action_specs.rst b/doc/api/need_action_specs.rst index de1b75a5fb1..111c156ac79 100644 --- a/doc/api/need_action_specs.rst +++ b/doc/api/need_action_specs.rst @@ -1,16 +1,18 @@ Need action mechanism ===================== -.. versionadded:: openobject-server.4124 +.. versionadded:: 7.0 -ir.needaction_mixin mixin class -+++++++++++++++++++++++++++++++ +ir.needaction_mixin class ++++++++++++++++++++++++++ + +.. versionadded:: openobject-server.4124 This revision adds a mixin class for objects using the need action feature. Need action feature can be used by objects willing to be able to signal that an action is required on a particular record. If in the business logic an action must be performed by somebody, for instance validation by a manager, this mechanism allows to set a list of users asked to perform an action. -This class wraps a class (ir.needaction_users) that behaves like a many2many field. However, no field is added to the model inheriting from ir.needaction_mixin. The mixin class manages the low-level considerations of updating relationships. Every change made on the record calls a method that updates the relationships. +This class wraps a class (ir.ir_needaction_users_rel) that behaves like a many2many field. However, no field is added to the model inheriting from ir.needaction_mixin. The mixin class manages the low-level considerations of updating relationships. Every change made on the record calls a method that updates the relationships. Objects using the need_action feature should override the ``get_needaction_user_ids`` method. This methods returns a dictionary whose keys are record ids, and values a list of user ids, like in a many2many relationship. Therefore by defining only one method, you can specify if an action is required by defining the users that have to do it, in every possible situation. @@ -39,6 +41,23 @@ This revision of the needaction_mixin mechanism slighty modifies the class behav to the ir.needaction_mixin class.'), } +ir.needaction_users_rel class ++++++++++++++++++++++++++++++ + +.. versionadded:: openobject-server.4124 + +This class essentially wraps a database table that behaves like a many2many. +It holds data related to the needaction mechanism inside OpenERP. A row +in this model is characterized by: + + - ``res_model``: model of the record requiring an action + - ``res_id``: ID of the record requiring an action + - ``user_id``: foreign key to the res.users table, to the user that + has to perform the action + +This model can be seen as a many2many, linking (res_model, res_id) to +users (those whose attention is required on the record) + Menu modification +++++++++++++++++ From 8d858cf0b02e4e9a053ac3c4dd1438afe1c6278a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 13:12:12 +0200 Subject: [PATCH 50/83] [TMP] Uncommit temp fix. bzr revid: tde@openerp.com-20120418111212-j035lajdu2g045kf --- openerp/addons/base/ir/ir_ui_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 0de4ff74c11..e861f8231ca 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -264,7 +264,7 @@ class ir_ui_menu(osv.osv): if menu.action and menu.action.type == 'ir.actions.act_window' and menu.action.res_model: menu_needaction_res = self.pool.get(menu.action.res_model)._get_needaction_info(cr, uid, uid, domain=menu.action.domain, context=context) # TODO: find the addon that causes a bug on runbot, not on local - if not isinstance(menu_needaction_res[1], (int, long)): menu_needaction_res[1] = 0 + #if not isinstance(menu_needaction_res[1], (int, long)): menu_needaction_res[1] = 0 else: menu_needaction_res = [False, 0] res[menu.id]['needaction_enabled'] = menu_needaction_res[0] From 78883771bdb16b9d2c4c926162fc1d5dfab0e068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 13:25:43 +0200 Subject: [PATCH 51/83] [DOC] Added comments in code. bzr revid: tde@openerp.com-20120418112543-g5jbcn2h5mgrbxtc --- openerp/addons/base/ir/ir_needaction.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_needaction.py b/openerp/addons/base/ir/ir_needaction.py index 7ed9762f8db..cb6d29d1715 100644 --- a/openerp/addons/base/ir/ir_needaction.py +++ b/openerp/addons/base/ir/ir_needaction.py @@ -111,7 +111,15 @@ class ir_needaction_mixin(osv.osv): - ``needaction_get_user_record_references``: for a given uid, get all the records that ask this user to perform an action. Records are given as references, a list of tuples (model_name, record_id) - ''' + + The ``ir_needaction_mixin`` class adds a function field on model inheriting + from the class. This field allows to state whether a given record has + a needaction for the current user. This is usefull if you want to customize + views according to the needaction feature. For example, you may want to + set records in bold in a list view if the current user has an action to + perform on the record. This makes the class not a pure abstract class, + but allows to easily use the action information.''' + _name = 'ir.needaction_mixin' _description = '"Need action" mixin' From 3da67c851fa3333215dfa9afb8faa2be032dac11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 13:37:06 +0200 Subject: [PATCH 52/83] [REM] Removed unused field in menus. BTW it solves a GTk client crash. bzr revid: tde@openerp.com-20120418113706-raesdy1tak5dwkwo --- openerp/addons/base/ir/ir_ui_menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index e861f8231ca..f73496cb230 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -289,7 +289,6 @@ class ir_ui_menu(osv.osv): 'web_icon_hover_data':fields.function(_get_image_icon, string='Web Icon Image (hover)', type='binary', readonly=True, store=True, multi='icon'), 'needaction_enabled': fields.function(_get_needaction, string='Target model uses the need action mechanism', type='boolean', help='If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false.', multi='_get_needaction'), 'needaction_counter': fields.function(_get_needaction, string='Number of actions the user has to perform', type='integer', help='If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform.', multi='_get_needaction'), - 'needaction_record_ids': fields.function(_get_needaction, string='Ids of records requesting an action from the user', type='many2many', help='If the target model uses the need action mechanism, this field holds the ids of the record requesting the user to perform an action.', multi='_get_needaction'), 'action': fields.function(_action, fnct_inv=_action_inv, type='reference', string='Action', selection=[ From d029e2734c65624083eeb593a9684e35d31e7b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 18 Apr 2012 13:37:48 +0200 Subject: [PATCH 53/83] [DOC] Small doc update. bzr revid: tde@openerp.com-20120418113748-vsdtsype2l94wbl5 --- doc/api/need_action_specs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/need_action_specs.rst b/doc/api/need_action_specs.rst index 111c156ac79..a3a8ba04bf0 100644 --- a/doc/api/need_action_specs.rst +++ b/doc/api/need_action_specs.rst @@ -66,7 +66,7 @@ Menu modification This revision adds three functional fields to ``ir.ui.menu`` model : - ``uses_needaction``: boolean field. If the menu entry action is an act_window action, and if this action is related to a model that uses the need_action mechanism, this field is set to true. Otherwise, it is false. - ``needaction_uid_ctr``: integer field. If the target model uses the need action mechanism, this field gives the number of actions the current user has to perform. - - ``needaction_record_ids``: many2many field. If the target model uses the need action mechanism, this field holds the ids of the record requesting the user to perform an action. **This field has been removed on version XXXX**. + - **REMOVED** ``needaction_record_ids``: many2many field. If the target model uses the need action mechanism, this field holds the ids of the record requesting the user to perform an action. **This field has been removed on version XXXX**. Those fields are functional, because they depend on the user and must therefore be computed at every refresh, each time menus are displayed. The use of the need action mechanism is done by taking into account the action domain in order to display accurate results. When computing the value of the functional fields, the ids of records asking the user to perform an action is concatenated to the action domain. A counting search is then performed on the model, giving back the number of action the users has to perform, limited to the domain of the action. From 29d48867ab89e6067fb34b97522095c334040766 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Wed, 18 Apr 2012 13:50:04 +0200 Subject: [PATCH 54/83] [imp] changed the way validity is tested bzr revid: nicolas.vanhoren@openerp.com-20120418115004-otz3xocucky9u5gl --- addons/web/static/src/js/view_form.js | 47 ++++++++++----------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 3cda7bf1b4c..fedc602b4b1 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -247,7 +247,7 @@ instance.web.FormView = instance.web.View.extend({ } _(this.fields).each(function (field, f) { - field.reset(); + field._dirty_flag = false; var result = field.set_value(self.datarecord[f] || false); set_values.push(result); }); @@ -258,7 +258,7 @@ instance.web.FormView = instance.web.View.extend({ _.each(self.fields_order, function(field_name) { if (record[field_name] !== undefined) { var field = self.fields[field_name]; - field.dirty = true; + field._dirty_flag = true; self.do_onchange(field); } }); @@ -469,7 +469,7 @@ instance.web.FormView = instance.web.View.extend({ var value_ = result.value[f]; if (field.get_value() != value_) { field.set_value(value_); - field.dirty = true; + field._dirty_flag = true; if (!_.contains(processed, field.name)) { this.do_onchange(field, processed); } @@ -615,7 +615,7 @@ instance.web.FormView = instance.web.View.extend({ if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f.is_dirty())) { + } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields @@ -754,7 +754,7 @@ instance.web.FormView = instance.web.View.extend({ }, is_dirty: function() { return _.any(this.fields, function (value_) { - return value_.is_dirty(); + return value_._dirty_flag; }); }, is_interactible_record: function() { @@ -854,9 +854,11 @@ instance.web.FormView = instance.web.View.extend({ this.translatable_fields.push(field); } field.on('changed_value', this, function() { - this.do_onchange(field); - this.on_form_changed(true); - this.do_notify_change(); + if (field.is_syntax_valid()) { + this.do_onchange(field); + this.on_form_changed(true); + this.do_notify_change(); + } }); }, get_field: function(field_name) { @@ -1628,7 +1630,11 @@ instance.web.form.FieldInterface = { * Inform the current object of the id it should use to match a html