dataviz: allow control of total display in tables (#85654) #233
|
@ -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,
|
||||
vdeniaud
commented
pygal ajoutait le total dans une balise pygal ajoutait le total dans une balise `<tfood>` et ça se retrouvait pris dans un style posé par gadjo, donc ce bout pour conserver le rendu
|
||||
.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)
|
||||
|
@ -150,6 +150,8 @@ class DatavizGraphView(DetailView):
|
|||
line = [x or 0 for x in line]
|
||||
data.append(line)
|
||||
|
||||
data = [list(line) for line in zip(*data)]
|
||||
|
||||
output = io.BytesIO()
|
||||
pyexcel_ods.save_data(output, {self.cell.title or self.cell.statistic.label: data})
|
||||
output.seek(0)
|
||||
|
|
|
@ -1669,9 +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'] == [
|
||||
['', '2020-10', '2020-11', '2020-12'],
|
||||
['Serie 1', 0, 16, 2],
|
||||
['Serie 2', 2, 1, 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')
|
||||
|
@ -1679,7 +1681,87 @@ def test_chartng_cell_view_new_api_export(app, normal_user, new_api_statistics):
|
|||
|
||||
ods_resp = resp.form.submit().follow()
|
||||
data = pyexcel_ods.get_data(io.BytesIO(ods_resp.body))
|
||||
assert data['Empty x_labels'] == [['Serie 1', 4242]]
|
||||
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):
|
||||
vdeniaud
commented
Détour par l'export ODS pour tester tout ça, nettement plus clair que les tests qui font Détour par l'export ODS pour tester tout ça, nettement plus clair que les tests qui font `'<td>222</td>' in resp.text` actuels
|
||||
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)
|
||||
|
@ -1694,31 +1776,37 @@ def test_chartng_cell_manager(app, admin_user, statistics):
|
|||
|
||||
cell = ChartNgCell.objects.create(page=page, order=1, placeholder='content')
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
assert 'time_range' not in resp.form.fields
|
||||
assert 'time_range_start' not in resp.form.fields
|
||||
assert 'time_range_end' not in resp.form.fields
|
||||
assert 'time_range_start_template' not in resp.form.fields
|
||||
assert 'time_range_end_template_end' not in resp.form.fields
|
||||
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id
|
||||
|
||||
assert field_prefix + 'statistic' in resp.form.fields
|
||||
assert field_prefix + 'time_range' not in resp.form.fields
|
||||
assert field_prefix + 'time_range_start' not in resp.form.fields
|
||||
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()
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
statistics_field = resp.form['cdataviz_chartngcell-%s-statistic' % cell.id]
|
||||
statistics_field = resp.form[field_prefix + 'statistic']
|
||||
# available visualizations and a blank choice
|
||||
assert len(statistics_field.options) == len(VISUALIZATION_JSON) + 1
|
||||
assert statistics_field.value == str(cell.statistic.pk)
|
||||
assert statistics_field.options[1][2] == 'test: eighth visualization (duration)'
|
||||
assert not 'Unavailable Stat' in resp.text
|
||||
assert 'time_range' not in resp.form.fields
|
||||
assert 'time_range_start' not in resp.form.fields
|
||||
assert 'time_range_end' not in resp.form.fields
|
||||
assert 'time_range_start_template' not in resp.form.fields
|
||||
assert 'time_range_end_template_end' not in resp.form.fields
|
||||
|
||||
assert field_prefix + 'time_range' not in resp.form.fields
|
||||
assert field_prefix + 'time_range_start' not in resp.form.fields
|
||||
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()
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
statistics_field = resp.form['cdataviz_chartngcell-%s-statistic' % cell.id]
|
||||
statistics_field = resp.form[field_prefix + 'statistic']
|
||||
# available visualizations, a blank choice and the current unavailable visualization
|
||||
assert len(statistics_field.options) == len(VISUALIZATION_JSON) + 2
|
||||
assert 'Unavailable Stat' in resp.text
|
||||
|
@ -2000,13 +2088,13 @@ def test_chartng_cell_manager_subfilters(app, admin_user, new_api_statistics):
|
|||
manager_submit_cell(resp.form)
|
||||
|
||||
assert len(new_api_mock.call['requests']) == 1
|
||||
assert 'menu' not in resp.form.fields
|
||||
assert field_prefix + 'menu' not in resp.form.fields
|
||||
|
||||
resp.form[field_prefix + 'form'] = 'error'
|
||||
manager_submit_cell(resp.form)
|
||||
|
||||
assert len(new_api_mock.call['requests']) == 2
|
||||
assert 'menu' not in resp.form.fields
|
||||
assert field_prefix + 'menu' not in resp.form.fields
|
||||
|
||||
# choice with subfilter
|
||||
resp.form[field_prefix + 'form'] = 'food-request'
|
||||
|
@ -2189,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
Le nouveau champ n'apparaît pas pour bijoe