[ADD] Model API, reimplement DataSet/DataSetSearch on top of it (as much as possible)
authorXavier Morel <xmo@openerp.com>
Mon, 27 Feb 2012 13:56:26 +0000 (14:56 +0100)
committerXavier Morel <xmo@openerp.com>
Mon, 27 Feb 2012 13:56:26 +0000 (14:56 +0100)
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
addons/web/static/src/js/core.js
addons/web/static/src/js/data.js
addons/web/static/test/fulltest/dataset.js
doc/source/changelog-6.2.rst [new file with mode: 0644]
doc/source/index.rst
doc/source/rpc.rst [new file with mode: 0644]

index eaf3d67..6a143d5 100644 (file)
@@ -820,11 +820,6 @@ 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)
     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
@@ -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"
index 10f3167..b078002 100644 (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
index 8e20095..58e491c 100644 (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";
index e9513b0..1b09bb7 100644 (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
+            });
         });
     });
 });
diff --git a/doc/source/changelog-6.2.rst b/doc/source/changelog-6.2.rst
new file mode 100644 (file)
index 0000000..2b49247
--- /dev/null
@@ -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.
index efa4d1c..9975663 100644 (file)
@@ -9,6 +9,17 @@ Welcome to OpenERP Web's documentation!
 Contents:
 
 .. toctree::
+    :maxdepth: 1
+
+    changelog-6.2
+
+    async
+    rpc
+
+Older stuff
+-----------
+
+.. toctree::
    :maxdepth: 2
 
    getting-started
diff --git a/doc/source/rpc.rst b/doc/source/rpc.rst
new file mode 100644 (file)
index 0000000..8ed72e8
--- /dev/null
@@ -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
+---------------------------------------
+