diff --git a/addons/web/static/src/js/core/rpc.js b/addons/web/static/src/js/core/rpc.js new file mode 100644 index 00000000000..76d416ec110 --- /dev/null +++ b/addons/web/static/src/js/core/rpc.js @@ -0,0 +1,273 @@ +odoo.define('web.rpc', function (require) { +"use strict"; + +var Class = require('web.Class'); + +var BaseRPCBuilder = Class.extend({ + /** + * @param {Widget} parent the rpc will go through this widget + * @param {Object} params + * @param {string} [params.model] + * @param {string} [params.method] + * @param {string} [params.route] + */ + init: function (parent, params) { + this.parent = parent; + this._route = params.route; + if (!this._route) { + this._route = '/web/dataset/call_kw/' + params.model + '/' + params.method; + } + this._model = params.model; + this._method = params.method; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {Array} args + * @returns {BaseRPCBuilder} + */ + args: function (args) { + if (!(args instanceof Array)) { + throw new Error("Arguments should be an array"); + } + this._args = args; + return this; + }, + /** + * @param {Object} options + * @return {Deferred<*>} + */ + exec: function (options) { + if (options && options.callback) { + return options.callback(this._getRoute(), this._getArgs(), options.callbackOptions); + } + return this.parent.call('ajax', 'rpc', this._getRoute(), this._getArgs(), options || {}); + }, + /** + * @param {Object} kwargs + * @returns {BaseRPCBuilder} + */ + kwargs: function (kwargs) { + this._kwargs = kwargs; + return this; + }, + /** + * @param {Object} context + * @returns {BaseRPCBuilder} + */ + withContext: function (context) { + this._context = context; + return this; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @returns {Object} + */ + _getArgs: function () { + var kwargs = _.extend({}, this._kwargs); + if (this._context) { + kwargs.context = this._context; + } + return { + method: this._method, + model: this._model, + args: this._args || [], + kwargs: kwargs, + }; + }, + /** + * @private + * @returns {string} + */ + _getRoute: function () { + return this._route; + }, +}); + +var SearchRPCBuilder = BaseRPCBuilder.extend({ + init: function () { + this._super.apply(this, arguments); + this._route = '/web/dataset/search_read'; + this._orderBy = false; + this._domain = []; + this._fields = false; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {Object[]} orderBy + * @returns {BaseRPCBuilder} + */ + orderBy: function (orderBy) { + // todo: serialize properly this + this._orderBy = this._serializeSort(orderBy); + return this; + }, + /** + * @param {Object} context + * @returns {BaseRPCBuilder} + */ + withContext: function (context) { + this._context = context; + return this; + }, + /** + * @param {Object} domain + * @returns {BaseRPCBuilder} + */ + withDomain: function (domain) { + this._domain = domain; + return this; + }, + /** + * @param {string[]} fields + * @returns {BaseRPCBuilder} + */ + withFields: function (fields) { + this._fields = fields; + return this; + }, + /** + * @param {Object} limit + * @returns {BaseRPCBuilder} + */ + withLimit: function (limit) { + this._limit = limit; + return this; + }, + /** + * @param {Object} offset + * @returns {BaseRPCBuilder} + */ + withOffset: function (offset) { + this._offset = offset; + return this; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + * @returns {Object} + */ + _getArgs: function () { + return { + context: this._context || {}, + domain: this._domain, + fields: this._fields, + limit: this._limit, + model: this._model, + sort: this._orderBy + }; + }, + /** + * Helper method, generates a string to describe a ordered by sequence for + * SQL. + * + * For example, [{name: 'foo'}, {name: 'bar', asc: false}] will + * be converted into 'foo ASC, bar DESC' + * + * @param {Object[]} orderBy list of objects {name:..., [asc: ...]} + * @returns {string} + */ + _serializeSort: function (orderBy) { + return _.map(orderBy, function (order) { + return order.name + (order.asc !== false ? ' ASC' : ' DESC'); + }).join(', '); + }, +}); + +var ReadGroupRPCBuilder = SearchRPCBuilder.extend({ + init: function () { + this._super.apply(this, arguments); + this._route = '/web/dataset/call_kw/' + this._model + '/read_group'; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * We need to make sure that arguments are properly set. + * @override + * @returns {Deferred<*>} + */ + exec: function () { + if (!this._groupBy) { + throw new Error("read_group must have a group_by argument"); + } + return this._super.apply(this, arguments); + }, + /** + * @param {string[]} groupBy + * @returns {BaseRPCBuilder} + */ + groupBy: function (groupBy) { + // todo: serialize properly this + this._groupBy = groupBy; + return this; + }, + /** + * @param {boolean} lazy + * @returns {BaseRPCBuilder} + */ + lazy: function (lazy) { + // todo: serialize properly this + this._lazy = lazy; + return this; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + * @returns {Object} + */ + _getArgs: function () { + var kwargs = _.extend({}, this._kwargs, { + context: this._context || {}, + domain: this._domain || [], + fields: this._fields, + groupby: this._groupBy, + lazy: !!this._lazy, + orderby: this._orderBy, + }); + return { + args: this._args || [], + kwargs: kwargs, + method: 'read_group', + model: this._model, + }; + }, +}); + +return { + builders: { + default: BaseRPCBuilder, + search_read: SearchRPCBuilder, + read_group: ReadGroupRPCBuilder, + }, + query: function (params) { + var Builder = this.builders[params.method] || this.builders.default; + return new Builder(params.parent, params); + }, +}; + +}); diff --git a/addons/web/static/test/core/rpc_tests.js b/addons/web/static/test/core/rpc_tests.js new file mode 100644 index 00000000000..beec3fce831 --- /dev/null +++ b/addons/web/static/test/core/rpc_tests.js @@ -0,0 +1,203 @@ +odoo.define('web.rpc_tests', function (require) { +"use strict"; + +var rpc = require('web.rpc'); +var testUtils = require('web.test_utils'); +var Widget = require('web.Widget'); + +function createQuery (params) { + var widget = new Widget(); + + if (params.mockRPC) { + testUtils.intercept(widget, 'call_service', function (event) { + if (event.data.service === 'ajax') { + params.mockRPC(event.data.args[0], JSON.parse(JSON.stringify(event.data.args[1]))); + } + }); + + } + + return rpc.query({ + method: params.method, + model: params.model, + route: params.route, + parent: widget, + }); +} + +QUnit.module('core', {}, function () { + + QUnit.module('RPC Builder'); + + QUnit.test('basic rpc (route)', function (assert) { + assert.expect(1); + var query = createQuery({ + route: '/my/route', + mockRPC: function (route) { + assert.strictEqual(route, '/my/route', + "should call the proper route"); + } + }); + query.exec(); + }); + + QUnit.test('basic rpc, with no context', function (assert) { + assert.expect(1); + var query = createQuery({ + route: '/my/route', + mockRPC: function (route, args) { + assert.notOk('context' in args.kwargs, + "does not automatically add a context"); + } + }); + query.exec(); + }); + + QUnit.test('basic rpc, with context', function (assert) { + assert.expect(1); + var query = createQuery({ + route: '/my/route', + mockRPC: function (route, args) { + assert.deepEqual(args.kwargs.context, {a: 1}, + "properly transfer the context"); + } + }); + query.withContext({a: 1}).exec(); + }); + + QUnit.test('basic rpc, with context, part 2', function (assert) { + assert.expect(1); + var query = createQuery({ + route: '/my/route', + mockRPC: function (route, args) { + assert.deepEqual(args.kwargs.context, {a: 1}, + "properly transfer the context"); + } + }); + query.kwargs({context: {a: 1}}).exec(); + }); + + QUnit.test('basic rpc (method of model)', function (assert) { + assert.expect(3); + var query = createQuery({ + model: 'partner', + method: 'test', + mockRPC: function (route, args) { + assert.strictEqual(route, '/web/dataset/call_kw/partner/test', + "should call the proper route"); + assert.strictEqual(args.model, 'partner', + "should correctly specify the model"); + assert.strictEqual(args.method, 'test', + "should correctly specify the method"); + } + }); + query.exec(); + }); + + QUnit.test('rpc with args and kwargs', function (assert) { + assert.expect(4); + var query = createQuery({ + model: 'partner', + method: 'test', + mockRPC: function (route, args) { + assert.strictEqual(route, '/web/dataset/call_kw/partner/test', + "should call the proper route"); + assert.strictEqual(args.args[0], 'arg1', + "should call with correct args"); + assert.strictEqual(args.args[1], 2, + "should call with correct args"); + assert.strictEqual(args.kwargs.k, 78, + "should call with correct kargs"); + } + }); + query + .args(['arg1', 2]) + .kwargs({k: 78}) + .exec(); + }); + + QUnit.test('rpc with context', function (assert) { + assert.expect(1); + var query = createQuery({ + model: 'partner', + method: 'test', + mockRPC: function (route, args) { + assert.deepEqual(args.kwargs.context, { a: 'hello' }, + "should have correct context"); + }, + }); + query + .withContext({a: 'hello'}) + .exec(); + }); + + QUnit.test('search_read', function (assert) { + assert.expect(1); + var query = createQuery({ + model: 'partner', + method: 'search_read', + mockRPC: function (route, args) { + assert.deepEqual(args, { + context: {}, + domain: ['a', '=', 1], + fields: ['name'], + limit: 32, + model: 'partner', + sort: 'yop ASC, aa DESC', + }, "should have correct args"); + }, + }); + query + .withDomain(['a', '=', 1]) + .withFields(['name']) + .withLimit(32) + .orderBy([{name: 'yop', asc: true}, {name: 'aa', asc: false}]) + .exec(); + }); + + QUnit.test('read_group', function (assert) { + assert.expect(2); + var query = createQuery({ + model: 'partner', + method: 'read_group', + mockRPC: function (route, args) { + assert.deepEqual(args, { + args: [], + kwargs: { + context: {abc: 'def'}, + domain: ['a', '=', 1], + fields: ['name'], + groupby: ['product_id'], + lazy: true, + orderby: false, + }, + method: 'read_group', + model: 'partner', + }, "should have correct args"); + assert.equal(route, '/web/dataset/call_kw/partner/read_group', + "should call correct route"); + }, + }); + query + .withDomain(['a', '=', 1]) + .withFields(['name']) + .groupBy(['product_id']) + .withContext({abc: 'def'}) + .lazy(true) + .exec(); + }); + + QUnit.test('search_read with no domain, nor fields', function (assert) { + assert.expect(2); + createQuery({ + model: 'partner', + method: 'search_read', + mockRPC: function (route, args) { + assert.deepEqual(args.domain, [], "should have [] as default domain"); + assert.strictEqual(args.fields, false, "should have false as default fields"); + }, + }).exec(); + }); +}); + +}); \ No newline at end of file diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 8792b7c988a..295001ccf94 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -69,10 +69,10 @@ + - @@ -141,7 +141,6 @@ - @@ -446,6 +445,7 @@ +