0001-dataviz-reload-chart-filters-cell-to-reflect-subfilt.patch
combo/apps/dataviz/forms.py | ||
---|---|---|
19 | 19 | |
20 | 20 |
from django import forms |
21 | 21 |
from django.conf import settings |
22 |
from django.core.cache import cache |
|
22 | 23 |
from django.db import transaction |
23 | 24 |
from django.db.models import Q |
24 | 25 |
from django.db.models.fields import BLANK_CHOICE_DASH |
... | ... | |
275 | 276 | |
276 | 277 |
def __init__(self, *args, **kwargs): |
277 | 278 |
page = kwargs.pop('page') |
279 |
filters_cell_id = kwargs.pop('filters_cell_id', None) |
|
278 | 280 |
super().__init__(*args, **kwargs) |
279 | 281 | |
280 | 282 |
chart_cells = list(ChartNgCell.objects.filter(page=page, statistic__isnull=False).order_by('order')) |
... | ... | |
282 | 284 |
self.fields.clear() |
283 | 285 |
return |
284 | 286 | |
287 |
if filters_cell_id: |
|
288 |
for cell in chart_cells: |
|
289 |
cell.subfilters = cache.get(cell.get_cache_key(filters_cell_id), cell.subfilters) |
|
290 | ||
285 | 291 |
first_cell = chart_cells[0] |
286 | 292 |
for field in self._meta.fields: |
287 | 293 |
self.fields[field].initial = getattr(first_cell, field) |
combo/apps/dataviz/models.py | ||
---|---|---|
727 | 727 |
self.filter_params = {k: v for k, v in self.filter_params.items() if k in subfilter_ids} |
728 | 728 |
self.save() |
729 | 729 | |
730 |
def get_cache_key(self, filters_cell_id): |
|
731 |
return 'dataviz:%s:%s' % (filters_cell_id, self.pk) |
|
732 | ||
730 | 733 | |
731 | 734 |
@register_cell_class |
732 | 735 |
class ChartFiltersCell(CellBase): |
... | ... | |
745 | 748 |
from .forms import ChartFiltersForm |
746 | 749 | |
747 | 750 |
ctx = super().get_cell_extra_context(context) |
748 |
ctx['form'] = ChartFiltersForm(page=self.page) |
|
751 |
if 'filters_cell_id' in context['request'].GET: # detect refresh on submit |
|
752 |
ctx['form'] = ChartFiltersForm( |
|
753 |
data=context['request'].GET, |
|
754 |
page=self.page, |
|
755 |
filters_cell_id=context['request'].GET['filters_cell_id'], |
|
756 |
) |
|
757 |
else: |
|
758 |
ctx['form'] = ChartFiltersForm(page=self.page) |
|
759 | ||
749 | 760 |
return ctx |
combo/apps/dataviz/templates/combo/chart-filters.html | ||
---|---|---|
23 | 23 | |
24 | 24 |
<script> |
25 | 25 |
$(function () { |
26 |
if (!$('body').data('filters-cell-id')) { |
|
27 |
$('body').data('filters-cell-id', Math.random().toString(36).slice(2, 7)); |
|
28 | ||
29 |
var loaded_cell_count = 0; |
|
30 |
document.querySelectorAll('div.chartngcell embed').forEach(graph => { |
|
31 |
graph.addEventListener('load', function() { |
|
32 |
if (++loaded_cell_count == $('div.chartngcell embed').length) { |
|
33 |
combo_load_cell($('.chart-filters-cell')); |
|
34 |
loaded_cell_count = 0; |
|
35 |
} |
|
36 |
}); |
|
37 |
}); |
|
38 |
} |
|
39 | ||
26 | 40 |
start_field = $('#id_time_range_start'); |
27 | 41 |
end_field = $('#id_time_range_end'); |
28 | 42 |
$('#id_time_range').change(function() { |
... | ... | |
37 | 51 |
$('#chart-filters').submit(function(e) { |
38 | 52 |
e.preventDefault(); |
39 | 53 |
$(window).trigger('combo:refresh-graphs'); |
54 |
chart_cell = $(this).parents('.cell'); |
|
55 |
new_url = chart_cell.data('ajax-cell-url') + '?filters_cell_id=' + $('body').data('filters-cell-id') + '&' + $(this).serialize(); |
|
56 |
chart_cell.data('ajax-cell-url', new_url); |
|
40 | 57 |
}); |
41 | 58 |
}); |
42 | 59 |
</script> |
combo/apps/dataviz/templates/combo/chartngcell.html | ||
---|---|---|
5 | 5 |
<script> |
6 | 6 |
$(function() { |
7 | 7 |
var extra_context = $('#chart-{{cell.id}}').parents('.cell').data('extra-context'); |
8 |
var chart_filters_form = $('#chart-filters'); |
|
9 | 8 |
$(window).on('combo:refresh-graphs', function() { |
10 | 9 |
qs = []; |
11 |
if(chart_filters_form) |
|
12 |
qs.push(chart_filters_form.serialize()); |
|
10 |
if($('#chart-filters')) { |
|
11 |
qs.push($('#chart-filters').serialize()); |
|
12 |
qs.push('filters_cell_id=' + $('body').data('filters-cell-id')); |
|
13 |
} |
|
13 | 14 |
if(extra_context) |
14 | 15 |
qs.push('ctx=' + extra_context); |
15 | 16 |
$.ajax({ |
... | ... | |
30 | 31 |
$(function() { |
31 | 32 |
var last_width = 1; |
32 | 33 |
var extra_context = $('#chart-{{cell.id}}').parents('.cell').data('extra-context'); |
33 |
var chart_filters_form = $('#chart-filters'); |
|
34 | 34 |
$(window).on('load resize gadjo:sidepage-toggled combo:resize-graphs', function() { |
35 | 35 |
var chart_cell = $('#chart-{{cell.id}}').parent(); |
36 | 36 |
var new_width = Math.floor($(chart_cell).width()); |
37 | 37 |
var ratio = new_width / last_width; |
38 | 38 |
var qs = '?width=' + new_width |
39 |
if(chart_filters_form)
|
|
40 |
qs += '&' + chart_filters_form.serialize()
|
|
39 |
if($('#chart-filters'))
|
|
40 |
qs += '&' + $('#chart-filters').serialize() + '&filters_cell_id=' + $('body').data('filters-cell-id')
|
|
41 | 41 |
if(extra_context) |
42 | 42 |
qs += '&ctx=' + extra_context |
43 | 43 |
if (ratio > 1.2 || ratio < 0.8) { |
... | ... | |
47 | 47 |
}).trigger('combo:resize-graphs'); |
48 | 48 |
$(window).on('combo:refresh-graphs', function() { |
49 | 49 |
var qs = '?width=' + last_width |
50 |
if(chart_filters_form)
|
|
51 |
qs += '&' + chart_filters_form.serialize()
|
|
50 |
if($('#chart-filters'))
|
|
51 |
qs += '&' + $('#chart-filters').serialize() + '&filters_cell_id=' + $('body').data('filters-cell-id')
|
|
52 | 52 |
if(extra_context) |
53 | 53 |
qs += '&ctx=' + extra_context |
54 | 54 |
$('#chart-{{cell.id}}').attr('src', "{% url 'combo-dataviz-graph' cell=cell.id %}" + qs); |
combo/apps/dataviz/views.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
from django.core import signing |
18 |
from django.core.cache import cache |
|
18 | 19 |
from django.core.exceptions import PermissionDenied |
19 | 20 |
from django.http import Http404, HttpResponse, HttpResponseBadRequest |
20 | 21 |
from django.shortcuts import render |
... | ... | |
24 | 25 |
from django.views.generic import DetailView |
25 | 26 |
from requests.exceptions import HTTPError |
26 | 27 | |
27 |
from combo.utils import get_templated_url, requests |
|
28 |
from combo.utils import NothingInCacheException, get_templated_url, requests
|
|
28 | 29 | |
29 | 30 |
from .forms import ChartNgPartialForm |
30 | 31 |
from .models import ChartNgCell, Gauge, MissingVariable, UnsupportedDataSet |
... | ... | |
42 | 43 | |
43 | 44 |
def dispatch(self, request, *args, **kwargs): |
44 | 45 |
self.cell = self.get_object() |
46 |
self.filters_cell_id = request.GET.get('filters_cell_id') |
|
45 | 47 | |
46 | 48 |
if not self.cell.page.is_visible(request.user): |
47 | 49 |
raise PermissionDenied() |
... | ... | |
50 | 52 |
if not self.cell.statistic or not self.cell.statistic.url: |
51 | 53 |
raise Http404('misconfigured cell') |
52 | 54 | |
55 |
if self.filters_cell_id: |
|
56 |
self.cell.subfilters = cache.get( |
|
57 |
self.cell.get_cache_key(self.filters_cell_id), self.cell.subfilters |
|
58 |
) |
|
53 | 59 |
return super().dispatch(request, *args, **kwargs) |
54 | 60 | |
55 | 61 |
def get(self, request, *args, **kwargs): |
... | ... | |
84 | 90 |
else: |
85 | 91 |
return self.error(_('Unknown HTTP error: %s' % e)) |
86 | 92 | |
93 |
if self.filters_cell_id: |
|
94 |
self.update_subfilters_cache(form.instance) |
|
95 | ||
87 | 96 |
if self.cell.chart_type == 'table': |
88 | 97 |
if not chart.raw_series: |
89 | 98 |
return self.error(_('No data')) |
... | ... | |
107 | 116 |
} |
108 | 117 |
return render(self.request, 'combo/dataviz-error.svg', context=context, content_type='image/svg+xml') |
109 | 118 | |
119 |
def update_subfilters_cache(self, cell): |
|
120 |
try: |
|
121 |
data = cell.get_statistic_data(raise_if_not_cached=True) |
|
122 |
except NothingInCacheException: |
|
123 |
pass # should not happen |
|
124 |
else: |
|
125 |
cache.set( |
|
126 |
cell.get_cache_key(self.filters_cell_id), data.json()['data'].get('subfilters', []), 300 |
|
127 |
) |
|
128 | ||
110 | 129 | |
111 | 130 |
dataviz_graph = xframe_options_sameorigin(DatavizGraphView.as_view()) |
combo/public/static/js/combo.public.js | ||
---|---|---|
4 | 4 |
var extra_context = $elem.data('extra-context'); |
5 | 5 |
$.support.cors = true; /* IE9 */ |
6 | 6 |
var qs; |
7 |
if (window.location.search) { |
|
7 |
if (url.includes('?')) { |
|
8 |
qs = '&'; |
|
9 |
if (window.location.search) { |
|
10 |
qs += window.location.search.slice(1) + '&'; |
|
11 |
} |
|
12 |
} else if (window.location.search) { |
|
8 | 13 |
qs = window.location.search + '&'; |
9 | 14 |
} else { |
10 | 15 |
qs = '?'; |
tests/test_dataviz.py | ||
---|---|---|
2566 | 2566 | |
2567 | 2567 |
refresh_statistics_data(cell.pk) |
2568 | 2568 |
assert len(bijoe_mock.call['requests']) == 1 |
2569 | ||
2570 | ||
2571 |
@with_httmock(new_api_mock) |
|
2572 |
def test_chart_filters_cell_dynamic_subfilters(new_api_statistics, app, admin_user): |
|
2573 |
page = Page.objects.create(title='One', slug='index') |
|
2574 |
ChartFiltersCell.objects.create(page=page, order=1, placeholder='content') |
|
2575 |
cell = ChartNgCell.objects.create(page=page, order=2, placeholder='content') |
|
2576 |
cell.statistic = Statistic.objects.get(slug='with-subfilter') |
|
2577 |
cell.save() |
|
2578 | ||
2579 |
app = login(app) |
|
2580 |
resp = app.get('/') |
|
2581 |
assert 'form' in resp.form.fields |
|
2582 |
assert 'menu' not in resp.form.fields |
|
2583 | ||
2584 |
# ensure choice exist |
|
2585 |
resp.form['form'] = 'food-request' |
|
2586 | ||
2587 |
# simulate chart cell ajax refresh on form submission |
|
2588 |
app.get('/api/dataviz/graph/%s/' % cell.pk + '?form=food-request&filters_cell_id=xxx') |
|
2589 | ||
2590 |
# simulate filters cell ajax refresh after cell refresh |
|
2591 |
location = resp.pyquery('.chartfilterscell').attr('data-ajax-cell-url') |
|
2592 |
resp = app.get(location + '?form=food-request&filters_cell_id=xxx') |
|
2593 |
assert resp.form['form'].value == 'food-request' |
|
2594 |
assert 'menu' in resp.form.fields |
|
2595 | ||
2596 |
# check isolation between pages by modifying filters_cell_id |
|
2597 |
app.get('/api/dataviz/graph/%s/' % cell.pk + '?form=contact&filters_cell_id=yyy') |
|
2598 |
resp = app.get(location + '?form=food-request&filters_cell_id=xxx') |
|
2599 |
assert 'form' in resp.form.fields |
|
2600 |
assert 'menu' in resp.form.fields |
|
2569 |
- |