Projet

Général

Profil

0004-dataviz-handle-new-api-to-get-statistics-from-elsewh.patch

Valentin Deniaud, 01 décembre 2020 17:01

Télécharger (14,6 ko)

Voir les différences:

Subject: [PATCH 4/4] dataviz: handle new api to get statistics from elsewhere
 (#48865)

 combo/apps/dataviz/__init__.py |  42 +++++----
 combo/apps/dataviz/forms.py    |   5 +-
 combo/apps/dataviz/models.py   | 152 +++++++++++++++++++++------------
 combo/settings.py              |   3 +
 4 files changed, 130 insertions(+), 72 deletions(-)
combo/apps/dataviz/__init__.py
41 41
        if not settings.KNOWN_SERVICES:
42 42
            return
43 43

  
44
        statistics_providers = settings.STATISTICS_PROVIDERS + ['bijoe']
44 45
        start_update = timezone.now()
45
        bijoe_sites = settings.KNOWN_SERVICES.get('bijoe').items()
46
        for site_key, site_dict in bijoe_sites:
47
            result = requests.get('/visualization/json/',
48
                    remote_service=site_dict, without_user=True,
49
                    headers={'accept': 'application/json'}).json()
50
            for stat in result:
51
                Statistic.objects.update_or_create(
52
                    slug=stat['slug'],
53
                    site_slug=site_key,
54
                    service_slug='bijoe',
55
                    defaults={
56
                        'label': stat['name'],
57
                        'url': stat['data-url'],
58
                        'site_title': site_dict.get('title', ''),
59
                        'available': True,
60
                    }
61
                )
46
        for service in statistics_providers:
47
            sites = settings.KNOWN_SERVICES.get(service, {}).items()
48
            for site_key, site_dict in sites:
49
                if service == 'bijoe':
50
                    result = requests.get('/visualization/json/',
51
                            remote_service=site_dict, without_user=True,
52
                            headers={'accept': 'application/json'}).json()
53
                else:
54
                    result = requests.get('/api/statistics/',
55
                            remote_service=site_dict, without_user=True,
56
                            headers={'accept': 'application/json'}).json()['data']
57

  
58
                for stat in result:
59
                    Statistic.objects.update_or_create(
60
                        slug=stat.get('slug') or stat['id'],
61
                        site_slug=site_key,
62
                        service_slug=service,
63
                        defaults={
64
                            'label': stat['name'],
65
                            'url': stat.get('data-url') or stat['url'],
66
                            'site_title': site_dict.get('title', ''),
67
                            'available': True,
68
                        }
69
                    )
62 70
        Statistic.objects.filter(last_update__lt=start_update).update(available=False)
63 71

  
64 72

  
combo/apps/dataviz/forms.py
44 44
class ChartNgForm(forms.ModelForm):
45 45
    class Meta:
46 46
        model = ChartNgCell
47
        fields = ('title', 'statistic', 'chart_type', 'height', 'sort_order',
47
        fields = ('title', 'statistic', 'time_interval', 'chart_type', 'height', 'sort_order',
48 48
                  'hide_null_values')
49 49

  
50 50
    def __init__(self, *args, **kwargs):
......
53 53
        if self.instance.statistic:
54 54
            q_filters |= Q(pk=self.instance.statistic.pk)
55 55
        self.fields['statistic'].queryset = self.fields['statistic'].queryset.filter(q_filters)
56

  
57
        if self.instance.statistic and self.instance.statistic.service_slug == 'bijoe':
58
            self.fields.pop('time_interval')
combo/apps/dataviz/models.py
136 136

  
137 137
@register_cell_class
138 138
class ChartNgCell(CellBase):
139
    DAY = 'day'
140
    MONTH = 'month'
141
    YEAR = 'year'
142
    TIME_INTERVAL_CHOICES = {
143
        (DAY, _('Day')),
144
        (MONTH, _('Month')),
145
        (YEAR, _('Year')),
146
    }
147

  
139 148
    statistic = models.ForeignKey(
140 149
        verbose_name=_('Data'), to=Statistic, blank=False, null=True, on_delete=models.SET_NULL, related_name='cells'
141 150
    )
151
    time_interval = models.CharField(
152
        _('Time interval (if applicable)'), max_length=16, choices=TIME_INTERVAL_CHOICES, default=MONTH
153
    )
142 154
    title = models.CharField(_('Title'), max_length=150, blank=True)
143 155
    chart_type = models.CharField(_('Chart Type'), max_length=20, default='bar',
144 156
            choices=(
......
177 189

  
178 190
    @classmethod
179 191
    def is_enabled(self):
180
        return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe')
192
        return settings.KNOWN_SERVICES.get('bijoe') or settings.STATISTICS_PROVIDERS
181 193

  
182 194
    def get_default_form_class(self):
183 195
        from .forms import ChartNgForm
......
202 214
            else:
203 215
                ctx['table'] = chart.render_table(
204 216
                    transpose=bool(chart.axis_count == 2),
205
                    total=chart.compute_sum,
217
                    total=getattr(chart, 'compute_sum', True),
206 218
                )
207 219
                ctx['table'] = ctx['table'].replace('<table>', '<table class="main">')
208 220
        return ctx
209 221

  
210 222
    def get_chart(self, width=None, height=None, raise_if_not_cached=False):
223
        params = {'time_interval': self.time_interval}
211 224
        response = requests.get(
212 225
                self.statistic.url,
226
                params=params,
213 227
                cache_duration=300,
228
                remote_service='auto',
229
                without_user=True,
214 230
                raise_if_not_cached=raise_if_not_cached)
215 231
        response.raise_for_status()
216 232
        response = response.json()
......
229 245
            'table': pygal.Bar,
230 246
            }[self.chart_type](config=pygal.Config(style=copy.copy(style)))
231 247

  
248
        chart.config.margin = 0
249
        if width:
250
            chart.config.width = width
251
        if height:
252
            chart.config.height = height
253
        if width or height:
254
            chart.config.explicit_size = True
255
        chart.config.js = [os.path.join(settings.STATIC_URL, 'js/pygal-tooltips.js')]
256

  
257
        chart.truncate_legend = 30
258
        # matplotlib tab10 palette
259
        chart.config.style.colors = (
260
                '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
261
                '#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
262
                '#bcbd22', '#17becf')
263

  
264
        if self.statistic.service_slug == 'bijoe':
265
            x_labels = response['axis'].get('x_labels') or []
266
            chart.show_legend = bool(len(response['axis']) > 1)
267
        else:
268
            x_labels = response['data']['x_labels']
269
            chart.show_legend = bool(len(response['data']['series']) > 1)
270

  
271
        chart.x_labels = x_labels
272

  
273
        if self.chart_type == 'dot':
274
            chart.show_legend = False
275
            # use a single colour for dots
276
            chart.config.style.colors = ('#1f77b4',) * len(x_labels)
277

  
278
        if self.chart_type != 'pie':
279
            if chart.config.width and chart.config.width < 500:
280
                chart.legend_at_bottom = True
281
                if self.chart_type == 'horizontal-bar':
282
                    # truncate labels
283
                    chart.x_labels = [pygal.util.truncate(x, 15) for x in chart.x_labels]
284
        else:
285
            chart.show_legend = True
286
            if chart.config.width and chart.config.width < 500:
287
                chart.truncate_legend = 15
288

  
289
        if self.statistic.service_slug == 'bijoe':
290
            return self.add_bijoe_data_to_chart(chart, response)
291
        else:
292
            return self.add_data_to_chart(chart, response['data'])
293

  
294
    def add_data_to_chart(self, chart, data):
295
        chart.axis_count = 1 if len(data['series']) == 1 else 2
296
        if self.chart_type == 'table' and chart.axis_count == 1:
297
            self.add_total_to_line_table(chart, data['series'][0]['data'])
298

  
299
        for serie in data['series']:
300
            chart.add(serie['label'], serie['data'])
301

  
302
        return chart
303

  
304
    def add_bijoe_data_to_chart(self, chart, response):
232 305
        # normalize axis to have a fake axis when there are no dimensions and
233 306
        # always a x axis when there is a single dimension.
234 307
        data = response['data']
235 308
        loop_labels = response['axis'].get('loop') or []
236
        x_labels = response['axis'].get('x_labels') or []
237 309
        y_labels = response['axis'].get('y_labels') or []
238 310
        if loop_labels:
239
            if x_labels and y_labels:
311
            if chart.x_labels and y_labels:
240 312
                # no support for three dimensions
241 313
                raise UnsupportedDataSet()
242 314
            if not y_labels:
243 315
                y_labels = loop_labels
244 316
            else:
245
                x_labels, y_labels = y_labels, loop_labels
246
            if len(y_labels) != len(data) or not all([len(x) == len(x_labels) for x in data]):
317
                chart.x_labels, y_labels = y_labels, loop_labels
318
            if len(y_labels) != len(data) or not all([len(x) == len(chart.x_labels) for x in data]):
247 319
                # varying dimensions
248 320
                raise UnsupportedDataSet()
249
        if not x_labels and not y_labels:  # unidata
250
            x_labels = ['']
321
        if not chart.x_labels and not y_labels:  # unidata
322
            chart.x_labels = ['']
251 323
            y_labels = ['']
252 324
            data = [data]
253 325
            chart.axis_count = 0
254
        elif not x_labels:
255
            x_labels = y_labels
326
        elif not chart.x_labels:
327
            chart.x_labels = y_labels
256 328
            y_labels = ['']
257 329
            chart.axis_count = 1
258 330
        elif not y_labels:
......
264 336
        # hide/sort values
265 337
        if chart.axis_count == 1 and (self.sort_order != 'none' or self.hide_null_values):
266 338
            if self.sort_order == 'alpha':
267
                tmp_items = sorted(zip(x_labels, data), key=lambda x: x[0])
339
                tmp_items = sorted(zip(chart.x_labels, data), key=lambda x: x[0])
268 340
            elif self.sort_order == 'asc':
269
                tmp_items = sorted(zip(x_labels, data), key=lambda x: (x[1] or 0))
341
                tmp_items = sorted(zip(chart.x_labels, data), key=lambda x: (x[1] or 0))
270 342
            elif self.sort_order == 'desc':
271
                tmp_items = sorted(zip(x_labels, data), key=lambda x: (x[1] or 0), reverse=True)
343
                tmp_items = sorted(zip(chart.x_labels, data), key=lambda x: (x[1] or 0), reverse=True)
272 344
            else:
273
                tmp_items = zip(x_labels, data)
345
                tmp_items = zip(chart.x_labels, data)
274 346
            tmp_x_labels = []
275 347
            tmp_data = []
276 348
            for label, value in tmp_items:
......
278 350
                    continue
279 351
                tmp_x_labels.append(label)
280 352
                tmp_data.append(value)
281
            x_labels = tmp_x_labels
353
            chart.x_labels = tmp_x_labels
282 354
            data = tmp_data
283 355

  
284
        chart.config.margin = 0
285
        if width:
286
            chart.config.width = width
287
        if height:
288
            chart.config.height = height
289
        if width or height:
290
            chart.config.explicit_size = True
291
        chart.config.js = [os.path.join(settings.STATIC_URL, 'js/pygal-tooltips.js')]
292
        chart.x_labels = x_labels
293

  
294
        chart.show_legend = bool(len(response['axis']) > 1)
295
        chart.truncate_legend = 30
296
        # matplotlib tab10 palette
297
        chart.config.style.colors = (
298
                '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
299
                '#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
300
                '#bcbd22', '#17becf')
301

  
302
        if self.chart_type == 'dot':
303
            chart.show_legend = False
304
            # use a single colour for dots
305
            chart.config.style.colors = ('#1f77b4',) * len(x_labels)
306

  
307 356
        chart.compute_sum = bool(response.get('measure') == 'integer')
308
        if chart.compute_sum and self.chart_type == 'table':
309
            if chart.axis_count < 2:  # workaround pygal
310
                chart.compute_sum = False
311
                if chart.axis_count == 1:
312
                    data.append(sum(data))
313
                    x_labels.append(gettext('Total'))
357
        if chart.compute_sum and self.chart_type == 'table' and chart.axis_count < 2:  # workaround pygal
358
            self.add_total_to_line_table(chart, data)
314 359

  
315 360
        if self.chart_type != 'pie':
316 361
            for i, serie_label in enumerate(y_labels):
317 362
                if chart.axis_count < 2:
318 363
                    values = data
319 364
                else:
320
                    values = [data[i][j] for j in range(len(x_labels))]
365
                    values = [data[i][j] for j in range(len(chart.x_labels))]
321 366
                chart.add(serie_label, values)
322
            if width and width < 500:
323
                chart.legend_at_bottom = True
324
                if self.chart_type == 'horizontal-bar':
325
                    # truncate labels
326
                    chart.x_labels = [pygal.util.truncate(x, 15) for x in chart.x_labels]
327 367
        else:
328 368
            # pie, create a serie by data, to get different colours
329 369
            values = data
330
            for label, value in zip(x_labels, values):
370
            for label, value in zip(chart.x_labels, values):
331 371
                if not value:
332 372
                    continue
333 373
                chart.add(label, value)
334
            chart.show_legend = True
335
            if width and width < 500:
336
                chart.truncate_legend = 15
337 374

  
338 375
        if response.get('unit') == 'seconds' or response.get('measure') == 'duration':
339 376
            def format_duration(value):
......
363 400
            chart.config.value_formatter = percent_formatter
364 401

  
365 402
        return chart
403

  
404
    @staticmethod
405
    def add_total_to_line_table(chart, data):
406
        chart.compute_sum = False
407
        if chart.axis_count == 1:
408
            data.append(sum(data))
409
            chart.x_labels.append(gettext('Total'))
combo/settings.py
348 348
# known services
349 349
KNOWN_SERVICES = {}
350 350

  
351
# known services exposing statistics
352
STATISTICS_PROVIDERS = []
353

  
351 354
# PWA Settings
352 355
PWA_VAPID_PUBLIK_KEY = None
353 356
PWA_VAPID_PRIVATE_KEY = None
354
-