dataviz: load table charts asynchronously (#64315)

This commit is contained in:
Valentin Deniaud 2022-04-20 16:00:32 +02:00
parent 5f918b53c0
commit a52491ea5e
4 changed files with 63 additions and 74 deletions

View File

@ -36,7 +36,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from requests.exceptions import HTTPError, RequestException
from requests.exceptions import RequestException
from combo.data.library import register_cell_class
from combo.data.models import CellBase, django_template_validator
@ -308,34 +308,6 @@ class ChartNgCell(CellBase):
resp, not_found_code='statistic_data_not_found', invalid_code='statistic_url_invalid'
)
def get_cell_extra_context(self, context):
ctx = super().get_cell_extra_context(context)
if self.chart_type == 'table' and self.statistic and self.statistic.url:
self._context = context
try:
chart = self.get_chart(raise_if_not_cached=not (context.get('synchronous')))
except UnsupportedDataSet:
ctx['table'] = '<p>%s</p>' % _('Unsupported dataset.')
except MissingVariable:
ctx['table'] = '<p>%s</p>' % _('Page variable not found.')
except TemplateSyntaxError:
ctx['table'] = '<p>%s</p>' % _('Syntax error in page variable.')
except VariableDoesNotExist:
ctx['table'] = '<p>%s</p>' % _('Cannot evaluate page variable.')
except HTTPError as e:
if e.response.status_code == 404:
ctx['table'] = '<p>%s</p>' % _('Visualization not found.')
else:
if not chart.raw_series:
ctx['table'] = '<p>%s</p>' % _('No data.')
else:
ctx['table'] = chart.render_table(
transpose=bool(chart.axis_count == 2),
total=getattr(chart, 'compute_sum', True),
)
ctx['table'] = ctx['table'].replace('<table>', '<table class="main">')
return ctx
def get_statistic_data(self, raise_if_not_cached=False, invalidate_cache=False):
return requests.get(
self.statistic.url,
@ -485,9 +457,6 @@ class ChartNgCell(CellBase):
@cached_property
def request_context(self):
if hasattr(self, '_context'):
return Context(self._context)
if not hasattr(self, '_request'):
raise MissingRequest

View File

@ -1,7 +1,27 @@
{% load i18n %}
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %}
{% if cell.chart_type == "table" %}
{{table|safe}}
<div id="chart-{{cell.id}}"></div>
<script>
$(function() {
var extra_context = $('#chart-{{cell.id}}').parents('.cell').data('extra-context');
var chart_filters_form = $('#chart-filters');
$(window).on('combo:refresh-graphs', function() {
qs = [];
if(chart_filters_form)
qs.push(chart_filters_form.serialize());
if(extra_context)
qs.push('ctx=' + extra_context);
$.ajax({
url : "{% url 'combo-dataviz-graph' cell=cell.id %}?" + qs.join('&'),
type: 'GET',
success: function(data) {
$('#chart-{{cell.id}}').html(data);
}
});
}).trigger('combo:refresh-graphs');
});
</script>
{% else %}
<div style="min-height: {{cell.height}}px">
<embed id="chart-{{cell.id}}" type="image/svg+xml" style="width: 100%"/>

View File

@ -54,7 +54,7 @@ class DatavizGraphView(DetailView):
def get(self, request, *args, **kwargs):
form = ChartNgPartialForm(request.GET, instance=self.cell)
if not form.is_valid():
return self.svg_error(_('Wrong parameters.'))
return self.error(_('Wrong parameters.'))
request.extra_context = {}
if request.GET.get('ctx'):
@ -70,22 +70,36 @@ class DatavizGraphView(DetailView):
height=int(request.GET['height']) if request.GET.get('height') else int(self.cell.height),
)
except UnsupportedDataSet:
return self.svg_error(_('Unsupported dataset.'))
return self.error(_('Unsupported dataset.'))
except MissingVariable:
return self.svg_error(_('Page variable not found.'))
return self.error(_('Page variable not found.'))
except TemplateSyntaxError:
return self.svg_error(_('Syntax error in page variable.'))
return self.error(_('Syntax error in page variable.'))
except VariableDoesNotExist:
return self.svg_error(_('Cannot evaluate page variable.'))
return self.error(_('Cannot evaluate page variable.'))
except HTTPError as e:
if e.response.status_code == 404:
return self.svg_error(_('Visualization not found.'))
return self.error(_('Visualization not found.'))
else:
return self.svg_error(_('Unknown HTTP error: %s' % e))
return self.error(_('Unknown HTTP error: %s' % e))
if self.cell.chart_type == 'table':
if not chart.raw_series:
return self.error(_('No data'))
rendered = chart.render_table(
transpose=bool(chart.axis_count == 2),
total=getattr(chart, 'compute_sum', True),
)
rendered = rendered.replace('<table>', '<table class="main">')
return HttpResponse(rendered)
return HttpResponse(chart.render(), content_type='image/svg+xml')
def svg_error(self, error_text):
def error(self, error_text):
if self.cell.chart_type == 'table':
return HttpResponse('<p>%s</p>' % error_text)
context = {
'width': self.request.GET.get('width', 200),
'text': error_text,

View File

@ -1142,15 +1142,14 @@ def test_chartng_cell_view(app, normal_user, statistics):
# table visualization
cell.chart_type = 'table'
cell.save()
resp = app.get('/')
resp = app.get(location, status=200)
assert '<td>222</td>' in resp.text
# unsupported dataset
cell.statistic = Statistic.objects.get(slug='seventh')
cell.save()
resp = app.get(location) # get data in cache
resp = app.get('/')
assert 'Unsupported dataset' in resp.text
resp = app.get(location, status=200)
assert '<p>Unsupported dataset.</p>' in resp.text
cell.chart_type = 'bar'
cell.save()
@ -1161,8 +1160,7 @@ def test_chartng_cell_view(app, normal_user, statistics):
cell.statistic = Statistic.objects.get(slug='eighth')
cell.chart_type = 'table'
cell.save()
resp = app.get(location) # get data in cache
resp = app.get('/')
resp = app.get(location, status=200)
assert '<td>Less than an hour</td>' in resp.text
assert '<td>1 day and 10 hours</td>' in resp.text
assert '<td>2 hours</td>' in resp.text
@ -1180,8 +1178,7 @@ def test_chartng_cell_view(app, normal_user, statistics):
cell.statistic = Statistic.objects.get(slug='tenth')
cell.chart_type = 'table'
cell.save()
resp = app.get(location) # get data in cache
resp = app.get('/')
resp = app.get(location, status=200)
assert '<td>10.0%</td>' in resp.text
cell.chart_type = 'bar'
@ -1195,13 +1192,6 @@ def test_chartng_cell_view(app, normal_user, statistics):
resp = app.get(location)
assert 'not found' in resp.text
# cell with no statistic chosen
cell.chart_type = 'table'
cell.statistic = None
cell.save()
resp = app.get('/')
assert not 'cell' in resp.text
@with_httmock(new_api_mock)
def test_chartng_cell_view_new_api(app, normal_user, new_api_statistics):
@ -1218,8 +1208,7 @@ def test_chartng_cell_view_new_api(app, normal_user, new_api_statistics):
# table visualization
cell.chart_type = 'table'
cell.save()
resp = app.get(location) # populate cache
resp = app.get('/')
resp = app.get(location, status=200)
assert '<td>18</td>' in resp.text
# deleted visualization
@ -1643,33 +1632,28 @@ def test_table_cell(app, admin_user, statistics):
cell.save()
location = '/api/dataviz/graph/%s/' % cell.id
resp = app.get(location)
resp = app.get('/')
assert resp.text.count('Total') == 1
cell.statistic = Statistic.objects.get(slug='second')
cell.save()
resp = app.get(location)
resp = app.get('/')
assert resp.text.count('Total') == 1
cell.statistic = Statistic.objects.get(slug='third')
cell.save()
resp = app.get(location)
resp = app.get('/')
assert '114' in resp.text
assert resp.text.count('Total') == 2
cell.statistic = Statistic.objects.get(slug='fourth')
cell.save()
resp = app.get(location)
resp = app.get('/')
assert resp.text.count('Total') == 0
# total of durations is not computed
cell.statistic = Statistic.objects.get(slug='eighth')
cell.save()
resp = app.get(location)
resp = app.get('/')
assert resp.text.count('Total') == 0
@ -1682,18 +1666,19 @@ def test_table_cell_new_api(app, admin_user, new_api_statistics):
cell.save()
app = login(app)
resp = app.get('/')
location = '/api/dataviz/graph/%s/' % cell.id
resp = app.get(location)
assert resp.text.count('Total') == 1
cell.statistic = Statistic.objects.get(slug='two-series')
cell.save()
resp = app.get('/')
resp = app.get(location)
assert '21' in resp.text
assert resp.text.count('Total') == 2
cell.statistic = Statistic.objects.get(slug='no-data')
cell.save()
resp = app.get('/')
resp = app.get(location)
assert resp.text.count('Total') == 0
@ -2009,7 +1994,7 @@ def test_chartng_cell_new_api_filter_params_page_variables(app, admin_user, new_
@with_httmock(new_api_mock)
def test_chartng_cell_new_api_filter_params_page_variables_table(new_api_statistics, nocache):
def test_chartng_cell_new_api_filter_params_page_variables_table(new_api_statistics, app, nocache):
Page.objects.create(title='One', slug='index')
page = Page.objects.create(
title='One',
@ -2026,7 +2011,8 @@ def test_chartng_cell_new_api_filter_params_page_variables_table(new_api_statist
cell.filter_params = {'service': 'chrono', 'ou': 'variable:foo'}
cell.save()
cell.render({**page.extra_variables, 'synchronous': True})
location = '/api/dataviz/graph/%s/' % cell.pk
app.get(location)
request = new_api_mock.call['requests'][0]
assert 'service=chrono' in request.url
assert 'ou=bar' in request.url
@ -2035,25 +2021,25 @@ def test_chartng_cell_new_api_filter_params_page_variables_table(new_api_statist
cell.filter_params = {'service': 'chrono', 'ou': 'variable:unknown'}
cell.save()
content = cell.render({'synchronous': True})
resp = app.get(location)
assert len(new_api_mock.call['requests']) == 1
assert 'Page variable not found.' in content
assert 'Page variable not found.' in resp.text
# variable with invalid syntax
cell.filter_params = {'service': 'chrono', 'ou': 'variable:syntax_error'}
cell.save()
content = cell.render({**page.extra_variables, 'synchronous': True})
resp = app.get(location)
assert len(new_api_mock.call['requests']) == 1
assert 'Syntax error in page variable.' in content
assert 'Syntax error in page variable.' in resp.text
# variable with missing context
cell.filter_params = {'service': 'chrono', 'ou': 'variable:subslug_dependant'}
cell.save()
content = cell.render({**page.extra_variables, 'synchronous': True})
resp = app.get(location)
assert len(new_api_mock.call['requests']) == 1
assert 'Cannot evaluate page variable.' in content
assert 'Cannot evaluate page variable.' in resp.text
def test_dataviz_check_validity(nocache):