0001-dataviz-remove-legacy-cubes-code-12743.patch
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 |
- |