_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
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]
}
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)
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)
@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"
* 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
* @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
this.context = context || {};
this.index = null;
this._sort = [];
+ this._model = new openerp.web.Model(model, context);
},
previous: function () {
this.index -= 1;
* @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
* @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)
* @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
* @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
* @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
*/
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
* @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
* @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
* @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
* @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);
},
/**
*
* @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.
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
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;
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";
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));
});
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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
+ });
});
});
});
--- /dev/null
+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.
Contents:
.. toctree::
+ :maxdepth: 1
+
+ changelog-6.2
+
+ async
+ rpc
+
+Older stuff
+-----------
+
+.. toctree::
:maxdepth: 2
getting-started
--- /dev/null
+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
+---------------------------------------
+