From 96c1692db7b54b6f3cc8dac3c0d003956211ee6d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sun, 1 Sep 2019 10:41:47 +0200
Subject: [PATCH] dataviz: add support for loop, warn if there are three
 dimensions (#35698)

---
 combo/apps/dataviz/models.py |  29 ++++++++--
 combo/apps/dataviz/views.py  |  26 +++++++--
 tests/test_dataviz.py        | 104 ++++++++++++++++++++++++++++++++++-
 3 files changed, 146 insertions(+), 13 deletions(-)

diff --git a/combo/apps/dataviz/models.py b/combo/apps/dataviz/models.py
index 2fcce5b2..4f8dcdc7 100644
--- a/combo/apps/dataviz/models.py
+++ b/combo/apps/dataviz/models.py
@@ -30,6 +30,10 @@ from combo.data.library import register_cell_class
 from combo.utils import get_templated_url, requests
 
 
+class UnsupportedDataSet(Exception):
+    pass
+
+
 @register_cell_class
 class Gauge(CellBase):
     title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
@@ -157,11 +161,15 @@ class ChartNgCell(CellBase):
     def get_cell_extra_context(self, context):
         ctx = super(ChartNgCell, self).get_cell_extra_context(context)
         if self.chart_type == 'table':
-            chart = self.get_chart(raise_if_not_cached=not(context.get('synchronous')))
-            ctx['table'] = chart.render_table(
-                transpose=bool(chart.axis_count == 2),
-            )
-            ctx['table'] = ctx['table'].replace('<table>', '<table class="main">')
+            try:
+                chart = self.get_chart(raise_if_not_cached=not(context.get('synchronous')))
+            except UnsupportedDataSet:
+                ctx['table'] = '<p>%s</p>' % _('Unsupported dataset.')
+            else:
+                ctx['table'] = chart.render_table(
+                    transpose=bool(chart.axis_count == 2),
+                )
+                ctx['table'] = ctx['table'].replace('<table>', '<table class="main">')
         return ctx
 
     def get_chart(self, width=None, height=None, raise_if_not_cached=False):
@@ -186,9 +194,18 @@ class ChartNgCell(CellBase):
 
         # normalize axis to have a fake axis when there are no dimensions and
         # always a x axis when there is a single dimension.
+        data = response['data']
+        loop_labels = response['axis'].get('loop') or []
         x_labels = response['axis'].get('x_labels') or []
         y_labels = response['axis'].get('y_labels') or []
-        data = response['data']
+        if loop_labels:
+            if x_labels and y_labels:
+                # no support for three dimensions
+                raise UnsupportedDataSet()
+            if not y_labels:
+                y_labels = loop_labels
+            else:
+                x_labels, y_labels = y_labels, loop_labels
         if not x_labels and not y_labels:  # unidata
             x_labels = ['']
             y_labels = ['']
diff --git a/combo/apps/dataviz/views.py b/combo/apps/dataviz/views.py
index 75c8dae1..54d28314 100644
--- a/combo/apps/dataviz/views.py
+++ b/combo/apps/dataviz/views.py
@@ -16,9 +16,10 @@
 
 from django.core.exceptions import PermissionDenied
 from django.http import HttpResponse
+from django.utils.translation import ugettext_lazy as _
 
 from combo.utils import get_templated_url, requests
-from .models import Gauge, ChartNgCell
+from .models import Gauge, ChartNgCell, UnsupportedDataSet
 
 
 def ajax_gauge_count(request, *args, **kwargs):
@@ -33,8 +34,21 @@ def dataviz_graph(request, *args, **kwargs):
         raise PermissionDenied()
     if not cell.is_visible(request.user):
         raise PermissionDenied()
-    chart = cell.get_chart(
-            width=int(request.GET.get('width', 0)) or None,
-            height=int(request.GET.get('height', 0)) or int(cell.height)
-    )
-    return HttpResponse(chart.render(), content_type='image/svg+xml')
+    try:
+        chart = cell.get_chart(
+                width=int(request.GET.get('width', 0)) or None,
+                height=int(request.GET.get('height', 0)) or int(cell.height)
+        )
+    except UnsupportedDataSet:
+        svg = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
+  viewBox="0 0 %(width)s 30" width="%(width)s" height="30">
+  <text
+       y="20"
+       x="10"
+       style="font-family: sans-serif; font-size: 16px; fill:#000000;">%(text)s</text>
+</svg>""" % {'width': request.GET.get('width', 200),
+             'text': _('Unsupported dataset.')}
+    else:
+        svg = chart.render()
+    return HttpResponse(svg, content_type='image/svg+xml')
diff --git a/tests/test_dataviz.py b/tests/test_dataviz.py
index 558505df..c58151f2 100644
--- a/tests/test_dataviz.py
+++ b/tests/test_dataviz.py
@@ -8,7 +8,7 @@ from django.contrib.auth.models import User, Group
 from django.test import override_settings
 
 from combo.data.models import Page
-from combo.apps.dataviz.models import Gauge, ChartNgCell
+from combo.apps.dataviz.models import Gauge, ChartNgCell, UnsupportedDataSet
 
 from .test_public import login, normal_user
 
@@ -70,6 +70,25 @@ VISUALIZATION_JSON = [
         'name': 'fourth visualization (no axis)',
         'slug': 'fourth',
     },
+    {
+        'data-url': 'https://bijoe.example.com/visualization/5/json/',
+        'path': 'https://bijoe.example.com/visualization/5/iframe/?signature=123',
+        'name': 'fifth visualization (loop/X)',
+        'slug': 'fifth',
+    },
+    {
+        'data-url': 'https://bijoe.example.com/visualization/6/json/',
+        'path': 'https://bijoe.example.com/visualization/6/iframe/?signature=123',
+        'name': 'sixth visualization (loop/Y)',
+        'slug': 'sixth',
+    },
+    {
+        'data-url': 'https://bijoe.example.com/visualization/7/json/',
+        'path': 'https://bijoe.example.com/visualization/7/iframe/?signature=123',
+        'name': 'seventh visualization (loop/X/Y)',
+        'slug': 'seventh',
+    },
+
 ]
 
 
@@ -114,6 +133,48 @@ def bijoe_mock(url, request):
             'axis': {}
         }
         return {'content': json.dumps(response), 'request': request, 'status_code': 200}
+    if url.path == '/visualization/5/json/':
+        response = {
+            'format': '1',
+            'data': [
+                [222, 134, 53],
+                [122, 114, 33],
+            ],
+            'axis': {
+                'x_labels': ['web', 'mail', 'email'],
+                'loop': ['foo', 'bar'],
+            }
+        }
+        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
+    if url.path == '/visualization/6/json/':
+        response = {
+            'format': '1',
+            'data': [
+                [222, 134, 53],
+                [122, 114, 33],
+            ],
+            'axis': {
+                'y_labels': ['web', 'mail', 'email'],
+                'loop': ['foo', 'bar'],
+            }
+        }
+        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
+    if url.path == '/visualization/7/json/':
+        response = {
+            'format': '1',
+            'data': [
+                [[222, 134, 53], [122, 114, 33]],
+                [[222, 134, 53], [122, 114, 33]],
+                [[222, 134, 53], [122, 114, 33]],
+                [[222, 134, 53], [122, 114, 33]],
+            ],
+            'axis': {
+                'x_labels': ['foo', 'bar'],
+                'y_labels': ['web', 'mail', 'email'],
+                'loop': ['a', 'b', 'c', 'd'],
+            }
+        }
+        return {'content': json.dumps(response), 'request': request, 'status_code': 200}
 
 
 def test_chartng_cell(app):
@@ -186,6 +247,33 @@ def test_chartng_cell(app):
             assert chart.x_labels == ['']
             assert chart.raw_series == [([222], {'title': ''})]
 
+            # loop/X
+            cell.data_reference = 'plop:fifth'
+            cell.save()
+            chart = cell.get_chart()
+            assert chart.x_labels == ['web', 'mail', 'email']
+            assert chart.raw_series == [
+                ([222, 134, 53], {'title': u'foo'}),
+                ([122, 114, 33], {'title': u'bar'}),
+            ]
+
+            # loop/Y
+            cell.data_reference = 'plop:sixth'
+            cell.save()
+            chart = cell.get_chart()
+            assert chart.x_labels == ['web', 'mail', 'email']
+            assert chart.raw_series == [
+                ([222, 134, 53], {'title': u'foo'}),
+                ([122, 114, 33], {'title': u'bar'}),
+            ]
+
+            # loop/X/Y
+            cell.data_reference = 'plop:seventh'
+            cell.save()
+            with pytest.raises(UnsupportedDataSet):
+                chart = cell.get_chart()
+
+
 def test_chartng_cell_view(app, normal_user):
     page = Page(title='One', slug='index')
     page.save()
@@ -230,6 +318,17 @@ def test_chartng_cell_view(app, normal_user):
             resp = app.get('/')
             assert '<td>222</td>' in resp.text
 
+            # unsupported dataset
+            cell.data_reference = 'plop:seventh'
+            cell.save()
+            resp = app.get('/')
+            assert 'Unsupported dataset' in resp.text
+
+            cell.chart_type = 'bar'
+            cell.save()
+            resp = app.get('/api/dataviz/graph/1/?width=400', status=200)
+            assert 'Unsupported dataset' in resp.text
+
 
 def test_chartng_cell_manager(app, admin_user):
     page = Page(title='One', slug='index')
@@ -247,7 +346,10 @@ def test_chartng_cell_manager(app, admin_user):
             resp = app.get('/manage/pages/%s/' % page.id)
             assert resp.form['cdataviz_chartngcell-%s-data_reference' % cell.id].options == [
                 (u'plop:example', True, u'example visualization (X)'),
+                (u'plop:fifth', False, u'fifth visualization (loop/X)'),
                 (u'plop:fourth', False, u'fourth visualization (no axis)'),
                 (u'plop:second', False, u'second visualization (Y)'),
+                (u'plop:seventh', False, u'seventh visualization (loop/X/Y)'),
+                (u'plop:sixth', False, u'sixth visualization (loop/Y)'),
                 (u'plop:third', False, u'third visualization (X/Y)'),
             ]
-- 
2.23.0

