dataviz: remove legacy cubes code (#12743)

This commit is contained in:
Frédéric Péters 2018-03-17 23:18:41 +01:00
parent 5c41792080
commit 27186abb01
11 changed files with 2 additions and 446 deletions

View File

@ -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'

View File

@ -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 []

View File

@ -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')

View File

@ -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>

View File

@ -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>

View File

@ -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']

View File

@ -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',

1
debian/control vendored
View File

@ -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,

View File

@ -1,2 +1 @@
django_ckeditor python-django-ckeditor
XStatic-ChartNew.js python-xstatic-chartnew-js

View File

@ -4,7 +4,6 @@ gadjo
feedparser
django-jsonfield
requests
XStatic-ChartNew.js
XStatic-Leaflet
XStatic_JosefinSans
XStatic_OpenSans

View File

@ -140,7 +140,6 @@ setup(
'feedparser',
'django-jsonfield',
'requests',
'XStatic-ChartNew.js',
'XStatic-Leaflet',
'XStatic-Leaflet-MarkerCluster',
'XStatic_JosefinSans',