Projet

Général

Profil

0001-dataviz-remove-legacy-cubes-code-12743.patch

Frédéric Péters, 19 mars 2018 14:38

Télécharger (19,7 ko)

Voir les différences:

Subject: [PATCH] dataviz: remove legacy cubes code (#12743)

 combo/apps/dataviz/__init__.py                     |   9 --
 combo/apps/dataviz/forms.py                        |  82 +----------
 combo/apps/dataviz/models.py                       | 162 +--------------------
 .../dataviz/templates/combo/cubes-barchart.html    |  15 --
 .../apps/dataviz/templates/combo/cubes-table.html  |  23 ---
 combo/apps/dataviz/utils.py                        | 152 -------------------
 6 files changed, 2 insertions(+), 441 deletions(-)
 delete mode 100644 combo/apps/dataviz/templates/combo/cubes-barchart.html
 delete mode 100644 combo/apps/dataviz/templates/combo/cubes-table.html
 delete mode 100644 combo/apps/dataviz/utils.py
combo/apps/dataviz/__init__.py
30 30
        from . import urls
31 31
        return urls.urlpatterns
32 32

  
33
    def ready(self):
34
        @checks.register('settings')
35
        def check_settings(**kwargs):
36
            # Check if CUBES_URL is a proper URL string
37
            if (getattr(settings, 'CUBES_URL', None) is not None
38
                and (not isinstance(settings.CUBES_URL, str)
39
                     or not re.match(r'https?://', settings.CUBES_URL))):
40
                yield checks.Error('settings.CUBES_URL must be an HTTP URL')
41

  
42 33
default_app_config = 'combo.apps.dataviz.AppConfig'
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
import json
18

  
19
from django.utils.translation import ugettext_lazy as _
20 17
from django import forms
21 18
from django.conf import settings
22
from django.core.exceptions import ValidationError
23 19

  
24 20
from combo.utils import requests
25 21

  
26
from .models import BaseCubesChart, ChartCell
27
from .utils import get_cubes, get_cube, get_drilldown
22
from .models import ChartCell
28 23

  
29 24

  
30 25
class ChartForm(forms.ModelForm):
......
42 37
            available_charts.extend([(x['path'], x['name']) for x in result])
43 38
        available_charts.sort(key=lambda x: x[1])
44 39
        self.fields['url'].widget = forms.Select(choices=available_charts)
45

  
46

  
47
class CubesBarChartForm(forms.ModelForm):
48
    EMPTY = [(u'', _('None'))]
49

  
50
    class Meta:
51
        model = BaseCubesChart
52
        fields = ('title', 'url', 'cube', 'aggregate1', 'drilldown1', 'drilldown2',
53
                  'other_parameters')
54

  
55
    def __init__(self, *args, **kwargs):
56
        super(CubesBarChartForm, self).__init__(*args, **kwargs)
57
        for field in ('cube', 'aggregate1', 'drilldown1', 'drilldown2'):
58
            self.fields[field] = forms.ChoiceField(
59
                label=self.fields[field].label,
60
                initial=self.fields[field].initial,
61
                required=False,
62
                choices=self.EMPTY)
63
        if getattr(settings, 'CUBES_URL', None):
64
            cube_choices = self.get_cubes_choices()
65
            if cube_choices:
66
                self.fields['cube'].choices = cube_choices
67
            aggregate1_choices = self.get_aggregate_choices()
68
            # If there is no choice, hide the selector
69
            if not aggregate1_choices or len(aggregate1_choices) < 3:
70
                self.fields['aggregate1'].widget = forms.HiddenInput()
71
            if aggregate1_choices:
72
                self.fields['aggregate1'].choices = aggregate1_choices
73
            drilldown_choices = self.get_drilldown_choices()
74
            if drilldown_choices:
75
                self.fields['drilldown1'].choices = drilldown_choices
76
                self.fields['drilldown2'].choices = drilldown_choices
77

  
78
    def clean(self):
79
        cleaned_data = self.cleaned_data
80
        if getattr(settings, 'CUBES_URL', None):
81
            aggregate1_choices = self.get_aggregate_choices()
82
            # If there is no choice, autoselect
83
            if aggregate1_choices and len(aggregate1_choices) == 2:
84
                cleaned_data['aggregate1'] = aggregate1_choices[1][0]
85
        return cleaned_data
86

  
87
    def clean_other_parameters(self):
88
        other_parameters = self.cleaned_data['other_parameters']
89
        if other_parameters:
90
            try:
91
                decoded = json.loads(other_parameters)
92
                assert isinstance(decoded, dict)
93
                for key, value in decoded.iteritems():
94
                    assert isinstance(key, unicode)
95
                    assert isinstance(value, unicode)
96
            except (ValueError, AssertionError):
97
                raise ValidationError(_('Other parameter must be a JSON object containing only '
98
                                        'strings'))
99
        return other_parameters
100

  
101
    def get_cubes_choices(self):
102
        cubes = get_cubes()
103
        return self.EMPTY + [(cube['name'], cube.get('label')) for cube in cubes]
104

  
105
    def get_aggregate_choices(self):
106
        cube = self.data.get(self.add_prefix('cube')) if self.data else self.instance.cube
107
        if cube:
108
            cube = get_cube(cube)
109
            if cube:
110
                return self.EMPTY + [(ag['name'], ag['label']) for ag in cube.get('aggregates', [])]
111
        return []
112

  
113
    def get_drilldown_choices(self):
114
        cube = self.data.get(self.add_prefix('cube')) if self.data else self.instance.cube
115
        if cube:
116
            choices = get_drilldown(cube)
117
            if choices:
118
                return self.EMPTY + choices
119
        return []
combo/apps/dataviz/models.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
import json
18
from collections import OrderedDict
19

  
20 17
from django.core.urlresolvers import reverse
21 18
from django.db import models
22 19
from django.utils.translation import ugettext_lazy as _
......
24 21

  
25 22
from combo.data.models import CellBase
26 23
from combo.data.library import register_cell_class
27
from combo.utils import NothingInCacheException, get_templated_url
24
from combo.utils import get_templated_url
28 25

  
29
from . import utils
30 26

  
31 27
@register_cell_class
32 28
class Gauge(CellBase):
......
92 88
        context['title'] = self.title
93 89
        context['url'] = self.url
94 90
        return context
95

  
96
class BaseCubesChart(CellBase):
97
    title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
98
    url = models.URLField(_('URL'), max_length=150, blank=True, null=True)
99
    cube = models.CharField(verbose_name=_('Form'), max_length=256, blank=True, null=True)
100
    aggregate1 = models.CharField(verbose_name=_('Aggregate'), max_length=64, blank=True, null=True)
101
    drilldown1 = models.CharField(verbose_name=_('Criterion 1'), max_length=64, blank=True,
102
                                  null=True)
103
    drilldown2 = models.CharField(verbose_name=_('Criterion 2'), max_length=64, blank=True,
104
                                  null=True)
105
    other_parameters = models.TextField(verbose_name=_('Other parameters'), blank=True, null=True)
106

  
107
    class Meta:
108
        abstract = True
109

  
110
    @classmethod
111
    def is_enabled(self):
112
        return bool(getattr(settings, 'CUBES_URL', None))
113

  
114
    def get_additional_label(self):
115
        return self.title
116

  
117
    def get_default_form_class(self):
118
        from .forms import CubesBarChartForm
119
        return CubesBarChartForm
120

  
121
    def get_cell_extra_context(self, context):
122
        ctx = {
123
            'cell': self,
124
            'title': self.title,
125
            'url': self.url,
126
            'aggregate': self.get_aggregate(context=context),
127
        }
128
        cube = utils.get_cube(self.cube)
129
        aggregates = dict((ag['name'], ag['label']) for ag in cube.get('aggregates', []))
130
        drilldowns = utils.get_drilldown(self.cube)
131
        if self.aggregate1 and aggregates:
132
            ctx['aggregate1_label'] = aggregates.get(self.aggregate1)
133
        if self.drilldown1 and drilldowns:
134
            ctx['drilldown1_label'] = dict(drilldowns).get(self.drilldown1)
135
        if self.drilldown2 and drilldowns:
136
            ctx['drilldown2_label'] = dict(drilldowns).get(self.drilldown2)
137
        return ctx
138

  
139
    def get_aggregate(self, context={}):
140
        '''Get aggregate defined by chosen cube and the two drildown paths, request ordering of the
141
           data by natural order of each axis.'''
142
        from .utils import get_aggregate, get_cube, compute_levels
143
        def simplify_integers(l):
144
            for x in l:
145
                if isinstance(x, float):
146
                    if x - round(x) < 0.001:
147
                        x = int(x)
148
                yield x
149

  
150
        other_parameters = json.loads(self.other_parameters) if self.other_parameters else {}
151
        if context and 'parameters' in context:
152
            parameters = context['parameters']
153
            for key in parameters:
154
                if not key.startswith('cubes-cut-'):
155
                    continue
156
                name = key.split(u'cubes-cut-', 1)[1]
157
                value = parameters[key]
158
                new_cut = u'%s:%s' % (name, value)
159
                if other_parameters.get('cut'):
160
                    other_parameters['cut'] = other_parameters['cut'] + u'|' + new_cut
161
                else:
162
                    other_parameters['cut'] = new_cut
163

  
164
        aggregate = get_aggregate(name=self.cube,
165
                                  aggregate1=self.aggregate1,
166
                                  drilldown1=self.drilldown1,
167
                                  drilldown2=self.drilldown2,
168
                                  other_parameters=other_parameters)
169

  
170
        cube = get_cube(self.cube)
171
        if not aggregate or not cube:
172
            return
173

  
174
        label_refs1 = []
175
        key_refs1 = []
176
        if self.drilldown1:
177
            compute_levels(cube, self.drilldown1, label_refs=label_refs1, key_refs=key_refs1)
178
        key_refs2 = []
179
        label_refs2 = []
180
        if self.drilldown2:
181
            compute_levels(cube, self.drilldown2, label_refs=label_refs2, key_refs=key_refs2)
182
        for ag in cube['aggregates']:
183
            if ag['name'] != self.aggregate1:
184
                continue
185
            break
186

  
187
        def cell_ref(cell, refs):
188
            return tuple(cell[ref] for ref in refs)
189

  
190
        keys1 = OrderedDict()
191
        labels = OrderedDict()
192
        datasets = OrderedDict()
193

  
194
        for cell in aggregate['cells']:
195
            label1 = u' / '.join(map(unicode, simplify_integers(cell_ref(cell, label_refs1))))
196
            key1 = cell_ref(cell, key_refs1)
197
            labels[key1] = label1
198
            keys1[key1] = 1
199
            if key_refs2:
200
                label2 = u' / '.join(map(unicode, simplify_integers(cell_ref(cell, label_refs2))))
201
                key2 = cell_ref(cell, key_refs2)
202
            else:
203
                label2 = ''
204
                key2 = 1
205
            dataset = datasets.setdefault(key2, {'label': label2,
206
                                                 'data': OrderedDict()})
207
            value = cell[self.aggregate1]
208
            dataset['data'][key1] = value
209
        for dataset in datasets.itervalues():
210
            dataset['data'] = [dataset['data'].get(key, 0) for key in keys1]
211

  
212
        return {
213
            'labels': labels.values(),
214
            'datasets': [{
215
                'label': dataset['label'],
216
                'data': dataset['data'],
217
            } for dataset in datasets.itervalues()]
218
        }
219

  
220
    def render(self, context):
221
        if not context.get('synchronous'):
222
            raise NothingInCacheException()
223
        return super(BaseCubesChart, self).render(context)
224

  
225
    def render_for_search(self):
226
        return ''
227

  
228

  
229
@register_cell_class
230
class CubesBarChart(BaseCubesChart):
231
    template_name = 'combo/cubes-barchart.html'
232

  
233
    class Media:
234
        js = ('xstatic/ChartNew.js', 'js/combo.cubes-barchart.js')
235

  
236
    class Meta:
237
        verbose_name = _('Cubes Barchart')
238

  
239
    def get_cell_extra_context(self, context):
240
        ctx = super(CubesBarChart, self).get_cell_extra_context(context)
241
        # Need JSON serialization to pass data to Chart.js
242
        ctx['json_aggregate'] = json.dumps(ctx['aggregate'])
243
        return ctx
244

  
245
@register_cell_class
246
class CubesTable(BaseCubesChart):
247
    template_name = 'combo/cubes-table.html'
248

  
249
    class Meta:
250
        verbose_name = _('Cubes Table')
combo/apps/dataviz/templates/combo/cubes-barchart.html
1
{% if title %}
2
<h2>{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %}</h2>
3
{% endif %}
4
<script>
5
    var combo_cube_aggregate_{{ cell.id }} = {{ json_aggregate|safe }};
6
</script>
7
<div
8
     class="combo-cube-aggregate"
9
     data-combo-cube-aggregate-id="{{ cell.id }}"
10
     data-y-label="{{ aggregate1_label }}"
11
     data-x-label="{{ drilldown1_label }}"
12
     >
13
  <canvas style="width: 100%;">
14
  </canvas>
15
</div>
combo/apps/dataviz/templates/combo/cubes-table.html
1
{% if title %}
2
<h2>
3
  {% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %}
4
</h2>
5
{% endif %}
6
<table class="combo-cube-table">
7
  <thead>
8
    <th></th>
9
    {% for label in aggregate.labels %}
10
      <td>{{ label }}</td>
11
    {% endfor %}
12
  </thead>
13
  <tbody>
14
    {% for dataset in aggregate.datasets %}
15
      <tr> 
16
        <th>{{ dataset.label }}</th>
17
        {% for value in dataset.data %}
18
          <td>{{ value }}</td>
19
        {% endfor %}
20
      </tr>
21
    {% endfor %}
22
  </tbody>
23
</table>
combo/apps/dataviz/utils.py
1
# combo - content management system
2
# Copyright (C) 2015  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import requests
18
from requests.exceptions import RequestException
19
import urlparse
20

  
21
from django.utils.translation import ugettext as _
22
from django.conf import settings
23

  
24

  
25
def get_requests_params():
26
    return getattr(settings, 'CUBES_REQUESTS_PARAMS', {})
27

  
28

  
29
def get_cubes():
30
    try:
31
        r = requests.get(urlparse.urljoin(settings.CUBES_URL, 'cubes'), **get_requests_params())
32
    except RequestException:
33
        return []
34
    try:
35
        return r.json()
36
    except ValueError:
37
        return []
38

  
39

  
40
def get_cube(name):
41
    model_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/model' % name)
42
    try:
43
        r = requests.get(model_url, **get_requests_params())
44
    except RequestException:
45
        return None
46
    try:
47
        return r.json()
48
    except ValueError:
49
        return None
50

  
51

  
52
def get_drilldown(name):
53
    cube = get_cube(name)
54
    if not cube:
55
        return []
56
    l = []
57
    seen = set()
58
    for dimension in cube.get('dimensions', []):
59
        dim_name = dimension['name']
60
        dim_label = dimension.get('label') or dim_name
61
        if dimension.get('levels'):
62
            levels = {}
63
            for level in dimension['levels']:
64
                levels[level['name']] = level.get('label') or level['name']
65
            if dimension.get('hierarchies'):
66
                for hierarchy in dimension['hierarchies']:
67
                    h_name = hierarchy['name']
68
                    h_label = hierarchy.get('label') or h_name
69
                    if h_name == 'default':
70
                        h_label = ''
71
                    for i in range(1, len(hierarchy['levels'])+1):
72
                        level = hierarchy['levels'][i-1]
73
                        label = _(u'by ') + _(u' and ').join(
74
                            levels[level] for level in hierarchy['levels'][:i]
75
                        )
76
                        name = '%s@%s:%s' % (dim_name, h_name, level)
77
                        if label not in seen:
78
                            l.append((name, label))
79
                            seen.add(label)
80
            else:
81
                raise NotImplementedError
82
        else:
83
            l.append((dim_name, _(u'by %s') % dim_label))
84
    return l
85

  
86

  
87
def compute_levels(cube, drilldown, key_refs=None, label_refs=None):
88
    from .utils import get_attribute_ref
89
    dim = drilldown.split('@')[0]
90
    hier = drilldown.split('@')[1].split(':')[0]
91
    lev = drilldown.split(':')[1]
92

  
93
    for dimension in cube['dimensions']:
94
        if dimension['name'] != dim:
95
            continue
96
        level_label_refs = {}
97
        level_key_refs = {}
98
        for level in dimension['levels']:
99
            level_key_refs[level['name']] = get_attribute_ref(level, level['key'])
100
            level_label_refs[level['name']] = get_attribute_ref(level, level['label_attribute'])
101
        for hierarchy in dimension['hierarchies']:
102
            if hierarchy['name'] != hier:
103
                continue
104
            for level in hierarchy['levels']:
105
                if key_refs is not None:
106
                    key_refs.append(level_key_refs[level])
107
                if label_refs is not None:
108
                    label_refs.append(level_label_refs[level])
109
                if level == lev:
110
                    break
111
            break
112
        break
113

  
114

  
115
def get_aggregate(name, aggregate1, drilldown1, drilldown2, other_parameters=None):
116
    if not name:
117
        return None
118
    cube = get_cube(name)
119
    aggregate_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/aggregate' % name)
120
    if not aggregate1:
121
        return None
122
    try:
123
        params = {'aggregate': aggregate1}
124
        drilldowns = []
125
        key_refs = []
126
        if drilldown1:
127
            compute_levels(cube, drilldown1, key_refs=key_refs)
128
            drilldowns.append(drilldown1)
129
        if drilldown2:
130
            compute_levels(cube, drilldown2, key_refs=key_refs)
131
            drilldowns.append(drilldown2)
132
        if drilldowns:
133
            params['drilldown'] = drilldowns
134

  
135
        if key_refs:
136
            params['order'] = key_refs
137
        if other_parameters:
138
            params.update(other_parameters)
139

  
140
        r = requests.get(aggregate_url, params=params, **get_requests_params())
141
    except RequestException:
142
        return None
143
    try:
144
        return r.json()
145
    except ValueError:
146
        return None
147

  
148

  
149
def get_attribute_ref(level, name):
150
    for attribute in level['attributes']:
151
        if attribute['name'] == name:
152
            return attribute['ref']
153
-