Skip to content

Commit

Permalink
[IMP] web,board: enable pagination in grouped lists
Browse files Browse the repository at this point in the history
With this rev., when there are a lot of groups in a grouped list
view, groups are displayed under several pages, whereas they were
all displayed in the same page before.

This is especially interesting with the new 'expand' attribute, to
ensure that we don't read records for a large number of groups.

By default, the groups limit is set to 80 (like records), and to 10
is the 'expand' attribute is set to true. This limit can be
overriden with the 'groups_limit' attribute.

Part of task 1915702
  • Loading branch information
aab-odoo committed Apr 2, 2019
1 parent 5895d23 commit 2be28d4
Show file tree
Hide file tree
Showing 19 changed files with 379 additions and 141 deletions.
2 changes: 1 addition & 1 deletion addons/board/static/tests/dashboard_tests.js
Expand Up @@ -841,7 +841,7 @@ QUnit.test("Dashboard should use correct groupby", async function (assert) {
'</board>' +
'</form>',
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');
}
Expand Down
34 changes: 34 additions & 0 deletions addons/web/models/models.py
Expand Up @@ -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):
"""
Expand Down
9 changes: 5 additions & 4 deletions addons/web/static/src/js/core/rpc.js
Expand Up @@ -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) {
Expand All @@ -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 || [];
}
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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 || {};
}
Expand Down
76 changes: 48 additions & 28 deletions addons/web/static/src/js/views/basic/basic_controller.js
Expand Up @@ -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
});
},
/**
Expand Down Expand Up @@ -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
*
Expand All @@ -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.
Expand Down Expand Up @@ -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());
}
},

Expand Down
26 changes: 21 additions & 5 deletions addons/web/static/src/js/views/basic/basic_model.js
Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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];
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions addons/web/static/src/js/views/form/form_controller.js
Expand Up @@ -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.
*
Expand Down
10 changes: 10 additions & 0 deletions addons/web/static/src/js/views/kanban/kanban_controller.js
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions addons/web/static/src/js/views/list/list_controller.js
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion addons/web/static/src/js/views/list/list_view.js
Expand Up @@ -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;
Expand All @@ -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);
},

//--------------------------------------------------------------------------
Expand Down

0 comments on commit 2be28d4

Please sign in to comment.