[ADD] web: introduce new RPC framework
With this commit, we introduce a new way to perform a RPC. During the history of the Odoo web client, many ways to do a rpc have been introduced, for various reasons. Let us mention the Model class (to perform a call on a specific method from a model, the web.ajax module (low level primitive), the dataset.call method, the this.rpc method (was removed at some point), the session.rpc method and maybe some others. With this commit, we reintroduce the this.rpc method, with a 'mid-level' API: not really high level (not automatic domain/context calculations), but still higher level than direct web.ajax calls. This new method is intended to replace all other RPCs in Odoo web client. Of course, web.ajax will still be available, but it is discouraged to use it directly. Using the rpc method means that the rpcs are done through an event bubbling up, which means that the call is testable.
This commit is contained in:
committed by
Aaron Bohy
parent
00a172b40d
commit
0df9968433
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -69,10 +69,10 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/core/ajax.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/time.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/mixins.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/rpc.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/widget.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/registry.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/session.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/_deprecated/model.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/dom.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/concurrency.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/utils.js"></script>
|
||||
@@ -141,7 +141,6 @@
|
||||
<!-- Special case: core.js declares $.browser needed by ba-bbq -->
|
||||
<script type="text/javascript" src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/_deprecated/data_model.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/domain.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/pyeval.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/chrome/action_manager.js"></script>
|
||||
@@ -446,6 +445,7 @@
|
||||
<script type="text/javascript" src="/web/static/test/core/registry_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/core/pyeval_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/core/class_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/core/rpc_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/core/domain_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/core/time_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/core/concurrency_tests.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user