dataviz: load table charts asynchronously (#64315)
This commit is contained in:
parent
5f918b53c0
commit
a52491ea5e
|
@ -36,7 +36,7 @@ from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import ungettext
|
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.library import register_cell_class
|
||||||
from combo.data.models import CellBase, django_template_validator
|
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'
|
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):
|
def get_statistic_data(self, raise_if_not_cached=False, invalidate_cache=False):
|
||||||
return requests.get(
|
return requests.get(
|
||||||
self.statistic.url,
|
self.statistic.url,
|
||||||
|
@ -485,9 +457,6 @@ class ChartNgCell(CellBase):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def request_context(self):
|
def request_context(self):
|
||||||
if hasattr(self, '_context'):
|
|
||||||
return Context(self._context)
|
|
||||||
|
|
||||||
if not hasattr(self, '_request'):
|
if not hasattr(self, '_request'):
|
||||||
raise MissingRequest
|
raise MissingRequest
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,27 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %}
|
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %}
|
||||||
{% if cell.chart_type == "table" %}
|
{% 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 %}
|
{% else %}
|
||||||
<div style="min-height: {{cell.height}}px">
|
<div style="min-height: {{cell.height}}px">
|
||||||
<embed id="chart-{{cell.id}}" type="image/svg+xml" style="width: 100%"/>
|
<embed id="chart-{{cell.id}}" type="image/svg+xml" style="width: 100%"/>
|
||||||
|
|
|
@ -54,7 +54,7 @@ class DatavizGraphView(DetailView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
form = ChartNgPartialForm(request.GET, instance=self.cell)
|
form = ChartNgPartialForm(request.GET, instance=self.cell)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return self.svg_error(_('Wrong parameters.'))
|
return self.error(_('Wrong parameters.'))
|
||||||
|
|
||||||
request.extra_context = {}
|
request.extra_context = {}
|
||||||
if request.GET.get('ctx'):
|
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),
|
height=int(request.GET['height']) if request.GET.get('height') else int(self.cell.height),
|
||||||
)
|
)
|
||||||
except UnsupportedDataSet:
|
except UnsupportedDataSet:
|
||||||
return self.svg_error(_('Unsupported dataset.'))
|
return self.error(_('Unsupported dataset.'))
|
||||||
except MissingVariable:
|
except MissingVariable:
|
||||||
return self.svg_error(_('Page variable not found.'))
|
return self.error(_('Page variable not found.'))
|
||||||
except TemplateSyntaxError:
|
except TemplateSyntaxError:
|
||||||
return self.svg_error(_('Syntax error in page variable.'))
|
return self.error(_('Syntax error in page variable.'))
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
return self.svg_error(_('Cannot evaluate page variable.'))
|
return self.error(_('Cannot evaluate page variable.'))
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
if e.response.status_code == 404:
|
if e.response.status_code == 404:
|
||||||
return self.svg_error(_('Visualization not found.'))
|
return self.error(_('Visualization not found.'))
|
||||||
else:
|
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')
|
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 = {
|
context = {
|
||||||
'width': self.request.GET.get('width', 200),
|
'width': self.request.GET.get('width', 200),
|
||||||
'text': error_text,
|
'text': error_text,
|
||||||
|
|
|
@ -1142,15 +1142,14 @@ def test_chartng_cell_view(app, normal_user, statistics):
|
||||||
# table visualization
|
# table visualization
|
||||||
cell.chart_type = 'table'
|
cell.chart_type = 'table'
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get('/')
|
resp = app.get(location, status=200)
|
||||||
assert '<td>222</td>' in resp.text
|
assert '<td>222</td>' in resp.text
|
||||||
|
|
||||||
# unsupported dataset
|
# unsupported dataset
|
||||||
cell.statistic = Statistic.objects.get(slug='seventh')
|
cell.statistic = Statistic.objects.get(slug='seventh')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location) # get data in cache
|
resp = app.get(location, status=200)
|
||||||
resp = app.get('/')
|
assert '<p>Unsupported dataset.</p>' in resp.text
|
||||||
assert 'Unsupported dataset' in resp.text
|
|
||||||
|
|
||||||
cell.chart_type = 'bar'
|
cell.chart_type = 'bar'
|
||||||
cell.save()
|
cell.save()
|
||||||
|
@ -1161,8 +1160,7 @@ def test_chartng_cell_view(app, normal_user, statistics):
|
||||||
cell.statistic = Statistic.objects.get(slug='eighth')
|
cell.statistic = Statistic.objects.get(slug='eighth')
|
||||||
cell.chart_type = 'table'
|
cell.chart_type = 'table'
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location) # get data in cache
|
resp = app.get(location, status=200)
|
||||||
resp = app.get('/')
|
|
||||||
assert '<td>Less than an hour</td>' in resp.text
|
assert '<td>Less than an hour</td>' in resp.text
|
||||||
assert '<td>1 day and 10 hours</td>' in resp.text
|
assert '<td>1 day and 10 hours</td>' in resp.text
|
||||||
assert '<td>2 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.statistic = Statistic.objects.get(slug='tenth')
|
||||||
cell.chart_type = 'table'
|
cell.chart_type = 'table'
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location) # get data in cache
|
resp = app.get(location, status=200)
|
||||||
resp = app.get('/')
|
|
||||||
assert '<td>10.0%</td>' in resp.text
|
assert '<td>10.0%</td>' in resp.text
|
||||||
|
|
||||||
cell.chart_type = 'bar'
|
cell.chart_type = 'bar'
|
||||||
|
@ -1195,13 +1192,6 @@ def test_chartng_cell_view(app, normal_user, statistics):
|
||||||
resp = app.get(location)
|
resp = app.get(location)
|
||||||
assert 'not found' in resp.text
|
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)
|
@with_httmock(new_api_mock)
|
||||||
def test_chartng_cell_view_new_api(app, normal_user, new_api_statistics):
|
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
|
# table visualization
|
||||||
cell.chart_type = 'table'
|
cell.chart_type = 'table'
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location) # populate cache
|
resp = app.get(location, status=200)
|
||||||
resp = app.get('/')
|
|
||||||
assert '<td>18</td>' in resp.text
|
assert '<td>18</td>' in resp.text
|
||||||
|
|
||||||
# deleted visualization
|
# deleted visualization
|
||||||
|
@ -1643,33 +1632,28 @@ def test_table_cell(app, admin_user, statistics):
|
||||||
cell.save()
|
cell.save()
|
||||||
location = '/api/dataviz/graph/%s/' % cell.id
|
location = '/api/dataviz/graph/%s/' % cell.id
|
||||||
resp = app.get(location)
|
resp = app.get(location)
|
||||||
resp = app.get('/')
|
|
||||||
assert resp.text.count('Total') == 1
|
assert resp.text.count('Total') == 1
|
||||||
|
|
||||||
cell.statistic = Statistic.objects.get(slug='second')
|
cell.statistic = Statistic.objects.get(slug='second')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location)
|
resp = app.get(location)
|
||||||
resp = app.get('/')
|
|
||||||
assert resp.text.count('Total') == 1
|
assert resp.text.count('Total') == 1
|
||||||
|
|
||||||
cell.statistic = Statistic.objects.get(slug='third')
|
cell.statistic = Statistic.objects.get(slug='third')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location)
|
resp = app.get(location)
|
||||||
resp = app.get('/')
|
|
||||||
assert '114' in resp.text
|
assert '114' in resp.text
|
||||||
assert resp.text.count('Total') == 2
|
assert resp.text.count('Total') == 2
|
||||||
|
|
||||||
cell.statistic = Statistic.objects.get(slug='fourth')
|
cell.statistic = Statistic.objects.get(slug='fourth')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location)
|
resp = app.get(location)
|
||||||
resp = app.get('/')
|
|
||||||
assert resp.text.count('Total') == 0
|
assert resp.text.count('Total') == 0
|
||||||
|
|
||||||
# total of durations is not computed
|
# total of durations is not computed
|
||||||
cell.statistic = Statistic.objects.get(slug='eighth')
|
cell.statistic = Statistic.objects.get(slug='eighth')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get(location)
|
resp = app.get(location)
|
||||||
resp = app.get('/')
|
|
||||||
assert resp.text.count('Total') == 0
|
assert resp.text.count('Total') == 0
|
||||||
|
|
||||||
|
|
||||||
|
@ -1682,18 +1666,19 @@ def test_table_cell_new_api(app, admin_user, new_api_statistics):
|
||||||
cell.save()
|
cell.save()
|
||||||
|
|
||||||
app = login(app)
|
app = login(app)
|
||||||
resp = app.get('/')
|
location = '/api/dataviz/graph/%s/' % cell.id
|
||||||
|
resp = app.get(location)
|
||||||
assert resp.text.count('Total') == 1
|
assert resp.text.count('Total') == 1
|
||||||
|
|
||||||
cell.statistic = Statistic.objects.get(slug='two-series')
|
cell.statistic = Statistic.objects.get(slug='two-series')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get('/')
|
resp = app.get(location)
|
||||||
assert '21' in resp.text
|
assert '21' in resp.text
|
||||||
assert resp.text.count('Total') == 2
|
assert resp.text.count('Total') == 2
|
||||||
|
|
||||||
cell.statistic = Statistic.objects.get(slug='no-data')
|
cell.statistic = Statistic.objects.get(slug='no-data')
|
||||||
cell.save()
|
cell.save()
|
||||||
resp = app.get('/')
|
resp = app.get(location)
|
||||||
assert resp.text.count('Total') == 0
|
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)
|
@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.objects.create(title='One', slug='index')
|
||||||
page = Page.objects.create(
|
page = Page.objects.create(
|
||||||
title='One',
|
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.filter_params = {'service': 'chrono', 'ou': 'variable:foo'}
|
||||||
cell.save()
|
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]
|
request = new_api_mock.call['requests'][0]
|
||||||
assert 'service=chrono' in request.url
|
assert 'service=chrono' in request.url
|
||||||
assert 'ou=bar' 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.filter_params = {'service': 'chrono', 'ou': 'variable:unknown'}
|
||||||
cell.save()
|
cell.save()
|
||||||
|
|
||||||
content = cell.render({'synchronous': True})
|
resp = app.get(location)
|
||||||
assert len(new_api_mock.call['requests']) == 1
|
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
|
# variable with invalid syntax
|
||||||
cell.filter_params = {'service': 'chrono', 'ou': 'variable:syntax_error'}
|
cell.filter_params = {'service': 'chrono', 'ou': 'variable:syntax_error'}
|
||||||
cell.save()
|
cell.save()
|
||||||
|
|
||||||
content = cell.render({**page.extra_variables, 'synchronous': True})
|
resp = app.get(location)
|
||||||
assert len(new_api_mock.call['requests']) == 1
|
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
|
# variable with missing context
|
||||||
cell.filter_params = {'service': 'chrono', 'ou': 'variable:subslug_dependant'}
|
cell.filter_params = {'service': 'chrono', 'ou': 'variable:subslug_dependant'}
|
||||||
cell.save()
|
cell.save()
|
||||||
|
|
||||||
content = cell.render({**page.extra_variables, 'synchronous': True})
|
resp = app.get(location)
|
||||||
assert len(new_api_mock.call['requests']) == 1
|
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):
|
def test_dataviz_check_validity(nocache):
|
||||||
|
|
Loading…
Reference in New Issue