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 from . import urls
return urls.urlpatterns 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' 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 # 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/>. # 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 import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from combo.utils import requests from combo.utils import requests
from .models import BaseCubesChart, ChartCell from .models import ChartCell
from .utils import get_cubes, get_cube, get_drilldown
class ChartForm(forms.ModelForm): 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.extend([(x['path'], x['name']) for x in result])
available_charts.sort(key=lambda x: x[1]) available_charts.sort(key=lambda x: x[1])
self.fields['url'].widget = forms.Select(choices=available_charts) 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 # 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/>. # 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.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ 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.models import CellBase
from combo.data.library import register_cell_class 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 @register_cell_class
class Gauge(CellBase): class Gauge(CellBase):
@ -92,159 +88,3 @@ class ChartCell(CellBase):
context['title'] = self.title context['title'] = self.title
context['url'] = self.url context['url'] = self.url
return context 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.maps',
'combo.apps.calendar', 'combo.apps.calendar',
'haystack', 'haystack',
'xstatic.pkg.chartnew_js',
'xstatic.pkg.josefinsans', 'xstatic.pkg.josefinsans',
'xstatic.pkg.leaflet', 'xstatic.pkg.leaflet',
'xstatic.pkg.opensans', 'xstatic.pkg.opensans',

1
debian/control vendored
View File

@ -14,7 +14,6 @@ Depends: ${misc:Depends}, ${python:Depends},
python-gadjo, python-gadjo,
python-requests, python-requests,
python-feedparser, python-feedparser,
python-xstatic-chartnew-js,
python-xstatic-josefinsans, python-xstatic-josefinsans,
python-xstatic-leaflet, python-xstatic-leaflet,
python-xstatic-leaflet-markercluster, python-xstatic-leaflet-markercluster,

View File

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

View File

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

View File

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