Projet

Général

Profil

0002-dataviz-handle-api-filters-49175.patch

Valentin Deniaud, 08 décembre 2020 14:25

Télécharger (11,2 ko)

Voir les différences:

Subject: [PATCH 2/3] dataviz: handle api filters (#49175)

 combo/apps/dataviz/__init__.py                |  1 +
 combo/apps/dataviz/forms.py                   | 47 +++++++++-
 .../migrations/0015_auto_20201202_1424.py     | 26 ++++++
 combo/apps/dataviz/models.py                  |  3 +
 tests/test_dataviz.py                         | 86 ++++++++++++++++++-
 5 files changed, 157 insertions(+), 6 deletions(-)
 create mode 100644 combo/apps/dataviz/migrations/0015_auto_20201202_1424.py
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
-