dataviz: allow control of total display in tables (#85654)
gitea/combo/pipeline/head This commit looks good
Details
gitea/combo/pipeline/head This commit looks good
Details
This commit is contained in:
parent
5e8b58c6ca
commit
6be1d6c5fc
|
@ -186,6 +186,7 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
|
|||
'time_range_start_template',
|
||||
'time_range_end_template',
|
||||
'chart_type',
|
||||
'display_total',
|
||||
'height',
|
||||
'sort_order',
|
||||
'hide_null_values',
|
||||
|
@ -217,6 +218,7 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
|
|||
'time_range_end',
|
||||
'time_range_start_template',
|
||||
'time_range_end_template',
|
||||
'display_total',
|
||||
):
|
||||
del self.fields[field]
|
||||
else:
|
||||
|
@ -228,6 +230,9 @@ class ChartNgForm(ChartFiltersMixin, forms.ModelForm):
|
|||
del self.fields['time_range_start_template']
|
||||
del self.fields['time_range_end_template']
|
||||
|
||||
if not self.instance.is_table_chart() or self.instance.statistic.data_type:
|
||||
del self.fields['display_total']
|
||||
|
||||
def add_filter_fields(self):
|
||||
new_fields = OrderedDict()
|
||||
for field_name, field in self.fields.items():
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.2.18 on 2024-02-14 11:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dataviz', '0028_increase_extra_css_class'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='chartngcell',
|
||||
name='display_total',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('none', 'None'),
|
||||
('line-and-column', 'Total line and total column'),
|
||||
('line', 'Total line'),
|
||||
('column', 'Total column'),
|
||||
],
|
||||
default='line-and-column',
|
||||
max_length=20,
|
||||
verbose_name='Display of total',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -246,6 +246,17 @@ class ChartNgCell(CellBase):
|
|||
('table-inverted', _('Table (inverted)')),
|
||||
),
|
||||
)
|
||||
display_total = models.CharField(
|
||||
_('Display of total'),
|
||||
max_length=20,
|
||||
default='line-and-column',
|
||||
choices=(
|
||||
('none', _('None')),
|
||||
('line-and-column', _('Total line and total column')),
|
||||
('line', _('Total line')),
|
||||
('column', _('Total column')),
|
||||
),
|
||||
)
|
||||
|
||||
height = models.CharField(
|
||||
_('Height'),
|
||||
|
@ -381,6 +392,8 @@ class ChartNgCell(CellBase):
|
|||
|
||||
if chart.axis_count == 1:
|
||||
data = self.process_one_dimensional_data(chart, data)
|
||||
if getattr(chart, 'compute_sum', True) and self.is_table_chart():
|
||||
data = self.add_total_to_line_table(chart, data)
|
||||
self.add_data_to_chart(chart, data, y_labels)
|
||||
else:
|
||||
data = response['data']
|
||||
|
@ -396,10 +409,10 @@ class ChartNgCell(CellBase):
|
|||
|
||||
chart.x_labels = data['x_labels']
|
||||
chart.axis_count = min(len(data['series']), 2)
|
||||
chart.compute_sum = False
|
||||
|
||||
if self.statistic.data_type:
|
||||
chart.config.value_formatter = self.get_value_formatter(self.statistic.data_type)
|
||||
chart.compute_sum = False
|
||||
|
||||
if chart.axis_count == 1:
|
||||
data['series'][0]['data'] = self.process_one_dimensional_data(
|
||||
|
@ -419,6 +432,10 @@ class ChartNgCell(CellBase):
|
|||
|
||||
for serie in data['series']:
|
||||
chart.add(serie['label'], serie['data'])
|
||||
|
||||
if self.is_table_chart() and not self.statistic.data_type:
|
||||
self.add_total_to_table(chart, [serie['data'] for serie in data['series']])
|
||||
|
||||
self.configure_chart(chart, width, height)
|
||||
|
||||
return chart
|
||||
|
@ -614,8 +631,6 @@ class ChartNgCell(CellBase):
|
|||
data = self.hide_values(chart, data)
|
||||
if data and self.sort_order != 'none':
|
||||
data = self.sort_values(chart, data)
|
||||
if getattr(chart, 'compute_sum', True) and self.is_table_chart():
|
||||
data = self.add_total_to_line_table(chart, data)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
|
@ -660,6 +675,28 @@ class ChartNgCell(CellBase):
|
|||
chart.x_labels.append(gettext('Total'))
|
||||
return data
|
||||
|
||||
def add_total_to_table(self, chart, series_data):
|
||||
if chart.axis_count == 0:
|
||||
return
|
||||
|
||||
# do not add total for single point
|
||||
if len(series_data) == 1 and len(series_data[0]) == 1:
|
||||
return
|
||||
|
||||
if self.display_total in ('line', 'line-and-column'):
|
||||
chart.x_labels.append(gettext('Total'))
|
||||
for serie in series_data:
|
||||
serie.append(sum(x for x in serie if x is not None))
|
||||
|
||||
if chart.axis_count == 1:
|
||||
return
|
||||
|
||||
if self.display_total in ('column', 'line-and-column'):
|
||||
line_totals = []
|
||||
for line in zip(*series_data):
|
||||
line_totals.append(sum(x for x in line if x is not None))
|
||||
chart.add(gettext('Total'), line_totals)
|
||||
|
||||
def add_data_to_chart(self, chart, data, y_labels):
|
||||
if self.chart_type != 'pie':
|
||||
series_data = []
|
||||
|
|
|
@ -13,3 +13,9 @@
|
|||
font-family: FontAwesome;
|
||||
content: "\f019"; /* download */
|
||||
}
|
||||
|
||||
.dataviz-table.total-line tr:last-child,
|
||||
.dataviz-table.total-line-and-column tr:last-child {
|
||||
font-weight: 600;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load i18n %}
|
||||
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %}
|
||||
{% if cell.is_table_chart %}
|
||||
<div id="chart-{{cell.id}}" class="dataviz-table"></div>
|
||||
<div id="chart-{{cell.id}}" class="dataviz-table total-{{ cell.display_total }}"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
var extra_context = $('#chart-{{cell.id}}').parents('.cell').data('extra-context');
|
||||
|
|
|
@ -111,7 +111,7 @@ class DatavizGraphView(DetailView):
|
|||
|
||||
rendered = chart.render_table(
|
||||
transpose=bool(self.cell.chart_type == 'table-inverted'),
|
||||
total=getattr(chart, 'compute_sum', True),
|
||||
total=bool(self.cell.statistic.service_slug == 'bijoe' and chart.compute_sum),
|
||||
)
|
||||
rendered = rendered.replace('<table>', '<table class="main">')
|
||||
return HttpResponse(rendered)
|
||||
|
|
|
@ -1669,10 +1669,11 @@ def test_chartng_cell_view_new_api_export(app, normal_user, new_api_statistics):
|
|||
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Two series stat'] == [
|
||||
['', 'Serie 1', 'Serie 2'],
|
||||
['2020-10', 0, 2],
|
||||
['2020-11', 16, 1],
|
||||
['2020-12', 2, 0],
|
||||
['', 'Serie 1', 'Serie 2', 'Total'],
|
||||
['2020-10', 0, 2, 2],
|
||||
['2020-11', 16, 1, 17],
|
||||
['2020-12', 2, 0, 2],
|
||||
['Total', 18, 3, 21],
|
||||
]
|
||||
|
||||
cell.statistic = Statistic.objects.get(slug='empty-x-labels')
|
||||
|
@ -1683,6 +1684,86 @@ def test_chartng_cell_view_new_api_export(app, normal_user, new_api_statistics):
|
|||
assert data['Empty x_labels'] == [['Serie 1'], [4242]]
|
||||
|
||||
|
||||
@with_httmock(new_api_mock)
|
||||
@pytest.mark.freeze_time('2023-09-28')
|
||||
def test_chartng_cell_view_new_api_export_ods_total(app, normal_user, new_api_statistics):
|
||||
page = Page.objects.create(title='One', slug='index')
|
||||
cell = ChartNgCell(page=page, order=1, chart_type='table', placeholder='content')
|
||||
cell.statistic = Statistic.objects.get(slug='two-series')
|
||||
cell.save()
|
||||
|
||||
resp = app.get('/')
|
||||
resp = resp.click('Download')
|
||||
|
||||
resp.form['export_format'] = 'ods'
|
||||
ods_resp = resp.form.submit().follow()
|
||||
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Two series stat'] == [
|
||||
['', 'Serie 1', 'Serie 2', 'Total'],
|
||||
['2020-10', 0, 2, 2],
|
||||
['2020-11', 16, 1, 17],
|
||||
['2020-12', 2, 0, 2],
|
||||
['Total', 18, 3, 21],
|
||||
]
|
||||
|
||||
cell.display_total = 'line'
|
||||
cell.save()
|
||||
|
||||
ods_resp = resp.form.submit().follow()
|
||||
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Two series stat'] == [
|
||||
['', 'Serie 1', 'Serie 2'],
|
||||
['2020-10', 0, 2],
|
||||
['2020-11', 16, 1],
|
||||
['2020-12', 2, 0],
|
||||
['Total', 18, 3],
|
||||
]
|
||||
|
||||
cell.display_total = 'column'
|
||||
cell.save()
|
||||
|
||||
ods_resp = resp.form.submit().follow()
|
||||
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Two series stat'] == [
|
||||
['', 'Serie 1', 'Serie 2', 'Total'],
|
||||
['2020-10', 0, 2, 2],
|
||||
['2020-11', 16, 1, 17],
|
||||
['2020-12', 2, 0, 2],
|
||||
]
|
||||
|
||||
cell.display_total = 'none'
|
||||
cell.save()
|
||||
|
||||
ods_resp = resp.form.submit().follow()
|
||||
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Two series stat'] == [
|
||||
['', 'Serie 1', 'Serie 2'],
|
||||
['2020-10', 0, 2],
|
||||
['2020-11', 16, 1],
|
||||
['2020-12', 2, 0],
|
||||
]
|
||||
|
||||
# total is not computed for statistics with data type
|
||||
cell.statistic = Statistic.objects.get(slug='duration-in-seconds')
|
||||
cell.display_total = 'line'
|
||||
cell.save()
|
||||
|
||||
ods_resp = resp.form.submit().follow()
|
||||
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Duration in seconds'] == [
|
||||
['', 'Serie 1'],
|
||||
['Minutes', 140],
|
||||
['Hours', 10800],
|
||||
['Days', 345600],
|
||||
['Months', 8640000],
|
||||
]
|
||||
|
||||
|
||||
@with_httmock(bijoe_mock)
|
||||
def test_chartng_cell_manager(app, admin_user, statistics):
|
||||
page = Page(title='One', slug='index')
|
||||
|
@ -1703,6 +1784,7 @@ def test_chartng_cell_manager(app, admin_user, statistics):
|
|||
assert field_prefix + 'time_range_end' not in resp.form.fields
|
||||
assert field_prefix + 'time_range_start_template' not in resp.form.fields
|
||||
assert field_prefix + 'time_range_end_template_end' not in resp.form.fields
|
||||
assert field_prefix + 'display_total' not in resp.form.fields
|
||||
|
||||
cell.statistic = Statistic.objects.get(slug='example')
|
||||
cell.save()
|
||||
|
@ -1719,6 +1801,7 @@ def test_chartng_cell_manager(app, admin_user, statistics):
|
|||
assert field_prefix + 'time_range_end' not in resp.form.fields
|
||||
assert field_prefix + 'time_range_start_template' not in resp.form.fields
|
||||
assert field_prefix + 'time_range_end_template_end' not in resp.form.fields
|
||||
assert field_prefix + 'display_total' not in resp.form.fields
|
||||
|
||||
cell.statistic = Statistic.objects.get(slug='unavailable-stat')
|
||||
cell.save()
|
||||
|
@ -2194,6 +2277,26 @@ def test_chartng_cell_manager_new_api_tabs(app, admin_user):
|
|||
assert resp.pyquery('[data-tab-slug="appearance"] input[name$="title"]')
|
||||
|
||||
|
||||
@with_httmock(new_api_mock)
|
||||
@pytest.mark.freeze_time('2021-10-06')
|
||||
def test_chartng_cell_manager_new_api_table_chart(app, admin_user, new_api_statistics):
|
||||
page = Page.objects.create(title='One', slug='index')
|
||||
cell = ChartNgCell(page=page, order=1, placeholder='content')
|
||||
cell.statistic = Statistic.objects.get(slug='one-serie')
|
||||
cell.save()
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
|
||||
|
||||
assert field_prefix + 'display_total' not in resp.form.fields
|
||||
|
||||
resp.form[field_prefix + 'chart_type'] = 'table'
|
||||
manager_submit_cell(resp.form)
|
||||
|
||||
assert resp.form[field_prefix + 'display_total'].value == 'line-and-column'
|
||||
|
||||
|
||||
@with_httmock(bijoe_mock)
|
||||
def test_table_cell(app, admin_user, statistics):
|
||||
page = Page(title='One', slug='index')
|
||||
|
|
Loading…
Reference in New Issue