0001-dataviz-load-table-charts-asynchronously-64315.patch
combo/apps/dataviz/models.py | ||
---|---|---|
36 | 36 |
from django.utils.translation import gettext |
37 | 37 |
from django.utils.translation import ugettext_lazy as _ |
38 | 38 |
from django.utils.translation import ungettext |
39 |
from requests.exceptions import HTTPError, RequestException
|
|
39 |
from requests.exceptions import RequestException |
|
40 | 40 | |
41 | 41 |
from combo.data.library import register_cell_class |
42 | 42 |
from combo.data.models import CellBase, django_template_validator |
... | ... | |
308 | 308 |
resp, not_found_code='statistic_data_not_found', invalid_code='statistic_url_invalid' |
309 | 309 |
) |
310 | 310 | |
311 |
def get_cell_extra_context(self, context): |
|
312 |
ctx = super().get_cell_extra_context(context) |
|
313 |
if self.chart_type == 'table' and self.statistic and self.statistic.url: |
|
314 |
self._context = context |
|
315 |
try: |
|
316 |
chart = self.get_chart(raise_if_not_cached=not (context.get('synchronous'))) |
|
317 |
except UnsupportedDataSet: |
|
318 |
ctx['table'] = '<p>%s</p>' % _('Unsupported dataset.') |
|
319 |
except MissingVariable: |
|
320 |
ctx['table'] = '<p>%s</p>' % _('Page variable not found.') |
|
321 |
except TemplateSyntaxError: |
|
322 |
ctx['table'] = '<p>%s</p>' % _('Syntax error in page variable.') |
|
323 |
except VariableDoesNotExist: |
|
324 |
ctx['table'] = '<p>%s</p>' % _('Cannot evaluate page variable.') |
|
325 |
except HTTPError as e: |
|
326 |
if e.response.status_code == 404: |
|
327 |
ctx['table'] = '<p>%s</p>' % _('Visualization not found.') |
|
328 |
else: |
|
329 |
if not chart.raw_series: |
|
330 |
ctx['table'] = '<p>%s</p>' % _('No data.') |
|
331 |
else: |
|
332 |
ctx['table'] = chart.render_table( |
|
333 |
transpose=bool(chart.axis_count == 2), |
|
334 |
total=getattr(chart, 'compute_sum', True), |
|
335 |
) |
|
336 |
ctx['table'] = ctx['table'].replace('<table>', '<table class="main">') |
|
337 |
return ctx |
|
338 | ||
339 | 311 |
def get_statistic_data(self, raise_if_not_cached=False, invalidate_cache=False): |
340 | 312 |
return requests.get( |
341 | 313 |
self.statistic.url, |
... | ... | |
485 | 457 | |
486 | 458 |
@cached_property |
487 | 459 |
def request_context(self): |
488 |
if hasattr(self, '_context'): |
|
489 |
return Context(self._context) |
|
490 | ||
491 | 460 |
if not hasattr(self, '_request'): |
492 | 461 |
raise MissingRequest |
493 | 462 |
combo/apps/dataviz/templates/combo/chartngcell.html | ||
---|---|---|
1 | 1 |
{% load i18n %} |
2 | 2 |
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %} |
3 | 3 |
{% if cell.chart_type == "table" %} |
4 |
{{table|safe}} |
|
4 |
<div id="chart-{{cell.id}}"></div> |
|
5 |
<script> |
|
6 |
$(function() { |
|
7 |
var extra_context = $('#chart-{{cell.id}}').parents('.cell').data('extra-context'); |
|
8 |
var chart_filters_form = $('#chart-filters'); |
|
9 |
$(window).on('combo:refresh-graphs', function() { |
|
10 |
qs = []; |
|
11 |
if(chart_filters_form) |
|
12 |
qs.push(chart_filters_form.serialize()); |
|
13 |
if(extra_context) |
|
14 |
qs.push('ctx=' + extra_context); |
|
15 |
$.ajax({ |
|
16 |
url : "{% url 'combo-dataviz-graph' cell=cell.id %}?" + qs.join('&'), |
|
17 |
type: 'GET', |
|
18 |
success: function(data) { |
|
19 |
$('#chart-{{cell.id}}').html(data); |
|
20 |
} |
|
21 |
}); |
|
22 |
}).trigger('combo:refresh-graphs'); |
|
23 |
}); |
|
24 |
</script> |
|
5 | 25 |
{% else %} |
6 | 26 |
<div style="min-height: {{cell.height}}px"> |
7 | 27 |
<embed id="chart-{{cell.id}}" type="image/svg+xml" style="width: 100%"/> |
combo/apps/dataviz/views.py | ||
---|---|---|
54 | 54 |
def get(self, request, *args, **kwargs): |
55 | 55 |
form = ChartNgPartialForm(request.GET, instance=self.cell) |
56 | 56 |
if not form.is_valid(): |
57 |
return self.svg_error(_('Wrong parameters.'))
|
|
57 |
return self.error(_('Wrong parameters.')) |
|
58 | 58 | |
59 | 59 |
request.extra_context = {} |
60 | 60 |
if request.GET.get('ctx'): |
... | ... | |
70 | 70 |
height=int(request.GET['height']) if request.GET.get('height') else int(self.cell.height), |
71 | 71 |
) |
72 | 72 |
except UnsupportedDataSet: |
73 |
return self.svg_error(_('Unsupported dataset.'))
|
|
73 |
return self.error(_('Unsupported dataset.')) |
|
74 | 74 |
except MissingVariable: |
75 |
return self.svg_error(_('Page variable not found.'))
|
|
75 |
return self.error(_('Page variable not found.')) |
|
76 | 76 |
except TemplateSyntaxError: |
77 |
return self.svg_error(_('Syntax error in page variable.'))
|
|
77 |
return self.error(_('Syntax error in page variable.')) |
|
78 | 78 |
except VariableDoesNotExist: |
79 |
return self.svg_error(_('Cannot evaluate page variable.'))
|
|
79 |
return self.error(_('Cannot evaluate page variable.')) |
|
80 | 80 |
except HTTPError as e: |
81 | 81 |
if e.response.status_code == 404: |
82 |
return self.svg_error(_('Visualization not found.'))
|
|
82 |
return self.error(_('Visualization not found.')) |
|
83 | 83 |
else: |
84 |
return self.svg_error(_('Unknown HTTP error: %s' % e)) |
|
84 |
return self.error(_('Unknown HTTP error: %s' % e)) |
|
85 | ||
86 |
if self.cell.chart_type == 'table': |
|
87 |
if not chart.raw_series: |
|
88 |
return self.error(_('No data')) |
|
89 | ||
90 |
rendered = chart.render_table( |
|
91 |
transpose=bool(chart.axis_count == 2), |
|
92 |
total=getattr(chart, 'compute_sum', True), |
|
93 |
) |
|
94 |
rendered = rendered.replace('<table>', '<table class="main">') |
|
95 |
return HttpResponse(rendered) |
|
85 | 96 | |
86 | 97 |
return HttpResponse(chart.render(), content_type='image/svg+xml') |
87 | 98 | |
88 |
def svg_error(self, error_text): |
|
99 |
def error(self, error_text): |
|
100 |
if self.cell.chart_type == 'table': |
|
101 |
return HttpResponse('<p>%s</p>' % error_text) |
|
102 | ||
89 | 103 |
context = { |
90 | 104 |
'width': self.request.GET.get('width', 200), |
91 | 105 |
'text': error_text, |
tests/test_dataviz.py | ||
---|---|---|
1142 | 1142 |
# table visualization |
1143 | 1143 |
cell.chart_type = 'table' |
1144 | 1144 |
cell.save() |
1145 |
resp = app.get('/')
|
|
1145 |
resp = app.get(location, status=200)
|
|
1146 | 1146 |
assert '<td>222</td>' in resp.text |
1147 | 1147 | |
1148 | 1148 |
# unsupported dataset |
1149 | 1149 |
cell.statistic = Statistic.objects.get(slug='seventh') |
1150 | 1150 |
cell.save() |
1151 |
resp = app.get(location) # get data in cache |
|
1152 |
resp = app.get('/') |
|
1153 |
assert 'Unsupported dataset' in resp.text |
|
1151 |
resp = app.get(location, status=200) |
|
1152 |
assert '<p>Unsupported dataset.</p>' in resp.text |
|
1154 | 1153 | |
1155 | 1154 |
cell.chart_type = 'bar' |
1156 | 1155 |
cell.save() |
... | ... | |
1161 | 1160 |
cell.statistic = Statistic.objects.get(slug='eighth') |
1162 | 1161 |
cell.chart_type = 'table' |
1163 | 1162 |
cell.save() |
1164 |
resp = app.get(location) # get data in cache |
|
1165 |
resp = app.get('/') |
|
1163 |
resp = app.get(location, status=200) |
|
1166 | 1164 |
assert '<td>Less than an hour</td>' in resp.text |
1167 | 1165 |
assert '<td>1 day and 10 hours</td>' in resp.text |
1168 | 1166 |
assert '<td>2 hours</td>' in resp.text |
... | ... | |
1180 | 1178 |
cell.statistic = Statistic.objects.get(slug='tenth') |
1181 | 1179 |
cell.chart_type = 'table' |
1182 | 1180 |
cell.save() |
1183 |
resp = app.get(location) # get data in cache |
|
1184 |
resp = app.get('/') |
|
1181 |
resp = app.get(location, status=200) |
|
1185 | 1182 |
assert '<td>10.0%</td>' in resp.text |
1186 | 1183 | |
1187 | 1184 |
cell.chart_type = 'bar' |
... | ... | |
1195 | 1192 |
resp = app.get(location) |
1196 | 1193 |
assert 'not found' in resp.text |
1197 | 1194 | |
1198 |
# cell with no statistic chosen |
|
1199 |
cell.chart_type = 'table' |
|
1200 |
cell.statistic = None |
|
1201 |
cell.save() |
|
1202 |
resp = app.get('/') |
|
1203 |
assert not 'cell' in resp.text |
|
1204 | ||
1205 | 1195 | |
1206 | 1196 |
@with_httmock(new_api_mock) |
1207 | 1197 |
def test_chartng_cell_view_new_api(app, normal_user, new_api_statistics): |
... | ... | |
1218 | 1208 |
# table visualization |
1219 | 1209 |
cell.chart_type = 'table' |
1220 | 1210 |
cell.save() |
1221 |
resp = app.get(location) # populate cache |
|
1222 |
resp = app.get('/') |
|
1211 |
resp = app.get(location, status=200) |
|
1223 | 1212 |
assert '<td>18</td>' in resp.text |
1224 | 1213 | |
1225 | 1214 |
# deleted visualization |
... | ... | |
1643 | 1632 |
cell.save() |
1644 | 1633 |
location = '/api/dataviz/graph/%s/' % cell.id |
1645 | 1634 |
resp = app.get(location) |
1646 |
resp = app.get('/') |
|
1647 | 1635 |
assert resp.text.count('Total') == 1 |
1648 | 1636 | |
1649 | 1637 |
cell.statistic = Statistic.objects.get(slug='second') |
1650 | 1638 |
cell.save() |
1651 | 1639 |
resp = app.get(location) |
1652 |
resp = app.get('/') |
|
1653 | 1640 |
assert resp.text.count('Total') == 1 |
1654 | 1641 | |
1655 | 1642 |
cell.statistic = Statistic.objects.get(slug='third') |
1656 | 1643 |
cell.save() |
1657 | 1644 |
resp = app.get(location) |
1658 |
resp = app.get('/') |
|
1659 | 1645 |
assert '114' in resp.text |
1660 | 1646 |
assert resp.text.count('Total') == 2 |
1661 | 1647 | |
1662 | 1648 |
cell.statistic = Statistic.objects.get(slug='fourth') |
1663 | 1649 |
cell.save() |
1664 | 1650 |
resp = app.get(location) |
1665 |
resp = app.get('/') |
|
1666 | 1651 |
assert resp.text.count('Total') == 0 |
1667 | 1652 | |
1668 | 1653 |
# total of durations is not computed |
1669 | 1654 |
cell.statistic = Statistic.objects.get(slug='eighth') |
1670 | 1655 |
cell.save() |
1671 | 1656 |
resp = app.get(location) |
1672 |
resp = app.get('/') |
|
1673 | 1657 |
assert resp.text.count('Total') == 0 |
1674 | 1658 | |
1675 | 1659 | |
... | ... | |
1682 | 1666 |
cell.save() |
1683 | 1667 | |
1684 | 1668 |
app = login(app) |
1685 |
resp = app.get('/') |
|
1669 |
location = '/api/dataviz/graph/%s/' % cell.id |
|
1670 |
resp = app.get(location) |
|
1686 | 1671 |
assert resp.text.count('Total') == 1 |
1687 | 1672 | |
1688 | 1673 |
cell.statistic = Statistic.objects.get(slug='two-series') |
1689 | 1674 |
cell.save() |
1690 |
resp = app.get('/')
|
|
1675 |
resp = app.get(location)
|
|
1691 | 1676 |
assert '21' in resp.text |
1692 | 1677 |
assert resp.text.count('Total') == 2 |
1693 | 1678 | |
1694 | 1679 |
cell.statistic = Statistic.objects.get(slug='no-data') |
1695 | 1680 |
cell.save() |
1696 |
resp = app.get('/')
|
|
1681 |
resp = app.get(location)
|
|
1697 | 1682 |
assert resp.text.count('Total') == 0 |
1698 | 1683 | |
1699 | 1684 | |
... | ... | |
2009 | 1994 | |
2010 | 1995 | |
2011 | 1996 |
@with_httmock(new_api_mock) |
2012 |
def test_chartng_cell_new_api_filter_params_page_variables_table(new_api_statistics, nocache): |
|
1997 |
def test_chartng_cell_new_api_filter_params_page_variables_table(new_api_statistics, app, nocache):
|
|
2013 | 1998 |
Page.objects.create(title='One', slug='index') |
2014 | 1999 |
page = Page.objects.create( |
2015 | 2000 |
title='One', |
... | ... | |
2026 | 2011 |
cell.filter_params = {'service': 'chrono', 'ou': 'variable:foo'} |
2027 | 2012 |
cell.save() |
2028 | 2013 | |
2029 |
cell.render({**page.extra_variables, 'synchronous': True}) |
|
2014 |
location = '/api/dataviz/graph/%s/' % cell.pk |
|
2015 |
app.get(location) |
|
2030 | 2016 |
request = new_api_mock.call['requests'][0] |
2031 | 2017 |
assert 'service=chrono' in request.url |
2032 | 2018 |
assert 'ou=bar' in request.url |
... | ... | |
2035 | 2021 |
cell.filter_params = {'service': 'chrono', 'ou': 'variable:unknown'} |
2036 | 2022 |
cell.save() |
2037 | 2023 | |
2038 |
content = cell.render({'synchronous': True})
|
|
2024 |
resp = app.get(location)
|
|
2039 | 2025 |
assert len(new_api_mock.call['requests']) == 1 |
2040 |
assert 'Page variable not found.' in content
|
|
2026 |
assert 'Page variable not found.' in resp.text
|
|
2041 | 2027 | |
2042 | 2028 |
# variable with invalid syntax |
2043 | 2029 |
cell.filter_params = {'service': 'chrono', 'ou': 'variable:syntax_error'} |
2044 | 2030 |
cell.save() |
2045 | 2031 | |
2046 |
content = cell.render({**page.extra_variables, 'synchronous': True})
|
|
2032 |
resp = app.get(location)
|
|
2047 | 2033 |
assert len(new_api_mock.call['requests']) == 1 |
2048 |
assert 'Syntax error in page variable.' in content
|
|
2034 |
assert 'Syntax error in page variable.' in resp.text
|
|
2049 | 2035 | |
2050 | 2036 |
# variable with missing context |
2051 | 2037 |
cell.filter_params = {'service': 'chrono', 'ou': 'variable:subslug_dependant'} |
2052 | 2038 |
cell.save() |
2053 | 2039 | |
2054 |
content = cell.render({**page.extra_variables, 'synchronous': True})
|
|
2040 |
resp = app.get(location)
|
|
2055 | 2041 |
assert len(new_api_mock.call['requests']) == 1 |
2056 |
assert 'Cannot evaluate page variable.' in content
|
|
2042 |
assert 'Cannot evaluate page variable.' in resp.text
|
|
2057 | 2043 | |
2058 | 2044 | |
2059 | 2045 |
def test_dataviz_check_validity(nocache): |
2060 |
- |