dataviz: remove legacy cubes code (#12743)
This commit is contained in:
parent
5c41792080
commit
27186abb01
|
@ -30,13 +30,4 @@ class AppConfig(django.apps.AppConfig):
|
|||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def ready(self):
|
||||
@checks.register('settings')
|
||||
def check_settings(**kwargs):
|
||||
# Check if CUBES_URL is a proper URL string
|
||||
if (getattr(settings, 'CUBES_URL', None) is not None
|
||||
and (not isinstance(settings.CUBES_URL, str)
|
||||
or not re.match(r'https?://', settings.CUBES_URL))):
|
||||
yield checks.Error('settings.CUBES_URL must be an HTTP URL')
|
||||
|
||||
default_app_config = 'combo.apps.dataviz.AppConfig'
|
||||
|
|
|
@ -14,17 +14,12 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from combo.utils import requests
|
||||
|
||||
from .models import BaseCubesChart, ChartCell
|
||||
from .utils import get_cubes, get_cube, get_drilldown
|
||||
from .models import ChartCell
|
||||
|
||||
|
||||
class ChartForm(forms.ModelForm):
|
||||
|
@ -42,78 +37,3 @@ class ChartForm(forms.ModelForm):
|
|||
available_charts.extend([(x['path'], x['name']) for x in result])
|
||||
available_charts.sort(key=lambda x: x[1])
|
||||
self.fields['url'].widget = forms.Select(choices=available_charts)
|
||||
|
||||
|
||||
class CubesBarChartForm(forms.ModelForm):
|
||||
EMPTY = [(u'', _('None'))]
|
||||
|
||||
class Meta:
|
||||
model = BaseCubesChart
|
||||
fields = ('title', 'url', 'cube', 'aggregate1', 'drilldown1', 'drilldown2',
|
||||
'other_parameters')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CubesBarChartForm, self).__init__(*args, **kwargs)
|
||||
for field in ('cube', 'aggregate1', 'drilldown1', 'drilldown2'):
|
||||
self.fields[field] = forms.ChoiceField(
|
||||
label=self.fields[field].label,
|
||||
initial=self.fields[field].initial,
|
||||
required=False,
|
||||
choices=self.EMPTY)
|
||||
if getattr(settings, 'CUBES_URL', None):
|
||||
cube_choices = self.get_cubes_choices()
|
||||
if cube_choices:
|
||||
self.fields['cube'].choices = cube_choices
|
||||
aggregate1_choices = self.get_aggregate_choices()
|
||||
# If there is no choice, hide the selector
|
||||
if not aggregate1_choices or len(aggregate1_choices) < 3:
|
||||
self.fields['aggregate1'].widget = forms.HiddenInput()
|
||||
if aggregate1_choices:
|
||||
self.fields['aggregate1'].choices = aggregate1_choices
|
||||
drilldown_choices = self.get_drilldown_choices()
|
||||
if drilldown_choices:
|
||||
self.fields['drilldown1'].choices = drilldown_choices
|
||||
self.fields['drilldown2'].choices = drilldown_choices
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = self.cleaned_data
|
||||
if getattr(settings, 'CUBES_URL', None):
|
||||
aggregate1_choices = self.get_aggregate_choices()
|
||||
# If there is no choice, autoselect
|
||||
if aggregate1_choices and len(aggregate1_choices) == 2:
|
||||
cleaned_data['aggregate1'] = aggregate1_choices[1][0]
|
||||
return cleaned_data
|
||||
|
||||
def clean_other_parameters(self):
|
||||
other_parameters = self.cleaned_data['other_parameters']
|
||||
if other_parameters:
|
||||
try:
|
||||
decoded = json.loads(other_parameters)
|
||||
assert isinstance(decoded, dict)
|
||||
for key, value in decoded.iteritems():
|
||||
assert isinstance(key, unicode)
|
||||
assert isinstance(value, unicode)
|
||||
except (ValueError, AssertionError):
|
||||
raise ValidationError(_('Other parameter must be a JSON object containing only '
|
||||
'strings'))
|
||||
return other_parameters
|
||||
|
||||
def get_cubes_choices(self):
|
||||
cubes = get_cubes()
|
||||
return self.EMPTY + [(cube['name'], cube.get('label')) for cube in cubes]
|
||||
|
||||
def get_aggregate_choices(self):
|
||||
cube = self.data.get(self.add_prefix('cube')) if self.data else self.instance.cube
|
||||
if cube:
|
||||
cube = get_cube(cube)
|
||||
if cube:
|
||||
return self.EMPTY + [(ag['name'], ag['label']) for ag in cube.get('aggregates', [])]
|
||||
return []
|
||||
|
||||
def get_drilldown_choices(self):
|
||||
cube = self.data.get(self.add_prefix('cube')) if self.data else self.instance.cube
|
||||
if cube:
|
||||
choices = get_drilldown(cube)
|
||||
if choices:
|
||||
return self.EMPTY + choices
|
||||
return []
|
||||
|
|
|
@ -14,9 +14,6 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -24,9 +21,8 @@ from django.conf import settings
|
|||
|
||||
from combo.data.models import CellBase
|
||||
from combo.data.library import register_cell_class
|
||||
from combo.utils import NothingInCacheException, get_templated_url
|
||||
from combo.utils import get_templated_url
|
||||
|
||||
from . import utils
|
||||
|
||||
@register_cell_class
|
||||
class Gauge(CellBase):
|
||||
|
@ -92,159 +88,3 @@ class ChartCell(CellBase):
|
|||
context['title'] = self.title
|
||||
context['url'] = self.url
|
||||
return context
|
||||
|
||||
class BaseCubesChart(CellBase):
|
||||
title = models.CharField(_('Title'), max_length=150, blank=True, null=True)
|
||||
url = models.URLField(_('URL'), max_length=150, blank=True, null=True)
|
||||
cube = models.CharField(verbose_name=_('Form'), max_length=256, blank=True, null=True)
|
||||
aggregate1 = models.CharField(verbose_name=_('Aggregate'), max_length=64, blank=True, null=True)
|
||||
drilldown1 = models.CharField(verbose_name=_('Criterion 1'), max_length=64, blank=True,
|
||||
null=True)
|
||||
drilldown2 = models.CharField(verbose_name=_('Criterion 2'), max_length=64, blank=True,
|
||||
null=True)
|
||||
other_parameters = models.TextField(verbose_name=_('Other parameters'), blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def is_enabled(self):
|
||||
return bool(getattr(settings, 'CUBES_URL', None))
|
||||
|
||||
def get_additional_label(self):
|
||||
return self.title
|
||||
|
||||
def get_default_form_class(self):
|
||||
from .forms import CubesBarChartForm
|
||||
return CubesBarChartForm
|
||||
|
||||
def get_cell_extra_context(self, context):
|
||||
ctx = {
|
||||
'cell': self,
|
||||
'title': self.title,
|
||||
'url': self.url,
|
||||
'aggregate': self.get_aggregate(context=context),
|
||||
}
|
||||
cube = utils.get_cube(self.cube)
|
||||
aggregates = dict((ag['name'], ag['label']) for ag in cube.get('aggregates', []))
|
||||
drilldowns = utils.get_drilldown(self.cube)
|
||||
if self.aggregate1 and aggregates:
|
||||
ctx['aggregate1_label'] = aggregates.get(self.aggregate1)
|
||||
if self.drilldown1 and drilldowns:
|
||||
ctx['drilldown1_label'] = dict(drilldowns).get(self.drilldown1)
|
||||
if self.drilldown2 and drilldowns:
|
||||
ctx['drilldown2_label'] = dict(drilldowns).get(self.drilldown2)
|
||||
return ctx
|
||||
|
||||
def get_aggregate(self, context={}):
|
||||
'''Get aggregate defined by chosen cube and the two drildown paths, request ordering of the
|
||||
data by natural order of each axis.'''
|
||||
from .utils import get_aggregate, get_cube, compute_levels
|
||||
def simplify_integers(l):
|
||||
for x in l:
|
||||
if isinstance(x, float):
|
||||
if x - round(x) < 0.001:
|
||||
x = int(x)
|
||||
yield x
|
||||
|
||||
other_parameters = json.loads(self.other_parameters) if self.other_parameters else {}
|
||||
if context and 'parameters' in context:
|
||||
parameters = context['parameters']
|
||||
for key in parameters:
|
||||
if not key.startswith('cubes-cut-'):
|
||||
continue
|
||||
name = key.split(u'cubes-cut-', 1)[1]
|
||||
value = parameters[key]
|
||||
new_cut = u'%s:%s' % (name, value)
|
||||
if other_parameters.get('cut'):
|
||||
other_parameters['cut'] = other_parameters['cut'] + u'|' + new_cut
|
||||
else:
|
||||
other_parameters['cut'] = new_cut
|
||||
|
||||
aggregate = get_aggregate(name=self.cube,
|
||||
aggregate1=self.aggregate1,
|
||||
drilldown1=self.drilldown1,
|
||||
drilldown2=self.drilldown2,
|
||||
other_parameters=other_parameters)
|
||||
|
||||
cube = get_cube(self.cube)
|
||||
if not aggregate or not cube:
|
||||
return
|
||||
|
||||
label_refs1 = []
|
||||
key_refs1 = []
|
||||
if self.drilldown1:
|
||||
compute_levels(cube, self.drilldown1, label_refs=label_refs1, key_refs=key_refs1)
|
||||
key_refs2 = []
|
||||
label_refs2 = []
|
||||
if self.drilldown2:
|
||||
compute_levels(cube, self.drilldown2, label_refs=label_refs2, key_refs=key_refs2)
|
||||
for ag in cube['aggregates']:
|
||||
if ag['name'] != self.aggregate1:
|
||||
continue
|
||||
break
|
||||
|
||||
def cell_ref(cell, refs):
|
||||
return tuple(cell[ref] for ref in refs)
|
||||
|
||||
keys1 = OrderedDict()
|
||||
labels = OrderedDict()
|
||||
datasets = OrderedDict()
|
||||
|
||||
for cell in aggregate['cells']:
|
||||
label1 = u' / '.join(map(unicode, simplify_integers(cell_ref(cell, label_refs1))))
|
||||
key1 = cell_ref(cell, key_refs1)
|
||||
labels[key1] = label1
|
||||
keys1[key1] = 1
|
||||
if key_refs2:
|
||||
label2 = u' / '.join(map(unicode, simplify_integers(cell_ref(cell, label_refs2))))
|
||||
key2 = cell_ref(cell, key_refs2)
|
||||
else:
|
||||
label2 = ''
|
||||
key2 = 1
|
||||
dataset = datasets.setdefault(key2, {'label': label2,
|
||||
'data': OrderedDict()})
|
||||
value = cell[self.aggregate1]
|
||||
dataset['data'][key1] = value
|
||||
for dataset in datasets.itervalues():
|
||||
dataset['data'] = [dataset['data'].get(key, 0) for key in keys1]
|
||||
|
||||
return {
|
||||
'labels': labels.values(),
|
||||
'datasets': [{
|
||||
'label': dataset['label'],
|
||||
'data': dataset['data'],
|
||||
} for dataset in datasets.itervalues()]
|
||||
}
|
||||
|
||||
def render(self, context):
|
||||
if not context.get('synchronous'):
|
||||
raise NothingInCacheException()
|
||||
return super(BaseCubesChart, self).render(context)
|
||||
|
||||
def render_for_search(self):
|
||||
return ''
|
||||
|
||||
|
||||
@register_cell_class
|
||||
class CubesBarChart(BaseCubesChart):
|
||||
template_name = 'combo/cubes-barchart.html'
|
||||
|
||||
class Media:
|
||||
js = ('xstatic/ChartNew.js', 'js/combo.cubes-barchart.js')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Cubes Barchart')
|
||||
|
||||
def get_cell_extra_context(self, context):
|
||||
ctx = super(CubesBarChart, self).get_cell_extra_context(context)
|
||||
# Need JSON serialization to pass data to Chart.js
|
||||
ctx['json_aggregate'] = json.dumps(ctx['aggregate'])
|
||||
return ctx
|
||||
|
||||
@register_cell_class
|
||||
class CubesTable(BaseCubesChart):
|
||||
template_name = 'combo/cubes-table.html'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Cubes Table')
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{% if title %}
|
||||
<h2>{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %}</h2>
|
||||
{% endif %}
|
||||
<script>
|
||||
var combo_cube_aggregate_{{ cell.id }} = {{ json_aggregate|safe }};
|
||||
</script>
|
||||
<div
|
||||
class="combo-cube-aggregate"
|
||||
data-combo-cube-aggregate-id="{{ cell.id }}"
|
||||
data-y-label="{{ aggregate1_label }}"
|
||||
data-x-label="{{ drilldown1_label }}"
|
||||
>
|
||||
<canvas style="width: 100%;">
|
||||
</canvas>
|
||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||
{% if title %}
|
||||
<h2>
|
||||
{% if url %}<a href="{{url}}">{% endif %}{{title}}{% if url %}</a>{% endif %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
<table class="combo-cube-table">
|
||||
<thead>
|
||||
<th></th>
|
||||
{% for label in aggregate.labels %}
|
||||
<td>{{ label }}</td>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for dataset in aggregate.datasets %}
|
||||
<tr>
|
||||
<th>{{ dataset.label }}</th>
|
||||
{% for value in dataset.data %}
|
||||
<td>{{ value }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -1,152 +0,0 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2015 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
import urlparse
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_requests_params():
|
||||
return getattr(settings, 'CUBES_REQUESTS_PARAMS', {})
|
||||
|
||||
|
||||
def get_cubes():
|
||||
try:
|
||||
r = requests.get(urlparse.urljoin(settings.CUBES_URL, 'cubes'), **get_requests_params())
|
||||
except RequestException:
|
||||
return []
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
|
||||
def get_cube(name):
|
||||
model_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/model' % name)
|
||||
try:
|
||||
r = requests.get(model_url, **get_requests_params())
|
||||
except RequestException:
|
||||
return None
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def get_drilldown(name):
|
||||
cube = get_cube(name)
|
||||
if not cube:
|
||||
return []
|
||||
l = []
|
||||
seen = set()
|
||||
for dimension in cube.get('dimensions', []):
|
||||
dim_name = dimension['name']
|
||||
dim_label = dimension.get('label') or dim_name
|
||||
if dimension.get('levels'):
|
||||
levels = {}
|
||||
for level in dimension['levels']:
|
||||
levels[level['name']] = level.get('label') or level['name']
|
||||
if dimension.get('hierarchies'):
|
||||
for hierarchy in dimension['hierarchies']:
|
||||
h_name = hierarchy['name']
|
||||
h_label = hierarchy.get('label') or h_name
|
||||
if h_name == 'default':
|
||||
h_label = ''
|
||||
for i in range(1, len(hierarchy['levels'])+1):
|
||||
level = hierarchy['levels'][i-1]
|
||||
label = _(u'by ') + _(u' and ').join(
|
||||
levels[level] for level in hierarchy['levels'][:i]
|
||||
)
|
||||
name = '%s@%s:%s' % (dim_name, h_name, level)
|
||||
if label not in seen:
|
||||
l.append((name, label))
|
||||
seen.add(label)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
l.append((dim_name, _(u'by %s') % dim_label))
|
||||
return l
|
||||
|
||||
|
||||
def compute_levels(cube, drilldown, key_refs=None, label_refs=None):
|
||||
from .utils import get_attribute_ref
|
||||
dim = drilldown.split('@')[0]
|
||||
hier = drilldown.split('@')[1].split(':')[0]
|
||||
lev = drilldown.split(':')[1]
|
||||
|
||||
for dimension in cube['dimensions']:
|
||||
if dimension['name'] != dim:
|
||||
continue
|
||||
level_label_refs = {}
|
||||
level_key_refs = {}
|
||||
for level in dimension['levels']:
|
||||
level_key_refs[level['name']] = get_attribute_ref(level, level['key'])
|
||||
level_label_refs[level['name']] = get_attribute_ref(level, level['label_attribute'])
|
||||
for hierarchy in dimension['hierarchies']:
|
||||
if hierarchy['name'] != hier:
|
||||
continue
|
||||
for level in hierarchy['levels']:
|
||||
if key_refs is not None:
|
||||
key_refs.append(level_key_refs[level])
|
||||
if label_refs is not None:
|
||||
label_refs.append(level_label_refs[level])
|
||||
if level == lev:
|
||||
break
|
||||
break
|
||||
break
|
||||
|
||||
|
||||
def get_aggregate(name, aggregate1, drilldown1, drilldown2, other_parameters=None):
|
||||
if not name:
|
||||
return None
|
||||
cube = get_cube(name)
|
||||
aggregate_url = urlparse.urljoin(settings.CUBES_URL, 'cube/%s/aggregate' % name)
|
||||
if not aggregate1:
|
||||
return None
|
||||
try:
|
||||
params = {'aggregate': aggregate1}
|
||||
drilldowns = []
|
||||
key_refs = []
|
||||
if drilldown1:
|
||||
compute_levels(cube, drilldown1, key_refs=key_refs)
|
||||
drilldowns.append(drilldown1)
|
||||
if drilldown2:
|
||||
compute_levels(cube, drilldown2, key_refs=key_refs)
|
||||
drilldowns.append(drilldown2)
|
||||
if drilldowns:
|
||||
params['drilldown'] = drilldowns
|
||||
|
||||
if key_refs:
|
||||
params['order'] = key_refs
|
||||
if other_parameters:
|
||||
params.update(other_parameters)
|
||||
|
||||
r = requests.get(aggregate_url, params=params, **get_requests_params())
|
||||
except RequestException:
|
||||
return None
|
||||
try:
|
||||
return r.json()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def get_attribute_ref(level, name):
|
||||
for attribute in level['attributes']:
|
||||
if attribute['name'] == name:
|
||||
return attribute['ref']
|
|
@ -77,7 +77,6 @@ INSTALLED_APPS = (
|
|||
'combo.apps.maps',
|
||||
'combo.apps.calendar',
|
||||
'haystack',
|
||||
'xstatic.pkg.chartnew_js',
|
||||
'xstatic.pkg.josefinsans',
|
||||
'xstatic.pkg.leaflet',
|
||||
'xstatic.pkg.opensans',
|
||||
|
|
|
@ -14,7 +14,6 @@ Depends: ${misc:Depends}, ${python:Depends},
|
|||
python-gadjo,
|
||||
python-requests,
|
||||
python-feedparser,
|
||||
python-xstatic-chartnew-js,
|
||||
python-xstatic-josefinsans,
|
||||
python-xstatic-leaflet,
|
||||
python-xstatic-leaflet-markercluster,
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
django_ckeditor python-django-ckeditor
|
||||
XStatic-ChartNew.js python-xstatic-chartnew-js
|
||||
|
|
|
@ -4,7 +4,6 @@ gadjo
|
|||
feedparser
|
||||
django-jsonfield
|
||||
requests
|
||||
XStatic-ChartNew.js
|
||||
XStatic-Leaflet
|
||||
XStatic_JosefinSans
|
||||
XStatic_OpenSans
|
||||
|
|
Loading…
Reference in New Issue