[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
This commit is contained in:
Xavier Morel 2012-02-27 14:56:26 +01:00
parent b8efd17bbb
commit e6f5d4c211
7 changed files with 468 additions and 248 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 </rpc>` API, which more closely
matches the API in OpenERP Web's Python side and in OpenObject addons
and removes most stateful behavior of DataSet.
Migration guide
~~~~~~~~~~~~~~~
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.

View File

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

141
doc/source/rpc.rst Normal file
View File

@ -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 </async>`. As a result, all
of them will return :js:class:`Deferred` objects (whether they resolve
those with values or not). Understanding how those work before before
moving on is probably necessary.
High-level API: calling into OpenERP models
-------------------------------------------
Access to OpenERP object methods (made available through XML-RPC from
the server) is done via the :js:class:`openerp.web.Model` class. This
class maps ontwo the OpenERP server objects via two primary methods,
:js:func:`~openerp.web.Model.call` and
:js:func:`~openerp.web.Model.query`.
:js:func:`~openerp.web.Model.call` is a direct mapping to the
corresponding method of the OpenERP server object.
: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<String> fields: list of fields to fetch during
the search
:returns: a :js:class:`~openerp.web.Query` object
representing the search to perform
.. js:class:: openerp.web.Query(fields)
The first set of methods is the "fetching" methods. They perform
RPC queries using the internal data of the object they're called
on.
.. js:function:: openerp.web.Query.all()
Fetches the result of the current
:js:class:`~openerp.web.Query` object's search.
:rtype: Deferred<Array<>>
.. js:function:: openerp.web.Query.first()
Fetches the **first** result of the current
:js:class:`~openerp.web.Query`, or ``null`` if the current
:js:class:`~openerp.web.Query` does have any result.
:rtype: Deferred<Object | null>
.. js:function:: openerp.web.Query.count()
Fetches the number of records the current
:js:class:`~openerp.web.Query` would retrieve.
:rtype: Deferred<Number>
The second set of methods is the "mutator" methods, they create a
**new** :js:class:`~openerp.web.Query` object with the relevant
(internal) attribute either augmented or replaced.
.. js:function:: openerp.web.Query.context(ctx)
Adds the provided ``ctx`` to the query, on top of any existing
context
.. js:function:: openerp.web.Query.filter(domain)
Adds the provided domain to the query, this domain is
``AND``-ed to the existing query domain.
.. js:function:: opeenrp.web.Query.offset(offset)
Sets the provided offset on the query. The new offset
*replaces* the old one.
.. js:function:: openerp.web.Query.limit(limit)
Sets the provided limit on the query. The new limit *replaces*
the old one.
.. js:function:: openerp.web.Query.order_by(fields…)
Overrides the model's natural order with the provided field
specifications. Behaves much like Django's `QuerySet.order_by
<https://docs.djangoproject.com/en/dev/ref/models/querysets/#order-by>`_:
* Takes 1..n field names, in order of most to least importance
(the first field is the first sorting key). Fields are
provided as strings.
* A field specifies an ascending order, unless it is prefixed
with the minus sign "``-``" in which case the field is used
in the descending order
Divergences from Django's sorting include a lack of random sort
(``?`` field) and the inability to "drill down" into relations
for sorting.
Low-level API: RPC calls to Python side
---------------------------------------