From e6f5d4c211e00ad8ce5f6cae24a65875c2bccf7c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 27 Feb 2012 14:56:26 +0100 Subject: [PATCH] [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 +--------------------------------------- +