[IMP] *: limit in search panel

This commit introduces a new attribute 'limit' for search panel fields
that allows to avoid performance issues. That integer attribute (with
default 200) allows to fix a maximal number of values to display for the
fields. When the number of field values to display reaches the limit,
no values will be displayed. Instead, a warning message will be shown
in the corresponding search panel section.
Note it is possible to have no limit using limit="0" on a field.
This commit reintroduces in a better way the principle brought by the
fix 8d57153b34c04952a85f6642951cb2697016da84.

Task ID: 2154749
This commit is contained in:
Mathieu Duckerts-Antoine
2020-06-12 13:34:04 +00:00
committed by Julien Mougenot
parent e5585c078e
commit 3306a0f414
11 changed files with 381 additions and 116 deletions
@@ -119,7 +119,7 @@
<filter string="Account Type" name="accounttype" domain="" context="{'group_by':'user_type_id'}"/>
</group>
<searchpanel class="account_root">
<field name="root_id" icon="fa-filter"/>
<field name="root_id" icon="fa-filter" limit="0"/>
</searchpanel>
</search>
</field>
+1 -1
View File
@@ -368,7 +368,7 @@
<field name="arch" type="xml">
<xpath expr="//group" position="after">
<searchpanel class="account_root">
<field name="account_root_id" icon="fa-filter"/>
<field name="account_root_id" icon="fa-filter" limit="0"/>
</searchpanel>
</xpath>
</field>
+58 -25
View File
@@ -12,6 +12,7 @@ from odoo.tools.misc import get_lang
from odoo.exceptions import UserError
from collections import defaultdict
SEARCH_PANEL_ERROR_MESSAGE = "Too many items to display."
class lazymapping(defaultdict):
def __missing__(self, key):
@@ -223,13 +224,14 @@ class Base(models.AbstractModel):
@api.model
def _search_panel_domain_image(self, field_name, domain, set_count=False):
def _search_panel_domain_image(self, field_name, domain, set_count=False, limit=False):
"""
Return the values in the image of the provided domain by field_name.
:param domain: domain whose image is returned
:param field_name: the name of a field (type many2one or selection)
:param set_count: whether to set the key '__count' in image values. Default is False.
:param limit: integer, maximal number of values to fetch. Default is False.
:return: a dict of the form
{
id: { 'id': id, 'display_name': display_name, ('__count': c,) },
@@ -253,7 +255,8 @@ class Base(models.AbstractModel):
domain,
[(field_name, '!=', False)],
])
groups = self.read_group(domain, [field_name], [field_name])
groups = self.read_group(domain, [field_name], [field_name], limit=limit)
domain_image = {}
for group in groups:
id, display_name = group_id_name(group[field_name])
@@ -312,7 +315,7 @@ class Base(models.AbstractModel):
{ 'id': id, parent_name: False or (id, display_name),... }
:param parent_name, string, indicates which key determines the parent
:param ids: list of record ids
:return: the sublist (as a filter object) of records with the above properties
:return: the sublist of records with the above properties
}
"""
def get_parent_id(record):
@@ -342,7 +345,7 @@ class Base(models.AbstractModel):
records_to_keep[id] = chain_is_fully_included
# we keep initial order
return filter(lambda rec: records_to_keep.get(rec['id']), records)
return [rec for rec in records if records_to_keep.get(rec['id'])]
@api.model
@@ -412,7 +415,9 @@ class Base(models.AbstractModel):
(if possible). If set to true and _parent_name is set on the
comodel field, the information necessary for the hierarchization will
be returned. Default is True.
:param limit: integer, maximal number of values to fetch. Default is None.
:param search_domain: base domain of search. Default is [].
with parents if hierarchize is set)
:return: {
'parent_field': parent field on the comodel of field, or False
'values': array of dictionaries containing some info on the records
@@ -420,6 +425,7 @@ class Base(models.AbstractModel):
The display name, the __count (how many records with that value)
and possibly parent_field are fetched.
}
or an object with an error message when limit is defined and is reached.
"""
field = self._fields[field_name]
supported_types = ['many2one', 'selection']
@@ -439,12 +445,6 @@ class Base(models.AbstractModel):
'values': self._search_panel_selection_range(field_name, model_domain=model_domain,**kwargs),
}
enable_counters = kwargs.get('enable_counters')
expand = kwargs.get('expand')
domain_image = {}
if enable_counters or not expand:
domain_image = self._search_panel_domain_image(field_name, model_domain, enable_counters)
Comodel = self.env[field.comodel_name].with_context(hierarchical_naming=False)
field_names = ['display_name']
hierarchize = kwargs.get('hierarchize', True)
@@ -460,10 +460,21 @@ class Base(models.AbstractModel):
hierarchize = False
comodel_domain = kwargs.get('comodel_domain', [])
enable_counters = kwargs.get('enable_counters')
expand = kwargs.get('expand')
limit = kwargs.get('limit')
domain_image = {}
if enable_counters or not expand:
domain_image = self._search_panel_domain_image(field_name, model_domain, enable_counters,
limit if not (expand or hierarchize or comodel_domain) else False
)
if not (expand or hierarchize or comodel_domain):
values = list(domain_image.values())
if limit and len(values) == limit:
return {'error_msg': _(SEARCH_PANEL_ERROR_MESSAGE)}
return {
'parent_field': parent_name,
'values': list(domain_image.values()),
'values': values,
}
if not expand:
@@ -473,12 +484,15 @@ class Base(models.AbstractModel):
else:
condition = [('id', 'in', image_element_ids)]
comodel_domain = AND([comodel_domain, condition])
comodel_records = Comodel.search_read(comodel_domain, field_names)
comodel_records = Comodel.search_read(comodel_domain, field_names, limit=limit)
if hierarchize:
ids = [rec['id'] for rec in comodel_records] if expand else image_element_ids
comodel_records = self._search_panel_sanitized_parent_hierarchy(comodel_records, parent_name, ids)
if limit and len(comodel_records) == limit:
return {'error_msg': _(SEARCH_PANEL_ERROR_MESSAGE)}
field_range = {}
for record in comodel_records:
record_id = record['id']
@@ -522,13 +536,17 @@ class Base(models.AbstractModel):
for the group_by (if any). Those domains are
used to fech accurate counters for values in each group.
Default is [] (many2one case) or None.
:param limit: integer, maximal number of values to fetch. Default is None.
:param search_domain: base domain of search. Default is [].
:return: a list of possible values, each being a dict with keys
'id' (value),
'name' (value label),
'__count' (how many records with that value),
'group_id' (value of group), set if a group_by has been provided,
'group_name' (label of group), set if a group_by has been provided
:return: {
'values': a list of possible values, each being a dict with keys
'id' (value),
'name' (value label),
'__count' (how many records with that value),
'group_id' (value of group), set if a group_by has been provided,
'group_name' (label of group), set if a group_by has been provided
}
or an object with an error message when limit is defined and reached.
"""
field = self._fields[field_name]
supported_types = ['many2one', 'many2many', 'selection']
@@ -544,11 +562,14 @@ class Base(models.AbstractModel):
])
if field.type == 'selection':
return self._search_panel_selection_range(field_name, model_domain=model_domain, **kwargs)
return {
'values': self._search_panel_selection_range(field_name, model_domain=model_domain, **kwargs)
}
Comodel = self.env.get(field.comodel_name).with_context(hierarchical_naming=False)
field_names = ['display_name']
group_by = kwargs.get('group_by')
limit = kwargs.get('limit')
if group_by:
group_by_field = Comodel._fields[group_by]
@@ -575,7 +596,9 @@ class Base(models.AbstractModel):
expand = kwargs.get('expand')
if field.type == 'many2many':
comodel_records = Comodel.search_read(comodel_domain, field_names)
comodel_records = Comodel.search_read(comodel_domain, field_names, limit=limit)
if expand and limit and len(comodel_records) == limit:
return {'error_msg': _(SEARCH_PANEL_ERROR_MESSAGE)}
group_domain = kwargs.get('group_domain')
field_range = []
@@ -608,7 +631,10 @@ class Base(models.AbstractModel):
values['__count'] = count
field_range.append(values)
return field_range
if not expand and limit and len(field_range) == limit:
return {'error_msg': _(SEARCH_PANEL_ERROR_MESSAGE)}
return { 'values': field_range, }
if field.type == 'many2one':
domain_image = {}
@@ -617,10 +643,15 @@ class Base(models.AbstractModel):
model_domain,
kwargs.get('group_domain', []),
])
domain_image = self._search_panel_domain_image(field_name, model_domain, enable_counters)
domain_image = self._search_panel_domain_image(field_name, model_domain, enable_counters,
limit if not (expand or group_by or comodel_domain) else False
)
if not (expand or group_by or comodel_domain):
return list(domain_image.values())
values = list(domain_image.values())
if limit and len(values) == limit:
return {'error_msg': _(SEARCH_PANEL_ERROR_MESSAGE)}
return {'values': values, }
if not expand:
image_element_ids = list(domain_image.keys())
@@ -628,7 +659,9 @@ class Base(models.AbstractModel):
comodel_domain,
[('id', 'in', image_element_ids)],
])
comodel_records = Comodel.search_read(comodel_domain, field_names)
comodel_records = Comodel.search_read(comodel_domain, field_names, limit=limit)
if limit and len(comodel_records) == limit:
return {'error_msg': _(SEARCH_PANEL_ERROR_MESSAGE)}
field_range = []
for record in comodel_records:
@@ -649,7 +682,7 @@ class Base(models.AbstractModel):
field_range.append(values)
return field_range
return { 'values': field_range, }
class ResCompany(models.Model):
+45 -28
View File
@@ -21,6 +21,8 @@ const qweb = core.qweb;
const defaultViewTypes = ['kanban', 'tree'];
let nextSectionId = 1;
const SEARCH_PANEL_DEFAULT_LIMIT = 200;
/**
* Given a <searchpanel> arch node, iterate over its children to generate the
* description of each section (being either a category or a filter).
@@ -31,31 +33,34 @@ let nextSectionId = 1;
*/
function _processSearchPanelNode(node, fields) {
const sections = {};
node.children.forEach((childNode, index) => {
if (childNode.tag !== 'field' || childNode.attrs.invisible === "1") {
node.children.forEach(({ attrs, tag }, index) => {
if (tag !== 'field' || attrs.invisible === "1") {
return;
}
const fieldName = childNode.attrs.name;
const type = childNode.attrs.select === 'multi' ? 'filter' : 'category';
const fieldName = attrs.name;
const type = attrs.select === 'multi' ? 'filter' : 'category';
const sectionId = `section_${nextSectionId++}`;
const section = {
color: childNode.attrs.color,
description: childNode.attrs.string || fields[fieldName].string,
enableCounters: !!pyUtils.py_eval(childNode.attrs.enable_counters || '0'),
expand: !!pyUtils.py_eval(childNode.attrs.expand || '0'),
color: attrs.color,
description: attrs.string || fields[fieldName].string,
enableCounters: !!pyUtils.py_eval(attrs.enable_counters || '0'),
expand: !!pyUtils.py_eval(attrs.expand || '0'),
fieldName,
icon: childNode.attrs.icon,
icon: attrs.icon,
id: sectionId,
index,
limit: attrs.limit ?
pyUtils.py_eval(attrs.limit) :
SEARCH_PANEL_DEFAULT_LIMIT,
type,
};
if (section.type === 'category') {
section.icon = section.icon || 'fa-folder';
section.hierarchize = !!pyUtils.py_eval(childNode.attrs.hierarchize || '1');
section.hierarchize = !!pyUtils.py_eval(attrs.hierarchize || '1');
} else if (section.type === 'filter') {
section.domain = childNode.attrs.domain || '[]';
section.groupBy = childNode.attrs.groupby;
section.domain = attrs.domain || '[]';
section.groupBy = attrs.groupby;
section.icon = section.icon || 'fa-filter';
}
sections[sectionId] = section;
@@ -267,11 +272,18 @@ const SearchPanel = Widget.extend({
/**
* @private
* @param {string} categoryId
* @param {Object[]} values
* @param {Object} result
*/
_createCategoryTree(categoryId, values) {
_createCategoryTree(categoryId, result) {
const category = this.categories[categoryId];
const parentField = category.parentField;
let { error_msg, parent_field: parentField, values } = result;
if (error_msg) {
category.errorMsg = error_msg;
values = [];
}
if (category.hierarchize) {
category.parentField = parentField;
}
const unfoldedIds = Object.values(category.values || {})
.filter(c => c.folded === false)
@@ -315,10 +327,15 @@ const SearchPanel = Widget.extend({
/**
* @private
* @param {string} filterId
* @param {Object[]} values
* @param {Object} result
*/
_createFilterTree(filterId, values) {
_createFilterTree(filterId, result) {
const filter = this.filters[filterId];
let { error_msg, values } = result;
if (error_msg) {
filter.errorMsg = error_msg;
values = [];
}
// restore checked property
values.forEach(value => {
@@ -379,8 +396,7 @@ const SearchPanel = Widget.extend({
_fetchCategories(force) {
const proms = [];
for (const category of Object.values(this.categories)) {
const { enableCounters, expand, hierarchize } = category;
const field = this.fields[category.fieldName];
const { enableCounters, expand, hierarchize, limit } = category;
if (force || enableCounters || !expand) {
const prom = this._rpc({
method: 'search_panel_select_range',
@@ -391,14 +407,12 @@ const SearchPanel = Widget.extend({
enable_counters: enableCounters,
expand,
hierarchize,
limit,
search_domain: this.searchDomain,
},
}).then(({ parent_field, values }) => {
if (field.type === 'many2one' && hierarchize) {
category.parentField = parent_field;
}
this._createCategoryTree(category.id, values);
});
}).then(
(result) => this._createCategoryTree(category.id, result)
);
proms.push(prom);
}
}
@@ -419,7 +433,7 @@ const SearchPanel = Widget.extend({
const categoryDomain = this._getCategoryDomain();
const proms = [];
for (const filter of Object.values(this.filters)) {
const { enableCounters, expand, groupBy } = filter;
const { enableCounters, expand, groupBy, limit } = filter;
const prom = this._rpc({
method: 'search_panel_select_multi_range',
model: this.model,
@@ -432,9 +446,12 @@ const SearchPanel = Widget.extend({
expand,
group_by: groupBy || false,
group_domain: this._getGroupDomain(filter),
limit,
search_domain: [...this.searchDomain, ...this.viewDomain],
},
}).then(values => { this._createFilterTree(filter.id, values); });
}).then(
(result) => this._createFilterTree(filter.id, result)
);
proms.push(prom);
}
return Promise.all(proms);
@@ -646,7 +663,7 @@ const SearchPanel = Widget.extend({
const sections = categories.concat(filters).sort((s1, s2) => s1.index - s2.index);
sections.forEach(section => {
if (Object.keys(section.values).length) {
if (Object.keys(section.values).length || section.errorMsg) {
if (section.type === 'category') {
this.$el.append(this._renderCategory(section));
} else {
+8 -2
View File
@@ -222,7 +222,10 @@
<t t-call="SearchPanel.SectionHeader">
<t t-set="section" t-value="category"/>
</t>
<ul class="list-group d-block o_search_panel_field" t-att-data-field-name="category.fieldName">
<div t-if="category.errorMsg" class="alert alert-warning">
<span><t t-esc="category.errorMsg"/></span>
</div>
<ul t-else="" class="list-group d-block o_search_panel_field" t-att-data-field-name="category.fieldName">
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-category-id="category.id">
<header t-att-class="'list-group-item-action' + (!category.activeValueId ? ' active' : '')">
<label class="o_search_panel_label mb0">
@@ -267,7 +270,10 @@
<t t-call="SearchPanel.SectionHeader">
<t t-set="section" t-value="filter"/>
</t>
<ul class="list-group d-block o_search_panel_field" t-att-data-field-name="filter.fieldName">
<div t-if="filter.errorMsg" class="alert alert-warning">
<span><t t-esc="filter.errorMsg"/></span>
</div>
<ul t-else="" class="list-group d-block o_search_panel_field" t-att-data-field-name="filter.fieldName">
<t t-if="filter.groups" t-call="SearchPanel.FilterGroups">
<t t-set="groups" t-value="filter.groups"/>
</t>
+66 -22
View File
@@ -539,15 +539,16 @@ var MockServer = Class.extend({
return modelFields;
},
/**
* Simulate a call to the server '_search_panel_domain_image' method.
* Simulates a call to the server '_search_panel_domain_image' method.
*
* @private
* @param {string} model
* @param {Array[]} domain
* @param {string} fieldName
* @param {boolean} setCount
* @returns {Map}
*/
_mockSearchPanelDomainImage: function (model, fieldName, domain, setCount=false) {
_mockSearchPanelDomainImage: function (model, fieldName, domain, setCount=false, limit=false) {
const field = this.data[model].fields[fieldName];
let groupIdName;
if (field.type === 'many2one') {
@@ -569,6 +570,7 @@ var MockServer = Class.extend({
domain,
fields: [fieldName],
groupby: [fieldName],
limit,
});
const domainImage = new Map();
for (const group of groups) {
@@ -582,8 +584,9 @@ var MockServer = Class.extend({
return domainImage;
},
/**
* Simulate a call to the server '_search_panel_global_counters' method.
* Simulates a call to the server '_search_panel_global_counters' method.
*
* @private
* @param {Map} valuesRange
* @param {(string|boolean)} parentName 'parent_id' or false
*/
@@ -602,8 +605,9 @@ var MockServer = Class.extend({
}
},
/**
* Simulate a call to the server '_search_panel_sanitized_parent_hierarchy' method.
* Simulates a call to the server '_search_panel_sanitized_parent_hierarchy' method.
*
* @private
* @param {Object[]} records
* @param {(string|boolean)} parentName 'parent_id' or false
* @param {number[]} ids
@@ -641,8 +645,9 @@ var MockServer = Class.extend({
return records.filter(rec => recordsToKeep[rec.id]);
},
/**
* Simulate a call to the server 'search_panel_selection_range' method.
* Simulates a call to the server 'search_panel_selection_range' method.
*
* @private
* @param {string} model
* @param {string} fieldName
* @param {Object} kwargs
@@ -677,8 +682,9 @@ var MockServer = Class.extend({
return selectionRange;
},
/**
* Simulate a call to the server 'search_panel_select_range' method.
* Simulates a call to the server 'search_panel_select_range' method.
*
* @private
* @param {string} model
* @param {string[]} args
* @param {string} args[fieldName]
@@ -688,6 +694,7 @@ var MockServer = Class.extend({
* @param {Array[]} [kwargs.comodel_domain] domain of field values (if relational)
* (this parameter is used in _search_panel_range)
* @param {boolean} [kwargs.enable_counters] whether to count records by value
* @param {integer} [kwargs.limit] maximal number of values to fetch
* @param {Array[]} [kwargs.search_domain] base domain of search (this parameter
* is used in _search_panel_range)
* @returns {Object}
@@ -710,12 +717,7 @@ var MockServer = Class.extend({
values: this._mockSearchPanelSelectionRange(model, fieldName, kwargs),
};
}
const enableCounters = kwargs.enable_counters;
const expand = kwargs.expand;
let domainImage = new Map();
if (enableCounters || !expand) {
domainImage = this._mockSearchPanelDomainImage(model, fieldName, modelDomain, enableCounters);
}
const fieldNames = ['display_name'];
let hierarchize = 'hierarchize' in kwargs ? kwargs.hierarchize : true;
let getParentId;
@@ -728,7 +730,20 @@ var MockServer = Class.extend({
hierarchize = false;
}
let comodelDomain = kwargs.comodel_domain || [];
const enableCounters = kwargs.enable_counters;
const expand = kwargs.expand;
const limit = kwargs.limit;
let domainImage = new Map();
if (enableCounters || !expand) {
domainImage = this._mockSearchPanelDomainImage(
model, fieldName, modelDomain, enableCounters,
!expand && !hierarchize && !comodelDomain.length ? limit : false
);
}
if (!expand && !hierarchize && !comodelDomain.length) {
if (limit && domainImage.size === limit) {
return { error_msg: "Too many items to display." };
}
return {
parent_field: parentName,
values: [...domainImage.values()],
@@ -759,11 +774,16 @@ var MockServer = Class.extend({
condition,
]);
}
let comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], {});
let comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit });
if (hierarchize) {
const ids = expand ? comodelRecords.map(rec => rec.id) : imageElementIds;
comodelRecords = this._mockSearchPanelSanitizedParentHierarchy(comodelRecords, parentName, ids);
}
if (limit && comodelRecords.length === limit) {
return { error_msg: "Too many items to display." };
}
// A map is used to keep the initial order.
const fieldRange = new Map();
for (const record of comodelRecords) {
@@ -790,8 +810,9 @@ var MockServer = Class.extend({
};
},
/**
* Simulate a call to the server 'search_panel_select_multi_range' method.
* Simulates a call to the server 'search_panel_select_multi_range' method.
*
* @private
* @param {string} model
* @param {string[]} args
* @param {string} args[fieldName]
@@ -806,8 +827,9 @@ var MockServer = Class.extend({
* @param {Array[]} [kwargs.group_domain] dict, one domain for each activated
* group for the group_by (if any). Those domains are used to fech accurate
* counters for values in each group
* @param {integer} [kwargs.limit] maximal number of values to fetch
* @param {Array[]} [kwargs.search_domain] base domain of search
* @returns {Object[]}
* @returns {Object}
*/
_mockSearchPanelSelectMultiRange: function (model, [fieldName], kwargs) {
const field = this.data[model].fields[fieldName];
@@ -823,7 +845,9 @@ var MockServer = Class.extend({
]);
if (field.type === 'selection') {
kwargs.model_domain = modelDomain;
return this._mockSearchPanelSelectionRange(model, fieldName, kwargs);
return {
values: this._mockSearchPanelSelectionRange(model, fieldName, kwargs),
};
}
const fieldNames = ['display_name'];
const groupBy = kwargs.group_by;
@@ -844,8 +868,13 @@ var MockServer = Class.extend({
let comodelDomain = kwargs.comodel_domain || [];
const enableCounters = kwargs.enable_counters;
const expand = kwargs.expand;
const limit = kwargs.limit;
if (field.type === 'many2many') {
const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], {});
const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit });
if (expand && limit && comodelRecords.length === limit) {
return { error_msg: "Too many items to display." };
}
const groupDomain = kwargs.group_domain;
const fieldRange = [];
for (const record of comodelRecords) {
@@ -881,7 +910,12 @@ var MockServer = Class.extend({
fieldRange.push(values);
}
}
return fieldRange;
if (!expand && limit && fieldRange.length === limit) {
return { error_msg: "Too many items to display." };
}
return { values: fieldRange };
}
if (field.type === 'many2one') {
@@ -891,10 +925,16 @@ var MockServer = Class.extend({
...modelDomain,
...(kwargs.group_domain || []),
]);
domainImage = this._mockSearchPanelDomainImage(model, fieldName, modelDomain, enableCounters);
domainImage = this._mockSearchPanelDomainImage(
model, fieldName, modelDomain, enableCounters,
!expand && !groupBy && !comodelDomain.length ? limit : false,
);
}
if (!expand && !groupBy && !comodelDomain.length) {
return [...domainImage.values()];
if (limit && domainImage.size === limit) {
return { error_msg: "Too many items to display." };
}
return { values: [...domainImage.values()] };
}
if (!expand) {
const imageElementIds = [...domainImage.keys()].map(Number);
@@ -903,7 +943,11 @@ var MockServer = Class.extend({
['id', 'in', imageElementIds],
]);
}
const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], {});
const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit });
if (limit && comodelRecords.length === limit) {
return { error_msg: "Too many items to display." };
}
const fieldRange = [];
for (const record of comodelRecords) {
const values= {
@@ -920,7 +964,7 @@ var MockServer = Class.extend({
}
fieldRange.push(values);
}
return fieldRange;
return { values: fieldRange };
}
},
/**
@@ -1948,6 +1948,7 @@ QUnit.module('Views', {
comodel_domain: [['parent_id', '=', false]],
group_domain: [],
enable_counters: true,
limit: 200,
});
}
return result;
@@ -3834,5 +3835,81 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test("reached limit for a category", async function (assert) {
assert.expect(6);
const kanban = await createView({
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
</div>
</t>
</templates>
</kanban>`,
archs: {
'partner,false,search': `
<search>
<searchpanel>
<field name="company_id" limit="2"/>
</searchpanel>
</search>`,
},
data: this.data,
model: 'partner',
services: this.services,
View: KanbanView,
});
assert.containsOnce(kanban, '.o_search_panel_section');
assert.containsOnce(kanban, '.o_search_panel_section_header');
assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, " COMPANY");
assert.containsOnce(kanban, 'section div.alert.alert-warning');
assert.strictEqual(kanban.el.querySelector('section div.alert.alert-warning').innerText, "Too many items to display.");
assert.containsNone(kanban, '.o_search_panel_category_value');
kanban.destroy();
});
QUnit.test("reached limit for a filter", async function (assert) {
assert.expect(6);
const kanban = await createView({
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo"/>
</div>
</t>
</templates>
</kanban>`,
archs: {
'partner,false,search': `
<search>
<searchpanel>
<field name="company_id" select="multi" limit="2"/>
</searchpanel>
</search>`,
},
data: this.data,
model: 'partner',
services: this.services,
View: KanbanView,
});
assert.containsOnce(kanban, '.o_search_panel_section');
assert.containsOnce(kanban, '.o_search_panel_section_header');
assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, " COMPANY");
assert.containsOnce(kanban, 'section div.alert.alert-warning');
assert.strictEqual(kanban.el.querySelector('section div.alert.alert-warning').innerText, "Too many items to display.");
assert.containsNone(kanban, '.o_search_panel_filter_value');
kanban.destroy();
});
});
});
+5
View File
@@ -2005,6 +2005,11 @@ Possible children elements of the search view are:
* ``expand``: default is false. If set to false categories or filters with 0 records will be hidden.
* ``limit``: default is 200. Integer determining the maximal number of values to fetch for the field.
If the limit is reached, no values will be displayed in the search panel and an error message will
appear instead because we consider that is useless / bad performance-wise. All values will be
fetched if set to 0.
Additional optional attributes are available according to the chosen case:
For the ``one`` case:
+1
View File
@@ -237,6 +237,7 @@
<rng:optional><rng:attribute name="color"/></rng:optional>
<rng:optional><rng:attribute name="groupby"/></rng:optional>
<rng:optional><rng:attribute name="enable_counters"/></rng:optional>
<rng:optional><rng:attribute name="limit"/></rng:optional>
<rng:optional><rng:attribute name="operator"/></rng:optional>
<rng:optional><rng:attribute name="colspan"/></rng:optional>
<rng:optional><rng:attribute name="nolabel"/></rng:optional>
@@ -2,6 +2,9 @@
import odoo.tests
import json
SEARCH_PANEL_ERROR = {'error_msg': "Too many items to display.", }
@odoo.tests.tagged('post_install', '-at_install')
class TestSelectRangeMulti(odoo.tests.TransactionCase):
@@ -16,7 +19,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
def test_many2one_empty(self):
result = self.SourceModel.search_panel_select_multi_range('tag_id')
self.assertEqual(
result,
result['values'],
[],
)
@@ -55,7 +58,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='folder_id'
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -74,7 +77,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='status'
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': 'cool', 'group_name': 'Cool', },
@@ -93,7 +96,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='color'
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': 'Red', 'group_name': 'Red', },
@@ -111,7 +114,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id, },
{'__count': 1, 'display_name': 'Tag 2', 'id': t2_id, },
@@ -127,7 +130,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', 'in', [r1_id, r2_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id, },
{'__count': 0, 'display_name': 'Tag 2', 'id': t2_id, },
@@ -135,6 +138,15 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
],
)
# counters, expand, no group_by, and limit
result = self.SourceModel.search_panel_select_multi_range(
'tag_id',
enable_counters=True,
expand=True,
limit=2,
)
self.assertEqual(result, SEARCH_PANEL_ERROR, )
# no counters, expand, and group_by (many2one case)
result = self.SourceModel.search_panel_select_multi_range(
'tag_id',
@@ -142,7 +154,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='folder_id',
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -159,7 +171,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id, },
{'display_name': 'Tag 2', 'id': t2_id, },
@@ -174,7 +186,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', 'in', [r1_id, r2_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id, },
{'display_name': 'Tag 2', 'id': t2_id, },
@@ -189,7 +201,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='folder_id',
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -205,19 +217,27 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', 'in', [r1_id, r2_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id, },
]
)
# counters, no expand, no group_by, and limit
result = self.SourceModel.search_panel_select_multi_range(
'tag_id',
enable_counters=True,
limit=2,
)
self.assertEqual(result, SEARCH_PANEL_ERROR, )
# no counters, no expand, group_by (many2one case), and search domain
result = self.SourceModel.search_panel_select_multi_range(
'tag_id',
group_by='folder_id',
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -232,7 +252,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', 'in', [r1_id, r2_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id, },
]
@@ -244,7 +264,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
comodel_domain=[['id', 'in', [t2_id, t3_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 2', 'id': t2_id, },
]
@@ -255,7 +275,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
def test_many2many_empty(self):
result = self.SourceModel.search_panel_select_multi_range('tag_ids')
self.assertEqual(
result,
result['values'],
[],
)
@@ -294,7 +314,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='folder_id'
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -313,7 +333,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='status'
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': 'cool', 'group_name': 'Cool', },
@@ -332,7 +352,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='color'
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': 'Red', 'group_name': 'Red', },
@@ -356,7 +376,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
}
)
self.assertEqual(
result,
result['values'],
[
{'__count': 1, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -379,7 +399,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
}
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': 'Red', 'group_name': 'Red', },
@@ -397,7 +417,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id, },
{'__count': 2, 'display_name': 'Tag 2', 'id': t2_id, },
@@ -413,7 +433,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', 'in', [r1_id, r2_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'Tag 1', 'id': t1_id, },
{'__count': 1, 'display_name': 'Tag 2', 'id': t2_id, },
@@ -428,7 +448,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='folder_id',
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -445,7 +465,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id, },
{'display_name': 'Tag 2', 'id': t2_id, },
@@ -460,7 +480,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', 'in', [r1_id, r2_id]]],
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id, },
{'display_name': 'Tag 2', 'id': t2_id, },
@@ -476,7 +496,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', '=', r2_id]],
)
self.assertEqual(
result,
result['values'],
[
{'__count': 1, 'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -490,7 +510,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', '=', r2_id]],
)
self.assertEqual(
result,
result['values'],
[
{'__count': 1, 'display_name': 'Tag 1', 'id': t1_id, },
]
@@ -503,7 +523,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', '=', r2_id]],
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id,
'group_id': f2_id, 'group_name': 'Folder 2', },
@@ -516,12 +536,20 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', '=', r2_id]],
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'Tag 1', 'id': t1_id, },
]
)
# no counters, no expand, no group_by, and search_domain
result = self.SourceModel.search_panel_select_multi_range(
'tag_ids',
limit=2,
)
self.assertEqual(result, SEARCH_PANEL_ERROR, )
# Selection case
def test_selection_empty(self):
@@ -530,7 +558,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', },
{'display_name': 'B', 'id': 'b', },
@@ -553,7 +581,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
group_by='not_possible_to_group', # no impact expected
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', '__count': 2, },
{'display_name': 'B', 'id': 'b', '__count': 0, },
@@ -567,7 +595,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', '__count': 2, },
{'display_name': 'B', 'id': 'b', '__count': 0, },
@@ -582,7 +610,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', '=', r1_id]], # impact expected
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', '__count': 1, },
{'display_name': 'B', 'id': 'b', '__count': 0, },
@@ -595,7 +623,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
expand=True,
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', },
{'display_name': 'B', 'id': 'b', },
@@ -609,7 +637,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
search_domain=[['id', '=', r1_id]], # no impact expected
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', },
{'display_name': 'B', 'id': 'b', },
@@ -622,7 +650,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
enable_counters=True,
)
self.assertEqual(
result,
result['values'],
[
{'__count': 2, 'display_name': 'A', 'id': 'a', },
]
@@ -633,7 +661,7 @@ class TestSelectRangeMulti(odoo.tests.TransactionCase):
'state',
)
self.assertEqual(
result,
result['values'],
[
{'display_name': 'A', 'id': 'a', },
]
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import odoo.tests
SEARCH_PANEL_ERROR = {'error_msg': "Too many items to display.", }
@odoo.tests.tagged('post_install', '-at_install')
class TestSelectRange(odoo.tests.TransactionCase):
@@ -88,6 +90,39 @@ class TestSelectRange(odoo.tests.TransactionCase):
]
)
# counters, expand, hierarchization, and reached limit
result = self.SourceModel.search_panel_select_range(
'folder_id',
enable_counters=True,
expand=True,
limit=2,
)
self.assertEqual(result, SEARCH_PANEL_ERROR, )
# counters, expand, hierarchization, and unreached limit
result = self.SourceModel.search_panel_select_range(
'folder_id',
enable_counters=True,
expand=True,
limit=200,
)
self.assertEqual(
result,
{
'parent_field': 'parent_name_id',
'values': [
{'__count': 2, 'display_name': 'Folder 1',
'id': f1_id, 'parent_name_id': False, },
{'__count': 1, 'display_name': 'Folder 2',
'id': f2_id, 'parent_name_id': False, },
{'__count': 1, 'display_name': 'Folder 3',
'id': f3_id, 'parent_name_id': f1_id, },
{'__count': 1, 'display_name': 'Folder 4',
'id': f4_id, 'parent_name_id': f2_id, },
],
}
)
# counters, expand, and no hierarchization
result = self.SourceModel.search_panel_select_range(
'folder_id',
@@ -210,6 +245,15 @@ class TestSelectRange(odoo.tests.TransactionCase):
False
)
# counters, no expand, no hierarchization, and limit
result = self.SourceModel.search_panel_select_range(
'folder_id',
enable_counters=True,
hierarchize=False,
limit=2,
)
self.assertEqual(result, SEARCH_PANEL_ERROR, )
# no counters, no expand, and hierarchization
result = self.SourceModel.search_panel_select_range(
'folder_id',
@@ -233,6 +277,16 @@ class TestSelectRange(odoo.tests.TransactionCase):
'parent_name_id'
)
# no counters, no expand, and hierarchization
result = self.SourceModel.search_panel_select_range(
'folder_id',
search_domain=[(0, '=', 1)],
)
self.assertEqual(
result,
{'parent_field': 'parent_name_id', 'values': [], } # should not be a SEARCH_PANEL_ERROR
)
# no counters, no expand, and no hierarchization
result = self.SourceModel.search_panel_select_range(
'folder_id',