0002-dataviz-handle-api-filters-49175.patch
combo/apps/dataviz/__init__.py | ||
---|---|---|
64 | 64 |
'label': stat['name'], |
65 | 65 |
'url': stat.get('data-url') or stat['url'], |
66 | 66 |
'site_title': site_dict.get('title', ''), |
67 |
'filters': stat.get('filters', []), |
|
67 | 68 |
'available': True, |
68 | 69 |
} |
69 | 70 |
) |
combo/apps/dataviz/forms.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from collections import OrderedDict |
|
18 | ||
17 | 19 |
from django import forms |
18 | 20 |
from django.conf import settings |
19 | 21 |
from django.db.models import Q |
... | ... | |
42 | 44 | |
43 | 45 | |
44 | 46 |
class ChartNgForm(forms.ModelForm): |
47 |
blank_choice = ('', '---------') |
|
48 | ||
45 | 49 |
class Meta: |
46 | 50 |
model = ChartNgCell |
47 | 51 |
fields = ('title', 'statistic', 'chart_type', 'height', 'sort_order', |
... | ... | |
49 | 53 | |
50 | 54 |
def __init__(self, *args, **kwargs): |
51 | 55 |
super().__init__(*args, **kwargs) |
52 |
q_filters = Q(available=True) |
|
53 |
if self.instance.statistic: |
|
54 |
q_filters |= Q(pk=self.instance.statistic.pk) |
|
55 |
self.fields['statistic'].queryset = self.fields['statistic'].queryset.filter(q_filters) |
|
56 |
stat_field = self.fields['statistic'] |
|
57 |
if not self.instance.statistic: |
|
58 |
stat_field.queryset = stat_field.queryset.filter(available=True) |
|
59 |
return |
|
60 | ||
61 |
# display current statistic in choices even if unavailable |
|
62 |
stat_field.queryset = stat_field.queryset.filter(Q(available=True) | Q(pk=self.instance.statistic.pk)) |
|
63 | ||
64 |
field_ids = list(self._meta.fields) |
|
65 |
field_insert_index = field_ids.index('statistic') + 1 |
|
66 |
for filter_ in reversed(self.instance.statistic.filters): |
|
67 |
filter_id = filter_['id'] |
|
68 |
choices = [(option['id'], option['label']) for option in filter_['options']] |
|
69 |
initial = self.instance.filter_params.get(filter_id) or filter_.get('default') |
|
70 | ||
71 |
required = filter_.get('required', False) |
|
72 |
if not required: |
|
73 |
choices.insert(0, self.blank_choice) |
|
74 | ||
75 |
self.fields[filter_id] = forms.ChoiceField( |
|
76 |
label=filter_['label'], choices=choices, required=required, initial=initial |
|
77 |
) |
|
78 |
field_ids.insert(field_insert_index, filter_id) |
|
79 | ||
80 |
# reorder so that filter fields appear after 'statistic' field |
|
81 |
self.fields = OrderedDict((field_id, self.fields[field_id]) for field_id in field_ids) |
|
82 | ||
83 |
def save(self, *args, **kwargs): |
|
84 |
if 'statistic' in self.changed_data: |
|
85 |
self.instance.filter_params.clear() |
|
86 |
else: |
|
87 |
for filter_ in self.instance.statistic.filters: |
|
88 |
field = filter_['id'] |
|
89 |
value = self.cleaned_data.get(field) |
|
90 |
if value: |
|
91 |
self.instance.filter_params[field] = value |
|
92 |
else: |
|
93 |
self.instance.filter_params.pop(field, None) |
|
94 |
return super().save(*args, **kwargs) |
combo/apps/dataviz/migrations/0015_auto_20201202_1424.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.29 on 2020-12-02 13:24 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations |
|
6 |
import jsonfield.fields |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
('dataviz', '0014_auto_20201130_1534'), |
|
13 |
] |
|
14 | ||
15 |
operations = [ |
|
16 |
migrations.AddField( |
|
17 |
model_name='chartngcell', |
|
18 |
name='filter_params', |
|
19 |
field=jsonfield.fields.JSONField(default=dict), |
|
20 |
), |
|
21 |
migrations.AddField( |
|
22 |
model_name='statistic', |
|
23 |
name='filters', |
|
24 |
field=jsonfield.fields.JSONField(default=list), |
|
25 |
), |
|
26 |
] |
combo/apps/dataviz/models.py | ||
---|---|---|
115 | 115 |
service_slug = models.SlugField(_('Service slug'), max_length=256) |
116 | 116 |
site_title = models.CharField(_('Site title'), max_length=256) |
117 | 117 |
url = models.URLField(_('Data URL')) |
118 |
filters = JSONField(default=list) |
|
118 | 119 |
available = models.BooleanField(_('Available data'), default=True) |
119 | 120 |
last_update = models.DateTimeField(_('Last update'), null=True, auto_now=True) |
120 | 121 | |
... | ... | |
139 | 140 |
statistic = models.ForeignKey( |
140 | 141 |
verbose_name=_('Data'), to=Statistic, blank=False, null=True, on_delete=models.SET_NULL, related_name='cells' |
141 | 142 |
) |
143 |
filter_params = JSONField(default=dict) |
|
142 | 144 |
title = models.CharField(_('Title'), max_length=150, blank=True) |
143 | 145 |
chart_type = models.CharField(_('Chart Type'), max_length=20, default='bar', |
144 | 146 |
choices=( |
... | ... | |
213 | 215 |
def get_chart(self, width=None, height=None, raise_if_not_cached=False): |
214 | 216 |
response = requests.get( |
215 | 217 |
self.statistic.url, |
218 |
params=self.filter_params, |
|
216 | 219 |
cache_duration=300, |
217 | 220 |
remote_service='auto', |
218 | 221 |
without_user=True, |
tests/test_dataviz.py | ||
---|---|---|
3 | 3 |
import mock |
4 | 4 |
import pytest |
5 | 5 |
from datetime import timedelta |
6 |
from httmock import HTTMock, with_httmock |
|
6 |
from httmock import HTTMock, with_httmock, remember_called
|
|
7 | 7 |
from requests.exceptions import HTTPError |
8 | 8 | |
9 | 9 |
from django.apps import apps |
... | ... | |
225 | 225 |
'url': 'https://authentic.example.com/api/statistics/one-serie/', |
226 | 226 |
'name': 'One serie stat', |
227 | 227 |
'id': 'one-serie', |
228 |
"filters": [ |
|
229 |
{ |
|
230 |
"default": "month", |
|
231 |
"id": "time_interval", |
|
232 |
"label": "Time interval", |
|
233 |
"options": [ |
|
234 |
{"id": "day", "label": "Day"}, |
|
235 |
{"id": "month", "label": "Month"}, |
|
236 |
{"id": "year", "label": "Year"}, |
|
237 |
], |
|
238 |
"required": True, |
|
239 |
}, |
|
240 |
{ |
|
241 |
"id": "ou", |
|
242 |
"label": "Organizational Unit", |
|
243 |
"options": [ |
|
244 |
{"id": "default", "label": "Default OU"}, |
|
245 |
{"id": "other", "label": "Other OU"}, |
|
246 |
], |
|
247 |
}, |
|
248 |
], |
|
228 | 249 |
}, |
229 | 250 |
{ |
230 | 251 |
'url': 'https://authentic.example.com/api/statistics/two-series/', |
... | ... | |
245 | 266 |
} |
246 | 267 | |
247 | 268 | |
269 |
@remember_called |
|
248 | 270 |
def new_api_mock(url, request): |
249 | 271 |
if url.path == '/visualization/json/': # nothing from bijoe |
250 | 272 |
return {'content': b'{}', 'request': request, 'status_code': 200} |
... | ... | |
905 | 927 | |
906 | 928 |
app = login(app) |
907 | 929 |
resp = app.get('/manage/pages/%s/' % page.id) |
908 |
statistics_field = resp.form['cdataviz_chartngcell-%s-statistic' % cell.id] |
|
930 |
field_prefix = 'cdataviz_chartngcell-%s-' % cell.id |
|
931 |
statistics_field = resp.form[field_prefix + 'statistic'] |
|
909 | 932 |
assert len(statistics_field.options) == 5 |
910 | 933 |
assert statistics_field.value == str(cell.statistic.pk) |
911 | 934 |
assert statistics_field.options[3][2] == 'Connection: One serie stat' |
912 | 935 | |
936 |
time_interval_field = resp.form[field_prefix + 'time_interval'] |
|
937 |
assert time_interval_field.pos == statistics_field.pos + 1 |
|
938 |
assert time_interval_field.value == 'month' |
|
939 |
assert time_interval_field.options == [ |
|
940 |
('day', False, 'Day'), |
|
941 |
('month', True, 'Month'), |
|
942 |
('year', False, 'Year'), |
|
943 |
] |
|
944 | ||
945 |
ou_field = resp.form[field_prefix + 'ou'] |
|
946 |
assert ou_field.pos == statistics_field.pos + 2 |
|
947 |
assert ou_field.value == '' |
|
948 |
assert ou_field.options == [ |
|
949 |
('', True, '---------'), |
|
950 |
('default', False, 'Default OU'), |
|
951 |
('other', False, 'Other OU'), |
|
952 |
] |
|
953 |
resp.form[field_prefix + 'ou'] = 'default' |
|
954 | ||
955 |
resp = resp.form.submit().follow() |
|
956 |
assert resp.form[field_prefix + 'ou'].value == 'default' |
|
957 |
cell.refresh_from_db() |
|
958 |
assert cell.filter_params == {'ou': 'default', 'time_interval': 'month'} |
|
959 |
resp.form[field_prefix + 'ou'] = '' |
|
960 | ||
961 |
resp = resp.form.submit().follow() |
|
962 |
assert resp.form[field_prefix + 'ou'].value == '' |
|
963 |
cell.refresh_from_db() |
|
964 |
assert cell.filter_params == {'time_interval': 'month'} |
|
965 | ||
966 |
no_filters_stat = Statistic.objects.get(slug='two-series') |
|
967 |
resp.form[field_prefix + 'statistic'] = no_filters_stat.pk |
|
968 |
resp = resp.form.submit().follow() |
|
969 |
assert resp.form[field_prefix + 'statistic'].value == str(no_filters_stat.pk) |
|
970 |
assert field_prefix + 'time_interval' not in resp.form.fields |
|
971 |
assert field_prefix + 'ou' not in resp.form.fields |
|
972 |
cell.refresh_from_db() |
|
973 |
assert cell.filter_params == {} |
|
974 | ||
913 | 975 | |
914 | 976 |
@with_httmock(bijoe_mock) |
915 | 977 |
def test_table_cell(app, admin_user, statistics): |
... | ... | |
1072 | 1134 |
assert statistic.site_title == 'Connection' |
1073 | 1135 |
assert statistic.url == 'https://authentic.example.com/api/statistics/one-serie/' |
1074 | 1136 |
assert statistic.available |
1137 | ||
1138 | ||
1139 |
@with_httmock(new_api_mock) |
|
1140 |
def test_chartng_cell_new_api_filter_params(new_api_statistics, nocache): |
|
1141 |
page = Page.objects.create(title='One', slug='index') |
|
1142 |
cell = ChartNgCell(page=page, order=1, placeholder='content') |
|
1143 |
cell.statistic = Statistic.objects.get(slug='one-serie') |
|
1144 |
cell.save() |
|
1145 | ||
1146 |
chart = cell.get_chart() |
|
1147 |
request = new_api_mock.call['requests'][0] |
|
1148 |
assert 'time_interval' not in request.url |
|
1149 |
assert 'ou' not in request.url |
|
1150 | ||
1151 |
cell.filter_params = {'time_interval': 'day', 'ou': 'default'} |
|
1152 |
cell.save() |
|
1153 |
chart = cell.get_chart() |
|
1154 |
request = new_api_mock.call['requests'][1] |
|
1155 |
assert 'time_interval=day' in request.url |
|
1156 |
assert 'ou=default' in request.url |
|
1075 |
- |