diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index 08ddf9d88e0..771325e9281 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -841,7 +841,7 @@ QUnit.test("Dashboard should use correct groupby", async function (assert) {
'' +
'',
mockRPC: function (route, args) {
- if (args.method === 'read_group') {
+ if (args.method === 'web_read_group') {
assert.deepEqual(args.kwargs.groupby, ['bar'],
'user defined groupby should have precedence on action groupby');
}
diff --git a/addons/web/models/models.py b/addons/web/models/models.py
index 12e6a422507..271f8b831da 100644
--- a/addons/web/models/models.py
+++ b/addons/web/models/models.py
@@ -17,6 +17,40 @@ class IrActionsActWindowView(models.Model):
class Base(models.AbstractModel):
_inherit = 'base'
+ @api.model
+ def web_read_group(self, domain, fields, groupby, limit=None, offset=0, orderby=False,
+ lazy=True):
+ """
+ Returns the result of a read_group and the total number of groups matching the search
+ domain.
+
+ :param domain: search domain
+ :param fields: list of fields to read (see ``fields``` param of ``read_group``)
+ :param groupby: list of fields to group on (see ``groupby``` param of ``read_group``)
+ :param limit: see ``limit`` param of ``read_group``
+ :param offset: see ``offset`` param of ``read_group``
+ :param orderby: see ``orderby`` param of ``read_group``
+ :param lazy: see ``lazy`` param of ``read_group``
+ :return: {
+ 'groups': array of read groups
+ 'length': total number of groups
+ }
+ """
+ groups = self.read_group(domain, fields, groupby, offset=offset, limit=limit,
+ orderby=orderby, lazy=lazy)
+
+ if not groups:
+ length = 0
+ elif limit and len(groups) == limit:
+ all_groups = self.read_group(domain, ['display_name'], groupby, lazy=True)
+ length = len(all_groups)
+ else:
+ length = len(groups) + offset
+ return {
+ 'groups': groups,
+ 'length': length
+ }
+
@api.model
def read_progress_bar(self, domain, group_by, progress_bar):
"""
diff --git a/addons/web/static/src/js/core/rpc.js b/addons/web/static/src/js/core/rpc.js
index 21ec43e3d95..802f7082cad 100644
--- a/addons/web/static/src/js/core/rpc.js
+++ b/addons/web/static/src/js/core/rpc.js
@@ -37,6 +37,7 @@ return {
buildQuery: function (options) {
var route;
var params = options.params || {};
+ var orderBy;
if (options.route) {
route = options.route;
} else if (options.model && options.method) {
@@ -50,7 +51,7 @@ return {
params.kwargs.context = options.context || params.context || params.kwargs.context;
}
- if (options.method === 'read_group') {
+ if (options.method === 'read_group' || options.method === 'web_read_group') {
if (!(params.args && params.args[0] !== undefined)) {
params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
}
@@ -64,7 +65,7 @@ return {
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
// In kwargs, we look for "orderby" rather than "orderBy" (note the absence of capital B),
// since the Python argument to the actual function is "orderby".
- var orderBy = options.orderBy || params.orderBy || params.kwargs.orderby;
+ orderBy = options.orderBy || params.orderBy || params.kwargs.orderby;
params.kwargs.orderby = orderBy ? this._serializeSort(orderBy) : orderBy;
params.kwargs.lazy = 'lazy' in options ? options.lazy : params.lazy;
}
@@ -77,7 +78,7 @@ return {
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
// In kwargs, we look for "order" rather than "orderBy" since the Python
// argument to the actual function is "order".
- var orderBy = options.orderBy || params.orderBy || params.kwargs.order;
+ orderBy = options.orderBy || params.orderBy || params.kwargs.order;
params.kwargs.order = orderBy ? this._serializeSort(orderBy) : orderBy;
}
@@ -88,7 +89,7 @@ return {
params.fields = options.fields || params.fields;
params.limit = options.limit || params.limit;
params.offset = options.offset || params.offset;
- var orderBy = options.orderBy || params.orderBy;
+ orderBy = options.orderBy || params.orderBy;
params.sort = orderBy ? this._serializeSort(orderBy) : orderBy;
params.context = options.context || params.context || {};
}
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 19d2fd57892..064f7b84a52 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -148,25 +148,28 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
*/
renderPager: function ($node, options) {
var self = this;
- var data = this.model.get(this.handle, {raw: true});
- this.pager = new Pager(this, data.count, data.offset + 1, data.limit, options);
+ var params = this._getPagerParams();
+ this.pager = new Pager(this, params.size, params.current_min, params.limit, options);
this.pager.on('pager_changed', this, function (newState) {
- var self = this;
this.pager.disable();
- data = this.model.get(this.handle, {raw: true});
+ var data = this.model.get(this.handle, {raw: true});
var limitChanged = (data.limit !== newState.limit);
- this.reload({limit: newState.limit, offset: newState.current_min - 1})
- .then(function () {
- // Reset the scroll position to the top on page changed only
- if (!limitChanged) {
- self.trigger_up('scrollTo', {top: 0});
- }
- })
- .then(this.pager.enable.bind(this.pager));
+ var reloadParams;
+ if (data.groupedBy && data.groupedBy.length) {
+ reloadParams = {groupsLimit: newState.limit, groupsOffset: newState.current_min - 1};
+ } else {
+ reloadParams = {limit: newState.limit, offset: newState.current_min - 1};
+ }
+ this.reload(reloadParams).then(function () {
+ // reset the scroll position to the top on page changed only
+ if (!limitChanged) {
+ self.trigger_up('scrollTo', {top: 0});
+ }
+ }).then(this.pager.enable.bind(this.pager));
});
- return this.pager.appendTo($node).then(function() {
- self._updatePager(); // to force proper visibility
+ return this.pager.appendTo($node).then(function () {
+ self._updatePager(); // to force proper visibility
});
},
/**
@@ -412,6 +415,22 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
resIds: env.ids,
});
},
+ /**
+ * Return the params (current_min, limit and size) to pass to the pager,
+ * according to the current state.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _getPagerParams: function () {
+ var state = this.model.get(this.handle, {raw: true});
+ var isGrouped = state.groupedBy && state.groupedBy.length;
+ return {
+ current_min: (isGrouped ? state.groupsOffset : state.offset) + 1,
+ limit: isGrouped ? state.groupsLimit : state.limit,
+ size: isGrouped ? state.groupsCount : state.count,
+ };
+ },
/**
* Returns the new sidebar env
*
@@ -425,6 +444,16 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
model: this.modelName,
};
},
+ /**
+ * Determine whether or not the pager must be displayed (probably depending
+ * on the current state). Controllers must override this to implement their
+ * own logic.
+ *
+ * @private
+ */
+ _isPagerVisible: function () {
+ return true;
+ },
/**
* Helper function to display a warning that some fields have an invalid
* value. This is used when a save operation cannot be completed.
@@ -533,23 +562,14 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
},
/**
- * Helper method, to make sure the information displayed by the pager is up
- * to date.
+ * Update the pager with the current state.
+ *
+ * @private
*/
_updatePager: function () {
if (this.pager) {
- var data = this.model.get(this.handle, {raw: true});
- this.pager.updateState({
- current_min: data.offset + 1,
- size: data.count,
- });
- var isRecord = data.type === 'record';
- var hasData = !!data.count;
- var isGrouped = data.groupedBy ? !!data.groupedBy.length : false;
- var isNew = this.model.isNew(this.handle);
- var isPagerVisible = isRecord ? !isNew : (hasData && !isGrouped);
-
- this.pager.do_toggle(isPagerVisible);
+ this.pager.updateState(this._getPagerParams());
+ this.pager.do_toggle(this._isPagerVisible());
}
},
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 35435a3e500..d07ec61abdd 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -88,7 +88,6 @@ var concurrency = require('web.concurrency');
var Context = require('web.Context');
var core = require('web.core');
var Domain = require('web.Domain');
-var fieldUtils = require('web.field_utils');
var session = require('web.session');
var utils = require('web.utils');
var viewUtils = require('web.viewUtils');
@@ -639,6 +638,9 @@ var BasicModel = AbstractModel.extend({
getDomain: element.getDomain,
getFieldNames: element.getFieldNames,
groupedBy: element.groupedBy,
+ groupsCount: element.groupsCount,
+ groupsLimit: element.groupsLimit,
+ groupsOffset: element.groupsOffset,
id: element.id,
isDirty: element.isDirty,
isOpen: element.isOpen,
@@ -3732,6 +3734,9 @@ var BasicModel = AbstractModel.extend({
fields: fields,
fieldsInfo: params.fieldsInfo,
groupedBy: params.groupedBy || [],
+ groupsCount: 0,
+ groupsLimit: type === 'list' && params.groupsLimit || null,
+ groupsOffset: 0,
id: _.uniqueId(params.modelName + '_'),
isOpen: params.isOpen,
limit: type === 'record' ? 1 : params.limit,
@@ -4245,20 +4250,25 @@ var BasicModel = AbstractModel.extend({
var groupByField = list.groupedBy[0];
var rawGroupBy = groupByField.split(':')[0];
var fields = _.uniq(list.getFieldNames().concat(rawGroupBy));
- var orderedBy = _.filter(list.orderedBy, function(order){
+ var orderedBy = _.filter(list.orderedBy, function (order) {
return order.name === rawGroupBy || list.fields[order.name].group_operator !== undefined;
});
+ var openGroupsLimit = list.groupsLimit || self.OPEN_GROUP_LIMIT;
return this._rpc({
model: list.model,
- method: 'read_group',
+ method: 'web_read_group',
fields: fields,
domain: list.domain,
context: list.context,
groupBy: list.groupedBy,
+ limit: list.groupsLimit,
+ offset: list.groupsOffset,
orderBy: orderedBy,
lazy: true,
})
- .then(function (groups) {
+ .then(function (result) {
+ var groups = result.groups;
+ list.groupsCount = result.length;
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
@@ -4321,7 +4331,7 @@ var BasicModel = AbstractModel.extend({
// form view) are reloaded
newGroup.limit = oldGroup.limit + oldGroup.loadMoreOffset;
self.localData[newGroup.id] = newGroup;
- } else if (!newGroup.openGroupByDefault || openGroupCount >= self.OPEN_GROUP_LIMIT) {
+ } else if (!newGroup.openGroupByDefault || openGroupCount >= openGroupsLimit) {
newGroup.isOpen = false;
} else if ('__fold' in group) {
newGroup.isOpen = !group.__fold;
@@ -4486,6 +4496,12 @@ var BasicModel = AbstractModel.extend({
if (options.offset !== undefined) {
this._setOffset(element.id, options.offset);
}
+ if (options.groupsLimit !== undefined) {
+ element.groupsLimit = options.groupsLimit;
+ }
+ if (options.groupsOffset !== undefined) {
+ element.groupsOffset = options.groupsOffset;
+ }
if (options.loadMoreOffset !== undefined) {
element.loadMoreOffset = options.loadMoreOffset;
} else {
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 9997e144ca3..a351c8fa04f 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -347,6 +347,15 @@ var FormController = BasicController.extend({
this._super.apply(this, arguments);
this.renderer.enableButtons();
},
+ /**
+ * Only display the pager if we are not on a new record.
+ *
+ * @override
+ * @private
+ */
+ _isPagerVisible: function () {
+ return !this.model.isNew(this.handle);
+ },
/**
* Hook method, called when record(s) has been deleted.
*
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 58174c7812d..4ea823ed062 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -137,6 +137,16 @@ var KanbanController = BasicController.extend({
}
return this.renderer.updateRecord(this.model.get(id));
},
+ /**
+ * Only display the pager in the ungrouped case, with data.
+ *
+ * @override
+ * @private
+ */
+ _isPagerVisible: function () {
+ var state = this.model.get(this.handle, {raw: true});
+ return !!(state.count && !state.groupedBy.length);
+ },
/**
* @private
* @param {Widget} kanbanRecord
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 217f02c8fd1..a7b71ad4cf2 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -11,7 +11,6 @@ var core = require('web.core');
var BasicController = require('web.BasicController');
var DataExport = require('web.DataExport');
var Dialog = require('web.Dialog');
-var pyUtils = require('web.py_utils');
var Sidebar = require('web.Sidebar');
var _t = core._t;
@@ -342,6 +341,16 @@ var ListController = BasicController.extend({
var record = this.model.get(this.handle);
return _.extend(env, {domain: record.getDomain()});
},
+ /**
+ * Only display the pager when there are data to display.
+ *
+ * @override
+ * @private
+ */
+ _isPagerVisible: function () {
+ var state = this.model.get(this.handle, {raw: true});
+ return !!state.count;
+ },
/**
* Allows to change the mode of a single row.
*
@@ -395,11 +404,9 @@ var ListController = BasicController.extend({
* @returns {Promise}
*/
_update: function () {
- var self = this;
- return this._super.apply(this, arguments).then(function () {
- self._toggleSidebar();
- self._toggleCreateButton();
- });
+ return this._super.apply(this, arguments)
+ .then(this._toggleSidebar.bind(this))
+ .then(this._toggleCreateButton.bind(this));
},
/**
* This helper simply makes sure that the control panel buttons matches the
diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js
index 163ac850817..e65a8c4adc4 100644
--- a/addons/web/static/src/js/views/list/list_view.js
+++ b/addons/web/static/src/js/views/list/list_view.js
@@ -38,6 +38,7 @@ var ListView = BasicView.extend({
var selectedRecords = []; // there is no selected records by default
var mode = this.arch.attrs.editable && !params.readonly ? "edit" : "readonly";
+ var expandGroups = !!JSON.parse(this.arch.attrs.expand || "0");
this.controllerParams.editable = this.arch.attrs.editable;
this.controllerParams.hasSidebar = params.hasSidebar;
@@ -54,8 +55,10 @@ var ListView = BasicView.extend({
this.rendererParams.addCreateLineInGroups = this.rendererParams.editable && this.controllerParams.activeActions.create;
this.loadParams.limit = this.loadParams.limit || 80;
- this.loadParams.openGroupByDefault = !!JSON.parse(this.arch.attrs.expand || "0");
+ this.loadParams.openGroupByDefault = expandGroups;
this.loadParams.type = 'list';
+ var groupsLimit = parseInt(this.arch.attrs.groups_limit, 10);
+ this.loadParams.groupsLimit = groupsLimit || (expandGroups ? 10 : 80);
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 0f96b098bb8..d2060de9f59 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -808,7 +808,8 @@ var MockServer = Class.extend({
* @param {string[]} kwargs.fields fields that we are aggregating
* @param {Array} kwargs.domain the domain used for the read_group
* @param {boolean} kwargs.lazy still mostly ignored
- * @param {integer} kwargs.limit ignored as well
+ * @param {integer} [kwargs.limit]
+ * @param {integer} [kwargs.offset]
* @returns {Object[]}
*/
_mockReadGroup: function (model, kwargs) {
@@ -982,6 +983,11 @@ var MockServer = Class.extend({
});
}
+ if (kwargs.limit) {
+ var offset = kwargs.offset || 0;
+ result = result.slice(offset, kwargs.limit + offset);
+ }
+
return result;
},
/**
@@ -1164,6 +1170,34 @@ var MockServer = Class.extend({
return true;
},
+ /**
+ * Simulate a 'web_read_group' call to the server.
+ *
+ * Note: some keys in kwargs are still ignored
+ *
+ * @private
+ * @param {string} model a string describing an existing model
+ * @param {Object} kwargs various options supported by read_group
+ * @param {string[]} kwargs.groupby fields that we are grouping
+ * @param {string[]} kwargs.fields fields that we are aggregating
+ * @param {Array} kwargs.domain the domain used for the read_group
+ * @param {boolean} kwargs.lazy still mostly ignored
+ * @param {integer} [kwargs.limit]
+ * @param {integer} [kwargs.offset]
+ * @returns {Object[]}
+ */
+ _mockWebReadGroup: function (model, kwargs) {
+ var allGroups = this._mockReadGroup(model, {
+ domain: kwargs.domain,
+ fields: ['display_name'],
+ groupby: kwargs.groupby,
+ lazy: kwargs.lazy,
+ });
+ return {
+ groups: this._mockReadGroup(model, kwargs),
+ length: allGroups.length,
+ };
+ },
/**
* Simulate a 'write' operation
*
@@ -1243,6 +1277,9 @@ var MockServer = Class.extend({
case 'read_group':
return Promise.resolve(this._mockReadGroup(args.model, args.kwargs));
+ case 'web_read_group':
+ return Promise.resolve(this._mockWebReadGroup(args.model, args.kwargs));
+
case 'read_progress_bar':
return Promise.resolve(this._mockReadProgressBar(args.model, args.kwargs));
diff --git a/addons/web/static/tests/views/abstract_view_tests.js b/addons/web/static/tests/views/abstract_view_tests.js
index ba819379279..b2e0cee0dcd 100644
--- a/addons/web/static/tests/views/abstract_view_tests.js
+++ b/addons/web/static/tests/views/abstract_view_tests.js
@@ -129,8 +129,8 @@ QUnit.module('Views', {
'foo,false,search': '