[IMP] web: sample data in graph view
A previous commit has paved the way for displaying sample data in the reporting views. The present commit brings the required changes to have sample data in graph views. Task ID: 2282196 Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
co-authored by
Aaron Bohy
parent
fd6182443f
commit
ae58021255
@@ -35,7 +35,7 @@ return AbstractModel.extend({
|
||||
* @returns {Object}
|
||||
*/
|
||||
__get: function () {
|
||||
return _.extend({}, this.chart);
|
||||
return Object.assign({ isSample: this.isSampleModel }, this.chart);
|
||||
},
|
||||
/**
|
||||
* Initial loading.
|
||||
@@ -165,7 +165,12 @@ return AbstractModel.extend({
|
||||
this.chart.comparisonFieldIndex = -1;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_isEmpty() {
|
||||
return this.chart.dataPoints.length === 0;
|
||||
},
|
||||
/**
|
||||
* Fetch and process graph data. It is basically a(some) read_group(s)
|
||||
* with correct fields for each domain. We have to do some light processing
|
||||
|
||||
@@ -54,6 +54,7 @@ var MAX_LEGEND_LENGTH = 4 * (Math.max(1, config.device.size_class));
|
||||
|
||||
return AbstractRenderer.extend({
|
||||
className: "o_graph_renderer",
|
||||
sampleDataTargets: ['.o_graph_canvas_container'],
|
||||
/**
|
||||
* @override
|
||||
* @param {Widget} parent
|
||||
@@ -762,9 +763,30 @@ return AbstractRenderer.extend({
|
||||
}
|
||||
var dataPoints = this._filterDataPoints();
|
||||
dataPoints = this._sortDataPoints(dataPoints);
|
||||
if (!dataPoints.length && this.state.mode !== 'pie') {
|
||||
this.$el.append(qweb.render('View.NoContentHelper'));
|
||||
} else if (this.isInDOM) {
|
||||
if (this.isInDOM) {
|
||||
this._renderTitle();
|
||||
|
||||
// detect if some pathologies are still present after the filtering
|
||||
if (this.state.mode === 'pie') {
|
||||
const someNegative = dataPoints.some(dataPt => dataPt.value < 0);
|
||||
const somePositive = dataPoints.some(dataPt => dataPt.value > 0);
|
||||
if (someNegative && somePositive) {
|
||||
const context = {
|
||||
title: _t("Invalid data"),
|
||||
description: [
|
||||
_t("Pie chart cannot mix positive and negative numbers. "),
|
||||
_t("Try to change your domain to only display positive results")
|
||||
].join("")
|
||||
};
|
||||
this._renderNoContentHelper(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.isSample && !this.isEmbedded) {
|
||||
this._renderNoContentHelper();
|
||||
}
|
||||
|
||||
// only render the graph if the widget is already in the DOM (this
|
||||
// happens typically after an update), otherwise, it will be
|
||||
// rendered when the widget will be attached to the DOM (see
|
||||
@@ -785,8 +807,6 @@ return AbstractRenderer.extend({
|
||||
} else if (this.state.mode === 'pie') {
|
||||
this._renderPieChart(dataPoints);
|
||||
}
|
||||
|
||||
this._renderTitle();
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -875,6 +895,25 @@ return AbstractRenderer.extend({
|
||||
options: options,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_renderNoContentHelper: function (context) {
|
||||
let templateName;
|
||||
if (!context && this.noContentHelp) {
|
||||
templateName = "web.ActionHelper";
|
||||
context = { noContentHelp: this.noContentHelp };
|
||||
} else {
|
||||
templateName = "web.NoContentHelper";
|
||||
}
|
||||
const template = document.createElement('template');
|
||||
// FIXME: retrieve owl qweb instance via the env set on Component s.t.
|
||||
// it also works in the tests (importing 'web.env' wouldn't). This won't
|
||||
// be necessary as soon as this rendering will be written in owl.
|
||||
const OwlQweb = owl.Component.env.qweb;
|
||||
template.innerHTML = OwlQweb.renderToString(templateName, context);
|
||||
this.el.append(template.content.firstChild);
|
||||
},
|
||||
/**
|
||||
* create pie chart
|
||||
*
|
||||
@@ -883,38 +922,10 @@ return AbstractRenderer.extend({
|
||||
*/
|
||||
_renderPieChart: function (dataPoints) {
|
||||
var self = this;
|
||||
|
||||
// try to see if some pathologies are still present after the filtering
|
||||
var allNegative = true;
|
||||
var someNegative = false;
|
||||
var allZero = true;
|
||||
dataPoints.forEach(function (datapt) {
|
||||
allNegative = allNegative && (datapt.value < 0);
|
||||
someNegative = someNegative || (datapt.value < 0);
|
||||
allZero = allZero && (datapt.value === 0);
|
||||
});
|
||||
if (someNegative && !allNegative) {
|
||||
this.$el.empty();
|
||||
this.$el.append(qweb.render('View.NoContentHelper', {
|
||||
title: _t("Invalid data"),
|
||||
description: _t("Pie chart cannot mix positive and negative numbers. " +
|
||||
"Try to change your domain to only display positive results"),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (allZero && !this.isEmbedded && this.state.origins.length === 1) {
|
||||
this.$el.empty();
|
||||
this.$el.append(qweb.render('View.NoContentHelper', {
|
||||
title: _t("Invalid data"),
|
||||
description: _t("Pie chart cannot display all zero numbers.. " +
|
||||
"Try to change your domain to display positive results"),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare data
|
||||
var data = {};
|
||||
var colors = [];
|
||||
const allZero = dataPoints.every(dataPt => dataPt.value === 0);
|
||||
if (allZero) {
|
||||
// add fake data to display a pie chart with a grey zone associated
|
||||
// with every origin
|
||||
|
||||
@@ -803,17 +803,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="View.NoContentHelper" class="o_view_nocontent" role="alert">
|
||||
<div t-name="web.ActionHelper" class="o_view_nocontent" owl="1">
|
||||
<div class="o_nocontent_help">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
<t t-if="title" t-esc="title"/>
|
||||
<t t-else="1">No data to display</t>
|
||||
</p>
|
||||
<p>
|
||||
<t t-if="description" t-esc="description"/>
|
||||
<t t-else="1">Try to add some records, or make sure that there is no
|
||||
active filter in the search bar.</t>
|
||||
</p>
|
||||
<t t-raw="noContentHelp"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -338,43 +338,53 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('no content helper (bar chart)', async function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(3);
|
||||
this.data.foo.records = [];
|
||||
|
||||
var graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: '<graph string="Gloups">' +
|
||||
'<field name="product_id"/>' +
|
||||
'</graph>',
|
||||
arch: `
|
||||
<graph string="Gloups">
|
||||
<field name="product_id"/>
|
||||
</graph>`,
|
||||
viewOptions: {
|
||||
action: {
|
||||
help: '<p class="abc">This helper should not be displayed in graph views</p>'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(graph, 'div.o_graph_canvas_container canvas',
|
||||
"should not contain a div with a canvas element");
|
||||
assert.containsOnce(graph, 'div.o_view_nocontent',
|
||||
"should display the no content helper");
|
||||
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas');
|
||||
assert.containsNone(graph, 'div.o_view_nocontent');
|
||||
assert.containsNone(graph, '.abc');
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no content helper (pie chart)', async function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(3);
|
||||
this.data.foo.records = [];
|
||||
|
||||
var graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: '<graph type="pie">' +
|
||||
'<field name="product_id"/>' +
|
||||
'</graph>',
|
||||
arch: `
|
||||
<graph type="pie">
|
||||
<field name="product_id"/>
|
||||
</graph>`,
|
||||
viewOptions: {
|
||||
action: {
|
||||
help: '<p class="abc">This helper should not be displayed in graph views</p>'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone(graph, 'div.o_graph_canvas_container canvas',
|
||||
"should not contain a div with a canvas element");
|
||||
assert.containsOnce(graph, 'div.o_view_nocontent',
|
||||
"should display the no content helper");
|
||||
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas');
|
||||
assert.containsNone(graph, 'div.o_view_nocontent');
|
||||
assert.containsNone(graph, '.abc');
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
@@ -413,27 +423,33 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('no content helper after update', async function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(6);
|
||||
|
||||
var graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: '<graph string="Gloups">' +
|
||||
'<field name="product_id"/>' +
|
||||
'</graph>',
|
||||
arch: `
|
||||
<graph string="Gloups">
|
||||
<field name="product_id"/>
|
||||
</graph>`,
|
||||
viewOptions: {
|
||||
action: {
|
||||
help: '<p class="abc">This helper should not be displayed in graph views</p>'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas',
|
||||
"should contain a div with a canvas element");
|
||||
assert.containsNone(graph, 'div.o_view_nocontent',
|
||||
"should not display the no content helper");
|
||||
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas');
|
||||
assert.containsNone(graph, 'div.o_view_nocontent');
|
||||
assert.containsNone(graph, '.abc');
|
||||
|
||||
await testUtils.graph.reload(graph, {domain: [['product_id', '<', 0]]});
|
||||
|
||||
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas');
|
||||
assert.containsNone(graph, 'div.o_view_nocontent');
|
||||
assert.containsNone(graph, '.abc');
|
||||
|
||||
await testUtils.graph.reload(graph, {domain: [['product_id', '=', 4]]});
|
||||
assert.containsNone(graph, 'div.o_graph_canvas_container canvas',
|
||||
"should not contain a div with a canvas element");
|
||||
assert.containsOnce(graph, 'div.o_view_nocontent',
|
||||
"should display the no content helper");
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
@@ -1259,7 +1275,6 @@ QUnit.module('Views', {
|
||||
assert.checkDatasets(graph, 'data', { data: [4, 3, 1] });
|
||||
|
||||
graph.destroy();
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('graph view sort by measure for grouped data', async function (assert) {
|
||||
@@ -1301,7 +1316,6 @@ QUnit.module('Views', {
|
||||
assert.checkDatasets(graph, 'data', [{ data: [3, 0, 0] }, { data: [1, 3, 1] }]);
|
||||
|
||||
graph.destroy();
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('graph view sort by measure for multiple grouped data', async function (assert) {
|
||||
@@ -1356,6 +1370,75 @@ QUnit.module('Views', {
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('empty graph view with sample data', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: `
|
||||
<graph sample="1">
|
||||
<field name="product_id"/>
|
||||
<field name="date"/>
|
||||
</graph>`,
|
||||
domain: [['id', '<', 0]],
|
||||
viewOptions: {
|
||||
action: {
|
||||
help: '<p class="abc">click to add a foo</p>'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasClass(graph.el, 'o_view_sample_data');
|
||||
assert.containsOnce(graph, '.o_view_nocontent');
|
||||
assert.containsOnce(graph, '.o_graph_canvas_container canvas');
|
||||
assert.hasClass(graph.$('.o_graph_canvas_container'), 'o_sample_data_disabled');
|
||||
|
||||
await graph.reload({ domain: [] });
|
||||
|
||||
assert.doesNotHaveClass(graph.el, 'o_view_sample_data');
|
||||
assert.containsNone(graph, '.o_view_nocontent');
|
||||
assert.containsOnce(graph, '.o_graph_canvas_container canvas');
|
||||
assert.doesNotHaveClass(graph.$('.o_graph_canvas_container'), 'o_sample_data_disabled');
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('non empty graph view with sample data', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: `
|
||||
<graph sample="1">
|
||||
<field name="product_id"/>
|
||||
<field name="date"/>
|
||||
</graph>`,
|
||||
viewOptions: {
|
||||
action: {
|
||||
help: '<p class="abc">click to add a foo</p>'
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
assert.doesNotHaveClass(graph.el, 'o_view_sample_data');
|
||||
assert.containsNone(graph, '.o_view_nocontent');
|
||||
assert.containsOnce(graph, '.o_graph_canvas_container canvas');
|
||||
assert.doesNotHaveClass(graph.$('.o_graph_canvas_container'), 'o_sample_data_disabled');
|
||||
|
||||
await graph.reload({ domain: [['id', '<', 0]] });
|
||||
|
||||
assert.doesNotHaveClass(graph.el, 'o_view_sample_data');
|
||||
assert.containsOnce(graph, '.o_graph_canvas_container canvas');
|
||||
assert.doesNotHaveClass(graph.$('.o_graph_canvas_container'), 'o_sample_data_disabled');
|
||||
assert.containsNone(graph, '.o_view_nocontent');
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('GraphView: comparison mode', {
|
||||
beforeEach: async function () {
|
||||
this.data.foo.records[0].date = '2016-12-15';
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<rng:optional><rng:attribute name="orientation"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="interval"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="disable_linking"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="sample"/></rng:optional>
|
||||
<rng:zeroOrMore>
|
||||
<rng:ref name="field"/>
|
||||
</rng:zeroOrMore>
|
||||
|
||||
Reference in New Issue
Block a user