[ADD] grouping to the Model/Query API, but filtering looks completely broken
bzr revid: xmo@openerp.com-20120302154759-8ihi5p1ffygiyhw3
This commit is contained in:
parent
2c2df2b325
commit
9b0cc92b66
|
@ -9,13 +9,13 @@ 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({
|
||||
|
@ -103,6 +103,38 @@ openerp.web.Query = openerp.web.Class.extend({
|
|||
'search_count', [this._filter], {
|
||||
context: this._model.context(this._context)});
|
||||
},
|
||||
/**
|
||||
* Performs a groups read according to the provided grouping criterion
|
||||
*
|
||||
* @param {String|Array<String>} grouping
|
||||
* @returns {jQuery.Deferred<Array<openerp.web.data.Group>> | null}
|
||||
*/
|
||||
group_by: function (grouping) {
|
||||
if (grouping === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(grouping instanceof Array)) {
|
||||
grouping = _.toArray(arguments);
|
||||
}
|
||||
if (_.isEmpty(grouping)) { return null; }
|
||||
|
||||
var self = this;
|
||||
return this._model.call('read_group', {
|
||||
groupby: grouping,
|
||||
fields: _.uniq(grouping.concat(this._fields || [])),
|
||||
domain: this._model.domain(this._filter),
|
||||
context: this._model.context(this._context),
|
||||
offset: this._offset,
|
||||
limit: this._limit,
|
||||
orderby: openerp.web.serialize_sort(this._order_by) || false
|
||||
}).pipe(function (results) {
|
||||
return _(results).map(function (result) {
|
||||
return new openerp.web.data.Group(
|
||||
self._model.name, grouping[0], result);
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Creates a new query with the union of the current query's context and
|
||||
* the new context.
|
||||
|
@ -152,9 +184,12 @@ openerp.web.Query = openerp.web.Class.extend({
|
|||
* @param {String...} fields ordering clauses
|
||||
* @returns {openerp.web.Query}
|
||||
*/
|
||||
order_by: function () {
|
||||
if (arguments.length === 0) { return this; }
|
||||
return this.clone({order_by: _.toArray(arguments)});
|
||||
order_by: function (fields) {
|
||||
if (!fields instanceof Array) {
|
||||
fields = _.toArray(arguments);
|
||||
}
|
||||
if (_.isEmpty(fields)) { return this; }
|
||||
return this.clone({order_by: fields});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -345,6 +380,64 @@ openerp.web.Traverser = openerp.web.Class.extend(/** @lends openerp.web.Traverse
|
|||
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility objects, should never need to be instantiated from outside of this
|
||||
* module
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
openerp.web.data = {
|
||||
Group: openerp.web.Class.extend(/** @lends openerp.web.data.Group# */{
|
||||
/**
|
||||
* @constructs openerp.web.data.Group
|
||||
* @extends openerp.web.Class
|
||||
*/
|
||||
init: function (model, grouping_field, read_group_group) {
|
||||
// In cases where group_by_no_leaf and no group_by, the result of
|
||||
// read_group has aggregate fields but no __context or __domain.
|
||||
// Create default (empty) values for those so that things don't break
|
||||
var fixed_group = _.extend(
|
||||
{__context: {group_by: []}, __domain: []},
|
||||
read_group_group);
|
||||
|
||||
var aggregates = {};
|
||||
_(fixed_group).each(function (value, key) {
|
||||
if (key.indexOf('__') === 0
|
||||
|| key === grouping_field
|
||||
|| key === grouping_field + '_count') {
|
||||
return;
|
||||
}
|
||||
aggregates[key] = value || 0;
|
||||
});
|
||||
|
||||
this.model = new openerp.web.Model(
|
||||
model, fixed_group.__context, fixed_group.__domain);
|
||||
|
||||
var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0;
|
||||
var leaf_group = fixed_group.__context.group_by.length === 0;
|
||||
this.attributes = {
|
||||
grouped_on: grouping_field,
|
||||
// if terminal group (or no group) and group_by_no_leaf => use group.__count
|
||||
length: group_size,
|
||||
value: fixed_group[grouping_field],
|
||||
// A group is open-able if it's not a leaf in group_by_no_leaf mode
|
||||
has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']),
|
||||
|
||||
aggregates: aggregates
|
||||
};
|
||||
},
|
||||
get: function (key) {
|
||||
return this.attributes[key];
|
||||
},
|
||||
subgroups: function () {
|
||||
return this.model.query().group_by(this.model.context().group_by);
|
||||
},
|
||||
query: function () {
|
||||
return this.model.query.apply(this.model, arguments);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{
|
||||
/**
|
||||
* Management interface between views and grouped collections of OpenERP
|
||||
|
@ -368,181 +461,51 @@ openerp.web.DataGroup = openerp.web.OldWidget.extend( /** @lends openerp.web.Da
|
|||
*/
|
||||
init: function(parent, model, domain, context, group_by, level) {
|
||||
this._super(parent, null);
|
||||
if (group_by) {
|
||||
if (group_by.length || context['group_by_no_leaf']) {
|
||||
return new openerp.web.ContainerDataGroup( this, model, domain, context, group_by, level);
|
||||
} else {
|
||||
return new openerp.web.GrouplessDataGroup( this, model, domain, context, level);
|
||||
}
|
||||
}
|
||||
|
||||
this.model = model;
|
||||
this.model = new openerp.web.Model(model, context, domain);
|
||||
this.group_by = group_by;
|
||||
this.context = context;
|
||||
this.domain = domain;
|
||||
|
||||
this.level = level || 0;
|
||||
},
|
||||
cls: 'DataGroup'
|
||||
});
|
||||
openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.ContainerDataGroup# */ {
|
||||
/**
|
||||
*
|
||||
* @constructs openerp.web.ContainerDataGroup
|
||||
* @extends openerp.web.DataGroup
|
||||
*
|
||||
* @param session
|
||||
* @param model
|
||||
* @param domain
|
||||
* @param context
|
||||
* @param group_by
|
||||
* @param level
|
||||
*/
|
||||
init: function (parent, model, domain, context, group_by, level) {
|
||||
this._super(parent, model, domain, context, null, level);
|
||||
|
||||
this.group_by = group_by;
|
||||
},
|
||||
/**
|
||||
* The format returned by ``read_group`` is absolutely dreadful:
|
||||
*
|
||||
* * A ``__context`` key provides future grouping levels
|
||||
* * A ``__domain`` key provides the domain for the next search
|
||||
* * The current grouping value is provided through the name of the
|
||||
* current grouping name e.g. if currently grouping on ``user_id``, then
|
||||
* the ``user_id`` value for this group will be provided through the
|
||||
* ``user_id`` key.
|
||||
* * Similarly, the number of items in the group (not necessarily direct)
|
||||
* is provided via ``${current_field}_count``
|
||||
* * Other aggregate fields are just dumped there
|
||||
*
|
||||
* This function slightly improves the grouping records by:
|
||||
*
|
||||
* * Adding a ``grouped_on`` property providing the current grouping field
|
||||
* * Adding a ``value`` and a ``length`` properties which replace the
|
||||
* ``$current_field`` and ``${current_field}_count`` ones
|
||||
* * Moving aggregate values into an ``aggregates`` property object
|
||||
*
|
||||
* Context and domain keys remain as-is, they should not be used externally
|
||||
* but in case they're needed...
|
||||
*
|
||||
* @param {Object} group ``read_group`` record
|
||||
*/
|
||||
transform_group: function (group) {
|
||||
var field_name = this.group_by[0];
|
||||
// In cases where group_by_no_leaf and no group_by, the result of
|
||||
// read_group has aggregate fields but no __context or __domain.
|
||||
// Create default (empty) values for those so that things don't break
|
||||
var fixed_group = _.extend(
|
||||
{__context: {group_by: []}, __domain: []},
|
||||
group);
|
||||
|
||||
var aggregates = {};
|
||||
_(fixed_group).each(function (value, key) {
|
||||
if (key.indexOf('__') === 0
|
||||
|| key === field_name
|
||||
|| key === field_name + '_count') {
|
||||
return;
|
||||
}
|
||||
aggregates[key] = value || 0;
|
||||
});
|
||||
|
||||
var group_size = fixed_group[field_name + '_count'] || fixed_group.__count || 0;
|
||||
var leaf_group = fixed_group.__context.group_by.length === 0;
|
||||
return {
|
||||
__context: fixed_group.__context,
|
||||
__domain: fixed_group.__domain,
|
||||
|
||||
grouped_on: field_name,
|
||||
// if terminal group (or no group) and group_by_no_leaf => use group.__count
|
||||
length: group_size,
|
||||
value: fixed_group[field_name],
|
||||
// A group is openable if it's not a leaf in group_by_no_leaf mode
|
||||
openable: !(leaf_group && this.context['group_by_no_leaf']),
|
||||
|
||||
aggregates: aggregates
|
||||
};
|
||||
},
|
||||
fetch: function (fields) {
|
||||
// internal method
|
||||
var d = new $.Deferred();
|
||||
var self = this;
|
||||
|
||||
this.rpc('/web/group/read', {
|
||||
model: this.model,
|
||||
context: this.context,
|
||||
domain: this.domain,
|
||||
fields: _.uniq(this.group_by.concat(fields)),
|
||||
group_by_fields: this.group_by,
|
||||
sort: openerp.web.serialize_sort(this.sort)
|
||||
}, function () { }).then(function (response) {
|
||||
var data_groups = _(response).map(
|
||||
_.bind(self.transform_group, self));
|
||||
self.groups = data_groups;
|
||||
d.resolveWith(self, [data_groups]);
|
||||
}, function () {
|
||||
d.rejectWith.apply(d, [self, arguments]);
|
||||
});
|
||||
return d.promise();
|
||||
},
|
||||
/**
|
||||
* The items of a list have the following properties:
|
||||
*
|
||||
* ``length``
|
||||
* the number of records contained in the group (and all of its
|
||||
* sub-groups). This does *not* provide the size of the "next level"
|
||||
* of the group, unless the group is terminal (no more groups within
|
||||
* it).
|
||||
* ``grouped_on``
|
||||
* the name of the field this level was grouped on, this is mostly
|
||||
* used for display purposes, in order to know the name of the current
|
||||
* level of grouping. The ``grouped_on`` should be the same for all
|
||||
* objects of the list.
|
||||
* ``value``
|
||||
* the value which led to this group (this is the value all contained
|
||||
* records have for the current ``grouped_on`` field name).
|
||||
* ``aggregates``
|
||||
* a mapping of other aggregation fields provided by ``read_group``
|
||||
*
|
||||
* @param {Array} fields the list of fields to aggregate in each group, can be empty
|
||||
* @param {Function} ifGroups function executed if any group is found (DataGroup.group_by is non-null and non-empty), called with a (potentially empty) list of groups as parameters.
|
||||
* @param {Function} ifRecords function executed if there is no grouping left to perform, called with a DataSet instance as parameter
|
||||
*/
|
||||
list: function (fields, ifGroups, ifRecords) {
|
||||
var self = this;
|
||||
this.fetch(fields).then(function (group_records) {
|
||||
ifGroups(_(group_records).map(function (group) {
|
||||
var child_context = _.extend({}, self.context, group.__context);
|
||||
$.when(this.model.query(fields)
|
||||
.order_by(this.sort)
|
||||
.group_by(this.group_by)).then(function (groups) {
|
||||
if (!groups) {
|
||||
console.log(self.domain);
|
||||
console.log(self.model.domain());
|
||||
ifRecords(_.extend(
|
||||
new openerp.web.DataSetSearch(self, self.model.name),
|
||||
{domain: self.model.domain(), context: self.model.context(),
|
||||
_sort: self.sort}));
|
||||
return;
|
||||
}
|
||||
ifGroups(_(groups).map(function (group) {
|
||||
var child_context = _.extend(
|
||||
{}, self.model.context(), group.model.context());
|
||||
return _.extend(
|
||||
new openerp.web.DataGroup(
|
||||
self, self.model, group.__domain,
|
||||
self, self.model.name, group.model.domain(),
|
||||
child_context, child_context.group_by,
|
||||
self.level + 1),
|
||||
group, {sort: self.sort});
|
||||
{
|
||||
__context: child_context,
|
||||
__domain: group.model.domain(),
|
||||
grouped_on: group.get('grouped_on'),
|
||||
length: group.get('length'),
|
||||
value: group.get('value'),
|
||||
openable: group.get('has_children'),
|
||||
aggregates: group.get('aggregates')
|
||||
}, {sort: self.sort});
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend( /** @lends openerp.web.GrouplessDataGroup# */ {
|
||||
/**
|
||||
*
|
||||
* @constructs openerp.web.GrouplessDataGroup
|
||||
* @extends openerp.web.DataGroup
|
||||
*
|
||||
* @param session
|
||||
* @param model
|
||||
* @param domain
|
||||
* @param context
|
||||
* @param level
|
||||
*/
|
||||
init: function (parent, model, domain, context, level) {
|
||||
this._super(parent, model, domain, context, null, level);
|
||||
},
|
||||
list: function (fields, ifGroups, ifRecords) {
|
||||
ifRecords(_.extend(
|
||||
new openerp.web.DataSetSearch(this, this.model),
|
||||
{domain: this.domain, context: this.context, _sort: this.sort}));
|
||||
}
|
||||
});
|
||||
openerp.web.ContainerDataGroup = openerp.web.DataGroup.extend({ });
|
||||
openerp.web.GrouplessDataGroup = openerp.web.DataGroup.extend({ });
|
||||
|
||||
openerp.web.StaticDataGroup = openerp.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ {
|
||||
/**
|
||||
* A specialization of groupless data groups, relying on a single static
|
||||
|
|
|
@ -12,6 +12,51 @@ and removes most stateful behavior of DataSet.
|
|||
Migration guide
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
* Actual arbitrary RPC calls can just be remapped on a
|
||||
:js:class:`~openerp.web.Model` instance:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
dataset.call(method, args)
|
||||
|
||||
or
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
dataset.call_and_eval(method, args)
|
||||
|
||||
can be replaced by calls to :js:func:`openerp.web.Model.call`:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
model.call(method, args)
|
||||
|
||||
If callbacks are passed directly to the older methods, they need to
|
||||
be added to the new one via ``.then()``.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``context_index`` and ``domain_index`` features were not
|
||||
ported, context and domain now need to be passed in "in full",
|
||||
they won't be automatically filled with the user's current
|
||||
context.
|
||||
|
||||
* Shorcut methods (``name_get``, ``name_search``, ``unlink``,
|
||||
``write``, ...) should be ported to
|
||||
:js:func:`openerp.web.Model.call`, using the server's original
|
||||
signature. On the other hand, the non-shortcut equivalents can now
|
||||
use keyword arguments (see :js:func:`~openerp.web.Model.call`'s
|
||||
signature for details)
|
||||
|
||||
* ``read_slice``, which allowed a single round-trip to perform a
|
||||
search and a read, should be reimplemented via
|
||||
:js:class:`~openerp.web.Query` objects (see:
|
||||
:js:func:`~openerp.web.Model.query`) for clearer and simpler
|
||||
code. ``read_index`` should be replaced by a
|
||||
:js:class:`~openerp.web.Query` as well, combining
|
||||
:js:func:`~openerp.web.Query.offset` and
|
||||
:js:func:`~openerp.web.Query.first`.
|
||||
|
||||
Rationale
|
||||
~~~~~~~~~
|
||||
|
||||
|
@ -35,3 +80,29 @@ API simplification
|
|||
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.
|
||||
|
||||
DataGroup -> also Model
|
||||
-----------------------
|
||||
|
||||
Alongside the deprecation of ``DataSet`` for
|
||||
:js:class:`~openerp.web.Model`, OpenERP Web 6.2 also deprecates
|
||||
``DataGroup`` and its subtypes in favor of a single method on
|
||||
:js:class:`~openerp.web.Query`:
|
||||
:js:func:`~openerp.web.Query.group_by`.
|
||||
|
||||
Migration guide
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Rationale
|
||||
~~~~~~~~~
|
||||
|
||||
While the ``DataGroup`` API worked (mostly), it is quite odd and
|
||||
alien-looking, a bit too Smalltalk-inspired (behaves like a
|
||||
self-contained flow-control structure for reasons which may or may not
|
||||
have been good).
|
||||
|
||||
Because it is heavily related to ``DataSet`` (as it *yields*
|
||||
``DataSet`` objects), deprecating ``DataSet`` automatically deprecates
|
||||
``DataGroup`` (if we want to stay consistent), which is a good time to
|
||||
make the API more imperative and look more like what most developers
|
||||
are used to.
|
||||
|
|
|
@ -128,6 +128,17 @@ around and use them differently/add new specifications on them.
|
|||
|
||||
:rtype: Deferred<Number>
|
||||
|
||||
.. js:function:: openerp.web.Query.group_by(grouping...)
|
||||
|
||||
Fetches the groups for the query, using the first specified
|
||||
grouping parameter
|
||||
|
||||
:param Array<String> grouping: Lists the levels of grouping
|
||||
asked of the server. Grouping
|
||||
can actually be an array or
|
||||
varargs.
|
||||
:rtype: Deferred<Array<openerp.web.Group>> | null
|
||||
|
||||
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.
|
||||
|
@ -170,6 +181,65 @@ around and use them differently/add new specifications on them.
|
|||
(``?`` field) and the inability to "drill down" into relations
|
||||
for sorting.
|
||||
|
||||
Aggregation (grouping)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenERP has powerful grouping capacities, but they are kind-of strange
|
||||
in that they're recursive, and level n+1 relies on data provided
|
||||
directly by the grouping at level n. As a result, while ``read_group``
|
||||
works it's not a very intuitive API.
|
||||
|
||||
OpenERP Web 6.2 eschews direct calls to ``read_group`` in favor of
|
||||
calling a method of :js:class:`~openerp.web.Query`, `much in the way
|
||||
it is one in SQLAlchemy
|
||||
<http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.group_by>`_ [#]_:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
some_query.group_by(['field1', 'field2']).then(function (groups) {
|
||||
// do things with the fetched groups
|
||||
});
|
||||
|
||||
This method is asynchronous when provided with 1..n fields (to group
|
||||
on) as argument, but it can also be called without any field (empty
|
||||
fields collection or nothing at all). In this case, instead of
|
||||
returning a Deferred object it will return ``null``.
|
||||
|
||||
When grouping criterion come from a third-party and may or may not
|
||||
list fields (e.g. could be an empty list), this provides two ways to
|
||||
test the presence of actual subgroups (versus the need to perform a
|
||||
regular query for records):
|
||||
|
||||
* A check on ``group_by``'s result and two completely separate code
|
||||
paths
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var groups;
|
||||
if (groups = some_query.group_by(gby)) {
|
||||
groups.then(function (gs) {
|
||||
// groups
|
||||
});
|
||||
}
|
||||
// no groups
|
||||
|
||||
* Or a more coherent code path using :js:func:`when`'s ability to
|
||||
coerce values into deferreds:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
$.when(some_query.group_by(gby)).then(function (groups) {
|
||||
if (!groups) {
|
||||
// No grouping
|
||||
} else {
|
||||
// grouping, even if there are no groups (groups
|
||||
// itself could be an empty array)
|
||||
}
|
||||
});
|
||||
|
||||
The result of a (successful) :js:func:`~openerp.web.Query.group_by` is
|
||||
an array of :js:class:`~openerp.web.data.Group`.
|
||||
|
||||
Synchronizing views (provisional)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -268,5 +338,8 @@ For instance, to call the ``eval_domain_and_context`` of the
|
|||
// handle result
|
||||
});
|
||||
|
||||
.. [#] with a small twist: SQLAlchemy's ``orm.query.Query.group_by``
|
||||
is not terminal, it returns a query which can still be altered.
|
||||
|
||||
.. [#] except for ``context``, which is extracted and stored in the
|
||||
request object itself.
|
||||
|
|
Loading…
Reference in New Issue