dataviz: render graph locally using pygal (#20771)
This commit is contained in:
parent
ee2d6e7395
commit
6f82fe8eb4
7
README
7
README
|
@ -120,3 +120,10 @@ Gauge.js
|
||||||
License: MIT
|
License: MIT
|
||||||
Comment:
|
Comment:
|
||||||
From http://bernii.github.io/gauge.js/
|
From http://bernii.github.io/gauge.js/
|
||||||
|
|
||||||
|
Pygal.tooltip.js
|
||||||
|
Files: combo/apps/dataviz/static/js/pygal.tooltip.js
|
||||||
|
Copyright: 2015, Florian Mounier Kozea
|
||||||
|
License: LGPL-3+
|
||||||
|
Comment:
|
||||||
|
From https://github.com/Kozea/pygal.js/
|
||||||
|
|
|
@ -19,7 +19,7 @@ from django.conf import settings
|
||||||
|
|
||||||
from combo.utils import requests
|
from combo.utils import requests
|
||||||
|
|
||||||
from .models import ChartCell
|
from .models import ChartCell, ChartNgCell
|
||||||
|
|
||||||
|
|
||||||
class ChartForm(forms.ModelForm):
|
class ChartForm(forms.ModelForm):
|
||||||
|
@ -37,3 +37,28 @@ 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 ChartNgForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ChartNgCell
|
||||||
|
fields = ('title', 'data_reference', 'chart_type', 'height')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ChartNgForm, self).__init__(*args, **kwargs)
|
||||||
|
data_references = []
|
||||||
|
bijoe_sites = settings.KNOWN_SERVICES.get('bijoe').items()
|
||||||
|
for site_key, site_dict in bijoe_sites:
|
||||||
|
result = requests.get('/visualization/json/',
|
||||||
|
remote_service=site_dict, without_user=True,
|
||||||
|
headers={'accept': 'application/json'}).json()
|
||||||
|
if len(bijoe_sites) > 1:
|
||||||
|
label_prefix = _('%s: ') % site_dict.get('title')
|
||||||
|
else:
|
||||||
|
label_prefix = ''
|
||||||
|
data_references.extend([
|
||||||
|
('%s:%s' % (site_key, x['slug']), '%s%s' % (label_prefix, x['name']))
|
||||||
|
for x in result])
|
||||||
|
|
||||||
|
data_references.sort(key=lambda x: x[1])
|
||||||
|
self.fields['data_reference'].widget = forms.Select(choices=data_references)
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2019-03-28 10:11
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('data', '0036_page_sub_slug'),
|
||||||
|
('dataviz', '0009_auto_20190617_1214'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ChartNgCell',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('placeholder', models.CharField(max_length=20)),
|
||||||
|
('order', models.PositiveIntegerField()),
|
||||||
|
('slug', models.SlugField(blank=True, verbose_name='Slug')),
|
||||||
|
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
|
||||||
|
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||||
|
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||||
|
('last_update_timestamp', models.DateTimeField(auto_now=True)),
|
||||||
|
('data_reference', models.CharField(max_length=150, verbose_name='Data')),
|
||||||
|
('title', models.CharField(blank=True, max_length=150, verbose_name='Title')),
|
||||||
|
('cached_json', jsonfield.fields.JSONField(blank=True, default=dict)),
|
||||||
|
('chart_type', models.CharField(choices=[(b'bar', 'Bar'), (b'horizontal-bar', 'Horizontal Bar'), (b'stacked-bar', 'Stacked Bar'), (b'line', 'Line'), (b'pie', 'Pie'), (b'dot', 'Dot'), (b'table', 'Table')], default=b'bar', max_length=20, verbose_name='Chart Type')),
|
||||||
|
('height', models.CharField(choices=[(b'150', 'Short (150px)'), (b'250', 'Average (250px)'), (b'350', 'Tall (350px)')], default=b'250', max_length=20, verbose_name='Height')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
|
||||||
|
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Chart',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='chartcell',
|
||||||
|
options={'verbose_name': 'Chart (legacy)'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -14,14 +14,20 @@
|
||||||
# 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 copy
|
||||||
|
import os
|
||||||
|
|
||||||
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 _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from jsonfield import JSONField
|
||||||
|
import pygal
|
||||||
|
|
||||||
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 get_templated_url
|
from combo.utils import get_templated_url, requests
|
||||||
|
|
||||||
|
|
||||||
@register_cell_class
|
@register_cell_class
|
||||||
|
@ -68,11 +74,11 @@ class ChartCell(CellBase):
|
||||||
url = models.URLField(_('URL'), max_length=250, blank=True, null=True)
|
url = models.URLField(_('URL'), max_length=250, blank=True, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Chart')
|
verbose_name = _('Chart (legacy)')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe')
|
return settings.LEGACY_CHART_CELL_ENABLED and hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe')
|
||||||
|
|
||||||
def get_default_form_class(self):
|
def get_default_form_class(self):
|
||||||
from .forms import ChartForm
|
from .forms import ChartForm
|
||||||
|
@ -88,3 +94,149 @@ class ChartCell(CellBase):
|
||||||
context['title'] = self.title
|
context['title'] = self.title
|
||||||
context['url'] = self.url
|
context['url'] = self.url
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@register_cell_class
|
||||||
|
class ChartNgCell(CellBase):
|
||||||
|
data_reference = models.CharField(_('Data'), max_length=150)
|
||||||
|
title = models.CharField(_('Title'), max_length=150, blank=True)
|
||||||
|
cached_json = JSONField(blank=True)
|
||||||
|
chart_type = models.CharField(_('Chart Type'), max_length=20, default='bar',
|
||||||
|
choices=(
|
||||||
|
('bar', _('Bar')),
|
||||||
|
('horizontal-bar', _('Horizontal Bar')),
|
||||||
|
('stacked-bar', _('Stacked Bar')),
|
||||||
|
('line', _('Line')),
|
||||||
|
('pie', _('Pie')),
|
||||||
|
('dot', _('Dot')),
|
||||||
|
('table', _('Table')),
|
||||||
|
))
|
||||||
|
|
||||||
|
height = models.CharField(_('Height'), max_length=20, default='250',
|
||||||
|
choices=(
|
||||||
|
('150', _('Short (150px)')),
|
||||||
|
('250', _('Average (250px)')),
|
||||||
|
('350', _('Tall (350px)')),
|
||||||
|
))
|
||||||
|
|
||||||
|
manager_form_template = 'combo/chartngcell_form.html'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Chart')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_enabled(self):
|
||||||
|
return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe')
|
||||||
|
|
||||||
|
def get_default_form_class(self):
|
||||||
|
from .forms import ChartNgForm
|
||||||
|
return ChartNgForm
|
||||||
|
|
||||||
|
def get_additional_label(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.data_reference:
|
||||||
|
site_key, visualization_slug = self.data_reference.split(':')
|
||||||
|
site_dict = settings.KNOWN_SERVICES['bijoe'][site_key]
|
||||||
|
response_json = requests.get('/visualization/json/',
|
||||||
|
remote_service=site_dict, without_user=True,
|
||||||
|
headers={'accept': 'application/json'}).json()
|
||||||
|
if isinstance(response_json, dict):
|
||||||
|
# forward compatibility with possible API change
|
||||||
|
response_json = response_json.get('data')
|
||||||
|
for visualization in response_json:
|
||||||
|
slug = visualization.get('slug')
|
||||||
|
if slug == visualization_slug:
|
||||||
|
self.cached_json = visualization
|
||||||
|
return super(ChartNgCell, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_cell_extra_context(self, context):
|
||||||
|
ctx = super(ChartNgCell, self).get_cell_extra_context(context)
|
||||||
|
if self.chart_type == 'table':
|
||||||
|
chart = self.get_chart(raise_if_not_cached=not(context.get('synchronous')))
|
||||||
|
ctx['table'] = chart.render_table(
|
||||||
|
transpose=bool(chart.axis_count == 2),
|
||||||
|
)
|
||||||
|
ctx['table'] = ctx['table'].replace('<table>', '<table class="main">')
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_chart(self, width=None, height=None, raise_if_not_cached=False):
|
||||||
|
response = requests.get(
|
||||||
|
self.cached_json['data-url'],
|
||||||
|
cache_duration=300,
|
||||||
|
raise_if_not_cached=raise_if_not_cached).json()
|
||||||
|
|
||||||
|
style = pygal.style.DefaultStyle(
|
||||||
|
font_family='OpenSans, sans-serif',
|
||||||
|
background='transparent')
|
||||||
|
|
||||||
|
chart = {
|
||||||
|
'bar': pygal.Bar,
|
||||||
|
'horizontal-bar': pygal.HorizontalBar,
|
||||||
|
'stacked-bar': pygal.StackedBar,
|
||||||
|
'line': pygal.Line,
|
||||||
|
'pie': pygal.Pie,
|
||||||
|
'dot': pygal.Dot,
|
||||||
|
'table': pygal.Bar,
|
||||||
|
}[self.chart_type](config=pygal.Config(style=copy.copy(style)))
|
||||||
|
|
||||||
|
# normalize axis to have a fake axis when there are no dimensions and
|
||||||
|
# always a x axis when there is a single dimension.
|
||||||
|
x_labels = response['axis'].get('x_labels') or []
|
||||||
|
y_labels = response['axis'].get('y_labels') or []
|
||||||
|
data = response['data']
|
||||||
|
if not x_labels and not y_labels: # unidata
|
||||||
|
x_labels = ['']
|
||||||
|
y_labels = ['']
|
||||||
|
data = [data]
|
||||||
|
chart.axis_count = 0
|
||||||
|
elif not x_labels:
|
||||||
|
x_labels = y_labels
|
||||||
|
y_labels = ['']
|
||||||
|
chart.axis_count = 1
|
||||||
|
elif not y_labels:
|
||||||
|
y_labels = ['']
|
||||||
|
chart.axis_count = 1
|
||||||
|
else:
|
||||||
|
chart.axis_count = 2
|
||||||
|
|
||||||
|
chart.config.margin = 0
|
||||||
|
if width:
|
||||||
|
chart.config.width = width
|
||||||
|
if height:
|
||||||
|
chart.config.height = height
|
||||||
|
if width or height:
|
||||||
|
chart.config.explicit_size = True
|
||||||
|
chart.config.js = [os.path.join(settings.STATIC_URL, 'js/pygal-tooltips.js')]
|
||||||
|
chart.x_labels = x_labels
|
||||||
|
|
||||||
|
chart.show_legend = bool(len(response['axis']) > 1)
|
||||||
|
# matplotlib tab10 palette
|
||||||
|
chart.config.style.colors = (
|
||||||
|
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
|
||||||
|
'#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
|
||||||
|
'#bcbd22', '#17becf')
|
||||||
|
|
||||||
|
if self.chart_type == 'dot':
|
||||||
|
chart.show_legend = False
|
||||||
|
# use a single colour for dots
|
||||||
|
chart.config.style.colors = ('#1f77b4',) * len(x_labels)
|
||||||
|
|
||||||
|
if self.chart_type != 'pie':
|
||||||
|
for i, serie_label in enumerate(y_labels):
|
||||||
|
if chart.axis_count < 2:
|
||||||
|
values = data
|
||||||
|
else:
|
||||||
|
values = [data[i][j] for j in range(len(x_labels))]
|
||||||
|
chart.add(serie_label, values)
|
||||||
|
else:
|
||||||
|
# pie, create a serie by data, to get different colours
|
||||||
|
values = data
|
||||||
|
for label, value in zip(x_labels, values):
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
chart.add(label, value)
|
||||||
|
chart.show_legend = True
|
||||||
|
|
||||||
|
return chart
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
(function() {
|
||||||
|
var $, get_translation, init, init_svg, matches, padding, r_translation, sibl, svg_ns, tooltip_timeout, xlink_ns;
|
||||||
|
|
||||||
|
svg_ns = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
|
xlink_ns = 'http://www.w3.org/1999/xlink';
|
||||||
|
|
||||||
|
$ = function(sel, ctx) {
|
||||||
|
if (ctx == null) {
|
||||||
|
ctx = null;
|
||||||
|
}
|
||||||
|
ctx = ctx || document;
|
||||||
|
return Array.prototype.slice.call(ctx.querySelectorAll(sel), 0).filter(function(e) {
|
||||||
|
return e !== ctx;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
matches = function(el, selector) {
|
||||||
|
return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
sibl = function(el, match) {
|
||||||
|
if (match == null) {
|
||||||
|
match = null;
|
||||||
|
}
|
||||||
|
return Array.prototype.filter.call(el.parentElement.children, function(child) {
|
||||||
|
return child !== el && (!match || matches(child, match));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Array.prototype.one = function() {
|
||||||
|
return this.length > 0 && this[0] || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
padding = 5;
|
||||||
|
|
||||||
|
tooltip_timeout = null;
|
||||||
|
|
||||||
|
r_translation = /translate\((\d+)[ ,]+(\d+)\)/;
|
||||||
|
|
||||||
|
get_translation = function(el) {
|
||||||
|
return (r_translation.exec(el.getAttribute('transform')) || []).slice(1).map(function(x) {
|
||||||
|
return +x;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
init = function(ctx) {
|
||||||
|
var bbox, box, config, el, graph, inner_svg, num, parent, tooltip, tooltip_el, tt, uid, untooltip, xconvert, yconvert, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3;
|
||||||
|
if ($('svg', ctx).length) {
|
||||||
|
inner_svg = $('svg', ctx).one();
|
||||||
|
parent = inner_svg.parentElement;
|
||||||
|
box = inner_svg.viewBox.baseVal;
|
||||||
|
bbox = parent.getBBox();
|
||||||
|
xconvert = function(x) {
|
||||||
|
return ((x - box.x) / box.width) * bbox.width;
|
||||||
|
};
|
||||||
|
yconvert = function(y) {
|
||||||
|
return ((y - box.y) / box.height) * bbox.height;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
xconvert = yconvert = function(x) {
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (((_ref = window.pygal) != null ? _ref.config : void 0) != null) {
|
||||||
|
if (window.pygal.config.no_prefix != null) {
|
||||||
|
config = window.pygal.config;
|
||||||
|
} else {
|
||||||
|
uid = ctx.id.replace('chart-', '');
|
||||||
|
config = window.pygal.config[uid];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config = window.config;
|
||||||
|
}
|
||||||
|
tooltip_el = null;
|
||||||
|
graph = $('.graph').one();
|
||||||
|
tt = $('.tooltip', ctx).one();
|
||||||
|
_ref1 = $('.reactive', ctx);
|
||||||
|
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
||||||
|
el = _ref1[_i];
|
||||||
|
el.addEventListener('mouseenter', (function(el) {
|
||||||
|
return function() {
|
||||||
|
return el.classList.add('active');
|
||||||
|
};
|
||||||
|
})(el));
|
||||||
|
el.addEventListener('mouseleave', (function(el) {
|
||||||
|
return function() {
|
||||||
|
return el.classList.remove('active');
|
||||||
|
};
|
||||||
|
})(el));
|
||||||
|
}
|
||||||
|
_ref2 = $('.activate-serie', ctx);
|
||||||
|
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
|
||||||
|
el = _ref2[_j];
|
||||||
|
num = el.id.replace('activate-serie-', '');
|
||||||
|
el.addEventListener('mouseenter', (function(num) {
|
||||||
|
return function() {
|
||||||
|
var re, _k, _l, _len2, _len3, _ref3, _ref4, _results;
|
||||||
|
_ref3 = $('.serie-' + num + ' .reactive', ctx);
|
||||||
|
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
|
||||||
|
re = _ref3[_k];
|
||||||
|
re.classList.add('active');
|
||||||
|
}
|
||||||
|
_ref4 = $('.serie-' + num + ' .showable', ctx);
|
||||||
|
_results = [];
|
||||||
|
for (_l = 0, _len3 = _ref4.length; _l < _len3; _l++) {
|
||||||
|
re = _ref4[_l];
|
||||||
|
_results.push(re.classList.add('shown'));
|
||||||
|
}
|
||||||
|
return _results;
|
||||||
|
};
|
||||||
|
})(num));
|
||||||
|
el.addEventListener('mouseleave', (function(num) {
|
||||||
|
return function() {
|
||||||
|
var re, _k, _l, _len2, _len3, _ref3, _ref4, _results;
|
||||||
|
_ref3 = $('.serie-' + num + ' .reactive', ctx);
|
||||||
|
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
|
||||||
|
re = _ref3[_k];
|
||||||
|
re.classList.remove('active');
|
||||||
|
}
|
||||||
|
_ref4 = $('.serie-' + num + ' .showable', ctx);
|
||||||
|
_results = [];
|
||||||
|
for (_l = 0, _len3 = _ref4.length; _l < _len3; _l++) {
|
||||||
|
re = _ref4[_l];
|
||||||
|
_results.push(re.classList.remove('shown'));
|
||||||
|
}
|
||||||
|
return _results;
|
||||||
|
};
|
||||||
|
})(num));
|
||||||
|
el.addEventListener('click', (function(el, num) {
|
||||||
|
return function() {
|
||||||
|
var ov, re, rect, show, _k, _l, _len2, _len3, _ref3, _ref4, _results;
|
||||||
|
rect = $('rect', el).one();
|
||||||
|
show = rect.style.fill !== '';
|
||||||
|
rect.style.fill = show ? '' : 'transparent';
|
||||||
|
_ref3 = $('.serie-' + num + ' .reactive', ctx);
|
||||||
|
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
|
||||||
|
re = _ref3[_k];
|
||||||
|
re.style.display = show ? '' : 'none';
|
||||||
|
}
|
||||||
|
_ref4 = $('.text-overlay .serie-' + num, ctx);
|
||||||
|
_results = [];
|
||||||
|
for (_l = 0, _len3 = _ref4.length; _l < _len3; _l++) {
|
||||||
|
ov = _ref4[_l];
|
||||||
|
_results.push(ov.style.display = show ? '' : 'none');
|
||||||
|
}
|
||||||
|
return _results;
|
||||||
|
};
|
||||||
|
})(el, num));
|
||||||
|
}
|
||||||
|
_ref3 = $('.tooltip-trigger', ctx);
|
||||||
|
for (_k = 0, _len2 = _ref3.length; _k < _len2; _k++) {
|
||||||
|
el = _ref3[_k];
|
||||||
|
el.addEventListener('mouseenter', (function(el) {
|
||||||
|
return function() {
|
||||||
|
return tooltip_el = tooltip(el);
|
||||||
|
};
|
||||||
|
})(el));
|
||||||
|
}
|
||||||
|
tt.addEventListener('mouseenter', function() {
|
||||||
|
return tooltip_el != null ? tooltip_el.classList.add('active') : void 0;
|
||||||
|
});
|
||||||
|
tt.addEventListener('mouseleave', function() {
|
||||||
|
return tooltip_el != null ? tooltip_el.classList.remove('active') : void 0;
|
||||||
|
});
|
||||||
|
ctx.addEventListener('mouseleave', function() {
|
||||||
|
if (tooltip_timeout) {
|
||||||
|
clearTimeout(tooltip_timeout);
|
||||||
|
}
|
||||||
|
return untooltip(0);
|
||||||
|
});
|
||||||
|
graph.addEventListener('mousemove', function(el) {
|
||||||
|
if (tooltip_timeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!matches(el.target, '.background')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return untooltip(1000);
|
||||||
|
});
|
||||||
|
tooltip = function(el) {
|
||||||
|
var a, baseline, cls, current_x, current_y, dy, h, i, key, keys, label, legend, name, plot_x, plot_y, rect, serie_index, subval, text, text_group, texts, traversal, value, w, x, x_elt, x_label, xlink, y, y_elt, _l, _len3, _len4, _len5, _m, _n, _ref4, _ref5, _ref6, _ref7, _ref8;
|
||||||
|
clearTimeout(tooltip_timeout);
|
||||||
|
tooltip_timeout = null;
|
||||||
|
tt.style.opacity = 1;
|
||||||
|
tt.style.display = '';
|
||||||
|
text_group = $('g.text', tt).one();
|
||||||
|
rect = $('rect', tt).one();
|
||||||
|
text_group.innerHTML = '';
|
||||||
|
label = sibl(el, '.label').one().textContent;
|
||||||
|
x_label = sibl(el, '.x_label').one().textContent;
|
||||||
|
value = sibl(el, '.value').one().textContent;
|
||||||
|
xlink = sibl(el, '.xlink').one().textContent;
|
||||||
|
serie_index = null;
|
||||||
|
parent = el;
|
||||||
|
traversal = [];
|
||||||
|
while (parent) {
|
||||||
|
traversal.push(parent);
|
||||||
|
if (parent.classList.contains('series')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
if (parent) {
|
||||||
|
_ref4 = parent.classList;
|
||||||
|
for (_l = 0, _len3 = _ref4.length; _l < _len3; _l++) {
|
||||||
|
cls = _ref4[_l];
|
||||||
|
if (cls.indexOf('serie-') === 0) {
|
||||||
|
serie_index = +cls.replace('serie-', '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend = null;
|
||||||
|
if (serie_index !== null) {
|
||||||
|
legend = config.legends[serie_index];
|
||||||
|
}
|
||||||
|
dy = 0;
|
||||||
|
keys = [[label, 'label']];
|
||||||
|
_ref5 = value.split('\n');
|
||||||
|
for (i = _m = 0, _len4 = _ref5.length; _m < _len4; i = ++_m) {
|
||||||
|
subval = _ref5[i];
|
||||||
|
keys.push([subval, 'value-' + i]);
|
||||||
|
}
|
||||||
|
if (config.tooltip_fancy_mode) {
|
||||||
|
keys.push([xlink, 'xlink']);
|
||||||
|
keys.unshift([x_label, 'x_label']);
|
||||||
|
keys.unshift([legend, 'legend']);
|
||||||
|
}
|
||||||
|
texts = {};
|
||||||
|
for (_n = 0, _len5 = keys.length; _n < _len5; _n++) {
|
||||||
|
_ref6 = keys[_n], key = _ref6[0], name = _ref6[1];
|
||||||
|
if (key) {
|
||||||
|
text = document.createElementNS(svg_ns, 'text');
|
||||||
|
text.textContent = key;
|
||||||
|
text.setAttribute('x', padding);
|
||||||
|
text.setAttribute('dy', dy);
|
||||||
|
text.classList.add(name.indexOf('value') === 0 ? 'value' : name);
|
||||||
|
if (name.indexOf('value') === 0 && config.tooltip_fancy_mode) {
|
||||||
|
text.classList.add('color-' + serie_index);
|
||||||
|
}
|
||||||
|
if (name === 'xlink') {
|
||||||
|
a = document.createElementNS(svg_ns, 'a');
|
||||||
|
a.setAttributeNS(xlink_ns, 'href', key);
|
||||||
|
a.textContent = void 0;
|
||||||
|
a.appendChild(text);
|
||||||
|
text.textContent = 'Link >';
|
||||||
|
text_group.appendChild(a);
|
||||||
|
} else {
|
||||||
|
text_group.appendChild(text);
|
||||||
|
}
|
||||||
|
dy += text.getBBox().height + padding / 2;
|
||||||
|
baseline = padding;
|
||||||
|
if (text.style.dominantBaseline !== void 0) {
|
||||||
|
text.style.dominantBaseline = 'text-before-edge';
|
||||||
|
} else {
|
||||||
|
baseline += text.getBBox().height * .8;
|
||||||
|
}
|
||||||
|
text.setAttribute('y', baseline);
|
||||||
|
texts[name] = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w = text_group.getBBox().width + 2 * padding;
|
||||||
|
h = text_group.getBBox().height + 2 * padding;
|
||||||
|
rect.setAttribute('width', w);
|
||||||
|
rect.setAttribute('height', h);
|
||||||
|
if (texts.value) {
|
||||||
|
texts.value.setAttribute('dx', (w - texts.value.getBBox().width) / 2 - padding);
|
||||||
|
}
|
||||||
|
if (texts.x_label) {
|
||||||
|
texts.x_label.setAttribute('dx', w - texts.x_label.getBBox().width - 2 * padding);
|
||||||
|
}
|
||||||
|
if (texts.xlink) {
|
||||||
|
texts.xlink.setAttribute('dx', w - texts.xlink.getBBox().width - 2 * padding);
|
||||||
|
}
|
||||||
|
x_elt = sibl(el, '.x').one();
|
||||||
|
y_elt = sibl(el, '.y').one();
|
||||||
|
x = parseInt(x_elt.textContent);
|
||||||
|
if (x_elt.classList.contains('centered')) {
|
||||||
|
x -= w / 2;
|
||||||
|
} else if (x_elt.classList.contains('left')) {
|
||||||
|
x -= w;
|
||||||
|
} else if (x_elt.classList.contains('auto')) {
|
||||||
|
x = xconvert(el.getBBox().x + el.getBBox().width / 2) - w / 2;
|
||||||
|
}
|
||||||
|
y = parseInt(y_elt.textContent);
|
||||||
|
if (y_elt.classList.contains('centered')) {
|
||||||
|
y -= h / 2;
|
||||||
|
} else if (y_elt.classList.contains('top')) {
|
||||||
|
y -= h;
|
||||||
|
} else if (y_elt.classList.contains('auto')) {
|
||||||
|
y = yconvert(el.getBBox().y + el.getBBox().height / 2) - h / 2;
|
||||||
|
}
|
||||||
|
_ref7 = get_translation(tt.parentElement), plot_x = _ref7[0], plot_y = _ref7[1];
|
||||||
|
if (x + w + plot_x > config.width) {
|
||||||
|
x = config.width - w - plot_x;
|
||||||
|
}
|
||||||
|
if (y + h + plot_y > config.height) {
|
||||||
|
y = config.height - h - plot_y;
|
||||||
|
}
|
||||||
|
if (x + plot_x < 0) {
|
||||||
|
x = -plot_x;
|
||||||
|
}
|
||||||
|
if (y + plot_y < 0) {
|
||||||
|
y = -plot_y;
|
||||||
|
}
|
||||||
|
_ref8 = get_translation(tt), current_x = _ref8[0], current_y = _ref8[1];
|
||||||
|
if (current_x === x && current_y === y) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
tt.setAttribute('transform', "translate(" + x + " " + y + ")");
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
return untooltip = function(ms) {
|
||||||
|
return tooltip_timeout = setTimeout(function() {
|
||||||
|
tt.style.display = 'none';
|
||||||
|
tt.style.opacity = 0;
|
||||||
|
if (tooltip_el != null) {
|
||||||
|
tooltip_el.classList.remove('active');
|
||||||
|
}
|
||||||
|
return tooltip_timeout = null;
|
||||||
|
}, ms);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
init_svg = function() {
|
||||||
|
var chart, charts, _i, _len, _results;
|
||||||
|
charts = $('.pygal-chart');
|
||||||
|
if (charts.length) {
|
||||||
|
_results = [];
|
||||||
|
for (_i = 0, _len = charts.length; _i < _len; _i++) {
|
||||||
|
chart = charts[_i];
|
||||||
|
_results.push(init(chart));
|
||||||
|
}
|
||||||
|
return _results;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
init_svg();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
return init_svg();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pygal = window.pygal || {};
|
||||||
|
|
||||||
|
window.pygal.init = init;
|
||||||
|
|
||||||
|
window.pygal.init_svg = init_svg;
|
||||||
|
|
||||||
|
}).call(this);
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% if cell.title %}<h2>{{cell.title}}</h2>{% endif %}
|
||||||
|
{% if cell.chart_type == "table" %}
|
||||||
|
{{table|safe}}
|
||||||
|
{% else %}
|
||||||
|
<div style="min-height: {{cell.height}}px">
|
||||||
|
<embed id="chart-{{cell.id}}" type="image/svg+xml" style="width: 100%"/>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
var last_width = 1;
|
||||||
|
$(window).on('load resize gadjo:sidepage-toggled', function() {
|
||||||
|
var chart_cell = $('#chart-{{cell.id}}').parent();
|
||||||
|
var new_width = Math.floor($(chart_cell).width());
|
||||||
|
var ratio = new_width / last_width;
|
||||||
|
if (ratio > 1.2 || ratio < 0.8) {
|
||||||
|
$('#chart-{{cell.id}}').attr('src',
|
||||||
|
"{% url 'combo-dataviz-graph' cell=cell.id %}?width=" + new_width);
|
||||||
|
last_width = new_width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div style="position: relative">
|
||||||
|
{{ form.as_p }}
|
||||||
|
{% if cell.chart_type != "table" %}
|
||||||
|
<div style="position: absolute; right: 0; top: 0; width: 300px; height: 150px">
|
||||||
|
<embed type="image/svg+xml" src="{% url 'combo-dataviz-graph' cell=cell.id %}?width=300&height=150"/>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from .views import ajax_gauge_count
|
from .views import ajax_gauge_count, dataviz_graph
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$',
|
url(r'^ajax/gauge-count/(?P<cell>[\w_-]+)/$',
|
||||||
ajax_gauge_count, name='combo-ajax-gauge-count'),
|
ajax_gauge_count, name='combo-ajax-gauge-count'),
|
||||||
|
url(r'^api/dataviz/graph/(?P<cell>[\w_-]+)/$',
|
||||||
|
dataviz_graph, name='combo-dataviz-graph'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# combo - content management system
|
# combo - content management system
|
||||||
# Copyright (C) 2015 Entr'ouvert
|
# Copyright (C) 2015-2019 Entr'ouvert
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify it
|
# 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
|
# under the terms of the GNU Affero General Public License as published
|
||||||
|
@ -14,13 +14,27 @@
|
||||||
# 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/>.
|
||||||
|
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from combo.utils import get_templated_url, requests
|
from combo.utils import get_templated_url, requests
|
||||||
from .models import Gauge
|
from .models import Gauge, ChartNgCell
|
||||||
|
|
||||||
|
|
||||||
def ajax_gauge_count(request, *args, **kwargs):
|
def ajax_gauge_count(request, *args, **kwargs):
|
||||||
gauge = Gauge.objects.get(id=kwargs['cell'])
|
gauge = Gauge.objects.get(id=kwargs['cell'])
|
||||||
response = requests.get(get_templated_url(gauge.data_source))
|
response = requests.get(get_templated_url(gauge.data_source))
|
||||||
return HttpResponse(response.content, content_type='text/json')
|
return HttpResponse(response.content, content_type='text/json')
|
||||||
|
|
||||||
|
|
||||||
|
def dataviz_graph(request, *args, **kwargs):
|
||||||
|
cell = ChartNgCell.objects.get(id=kwargs.get('cell'))
|
||||||
|
if not cell.page.is_visible(request.user):
|
||||||
|
raise PermissionDenied()
|
||||||
|
if not cell.is_visible(request.user):
|
||||||
|
raise PermissionDenied()
|
||||||
|
chart = cell.get_chart(
|
||||||
|
width=int(request.GET.get('width', 0)) or None,
|
||||||
|
height=int(request.GET.get('height', 0)) or int(cell.height)
|
||||||
|
)
|
||||||
|
return HttpResponse(chart.render(), content_type='image/svg+xml')
|
||||||
|
|
|
@ -334,6 +334,7 @@ PWA_NOTIFICATION_ICON_URL = None
|
||||||
|
|
||||||
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
|
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
|
||||||
BOOKING_CALENDAR_CELL_ENABLED = False
|
BOOKING_CALENDAR_CELL_ENABLED = False
|
||||||
|
LEGACY_CHART_CELL_ENABLED = False
|
||||||
NEWSLETTERS_CELL_ENABLED = False
|
NEWSLETTERS_CELL_ENABLED = False
|
||||||
USERSEARCH_CELL_ENABLED = False
|
USERSEARCH_CELL_ENABLED = False
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,9 @@ Depends: ${misc:Depends}, ${python:Depends},
|
||||||
python-django-haystack (>= 2.4.0),
|
python-django-haystack (>= 2.4.0),
|
||||||
python-sorl-thumbnail,
|
python-sorl-thumbnail,
|
||||||
python-pil,
|
python-pil,
|
||||||
python-pywebpush
|
python-pywebpush,
|
||||||
|
python-pygal,
|
||||||
|
python-lxml
|
||||||
Recommends: python-django-mellon, python-whoosh
|
Recommends: python-django-mellon, python-whoosh
|
||||||
Conflicts: python-lingo
|
Conflicts: python-lingo
|
||||||
Description: Portal Management System (Python module)
|
Description: Portal Management System (Python module)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -166,6 +166,8 @@ setup(
|
||||||
'Pillow',
|
'Pillow',
|
||||||
'pyproj',
|
'pyproj',
|
||||||
'pywebpush',
|
'pywebpush',
|
||||||
|
'pygal',
|
||||||
|
'lxml',
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
cmdclass={
|
cmdclass={
|
||||||
|
|
|
@ -61,6 +61,7 @@ if 'DISABLE_MIGRATIONS' in os.environ:
|
||||||
FAMILY_SERVICE = {'root': '/'}
|
FAMILY_SERVICE = {'root': '/'}
|
||||||
|
|
||||||
BOOKING_CALENDAR_CELL_ENABLED = True
|
BOOKING_CALENDAR_CELL_ENABLED = True
|
||||||
|
LEGACY_CHART_CELL_ENABLED = True
|
||||||
NEWSLETTERS_CELL_ENABLED = True
|
NEWSLETTERS_CELL_ENABLED = True
|
||||||
|
|
||||||
USER_PROFILE_CONFIG = {
|
USER_PROFILE_CONFIG = {
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
|
import json
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
from httmock import HTTMock
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User, Group
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from combo.data.models import Page
|
from combo.data.models import Page
|
||||||
from combo.apps.dataviz.models import Gauge
|
from combo.apps.dataviz.models import Gauge, ChartNgCell
|
||||||
|
|
||||||
|
from .test_public import login, normal_user
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
@ -37,3 +43,211 @@ def test_json_gauge(app, cell):
|
||||||
resp = app.get('/ajax/gauge-count/%s/' % cell.id)
|
resp = app.get('/ajax/gauge-count/%s/' % cell.id)
|
||||||
assert resp.text == 'xxx'
|
assert resp.text == 'xxx'
|
||||||
assert requests_get.call_args[0][0] == 'http://www.example.net/XXX'
|
assert requests_get.call_args[0][0] == 'http://www.example.net/XXX'
|
||||||
|
|
||||||
|
|
||||||
|
VISUALIZATION_JSON = [
|
||||||
|
{
|
||||||
|
'data-url': 'https://bijoe.example.com/visualization/1/json/',
|
||||||
|
'path': 'https://bijoe.example.com/visualization/1/iframe/?signature=123',
|
||||||
|
'name': 'example visualization (X)',
|
||||||
|
'slug': 'example',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'data-url': 'https://bijoe.example.com/visualization/2/json/',
|
||||||
|
'path': 'https://bijoe.example.com/visualization/2/iframe/?signature=123',
|
||||||
|
'name': 'second visualization (Y)',
|
||||||
|
'slug': 'second',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'data-url': 'https://bijoe.example.com/visualization/3/json/',
|
||||||
|
'path': 'https://bijoe.example.com/visualization/3/iframe/?signature=123',
|
||||||
|
'name': 'third visualization (X/Y)',
|
||||||
|
'slug': 'third',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'data-url': 'https://bijoe.example.com/visualization/4/json/',
|
||||||
|
'path': 'https://bijoe.example.com/visualization/4/iframe/?signature=123',
|
||||||
|
'name': 'fourth visualization (no axis)',
|
||||||
|
'slug': 'fourth',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def bijoe_mock(url, request):
|
||||||
|
if url.path == '/visualization/json/':
|
||||||
|
return {'content': json.dumps(VISUALIZATION_JSON), 'request': request, 'status_code': 200}
|
||||||
|
if url.path == '/visualization/1/json/':
|
||||||
|
response = {
|
||||||
|
'format': '1',
|
||||||
|
'data': [222, 134, 53],
|
||||||
|
'axis': {
|
||||||
|
'x_labels': ['web', 'mail', 'email']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
|
||||||
|
if url.path == '/visualization/2/json/':
|
||||||
|
response = {
|
||||||
|
'format': '1',
|
||||||
|
'data': [222, 134, 53],
|
||||||
|
'axis': {
|
||||||
|
'y_labels': ['web', 'mail', 'email']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
|
||||||
|
if url.path == '/visualization/3/json/':
|
||||||
|
response = {
|
||||||
|
'format': '1',
|
||||||
|
'data': [
|
||||||
|
[222, 134, 53],
|
||||||
|
[122, 114, 33],
|
||||||
|
],
|
||||||
|
'axis': {
|
||||||
|
'x_labels': ['web', 'mail', 'email'],
|
||||||
|
'y_labels': ['foo', 'bar'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
|
||||||
|
if url.path == '/visualization/4/json/':
|
||||||
|
response = {
|
||||||
|
'format': '1',
|
||||||
|
'data': 222,
|
||||||
|
'axis': {}
|
||||||
|
}
|
||||||
|
return {'content': json.dumps(response), 'request': request, 'status_code': 200}
|
||||||
|
|
||||||
|
|
||||||
|
def test_chartng_cell(app):
|
||||||
|
page = Page(title='One', slug='index')
|
||||||
|
page.save()
|
||||||
|
|
||||||
|
with override_settings(KNOWN_SERVICES={
|
||||||
|
'bijoe': {'plop': {'title': 'test', 'url': 'https://bijoe.example.com',
|
||||||
|
'secret': 'combo', 'orig': 'combo'}}}):
|
||||||
|
with HTTMock(bijoe_mock):
|
||||||
|
cell = ChartNgCell(page=page, order=1)
|
||||||
|
cell.data_reference = 'plop:example'
|
||||||
|
cell.save()
|
||||||
|
assert cell.cached_json == VISUALIZATION_JSON[0]
|
||||||
|
|
||||||
|
# bar
|
||||||
|
chart = cell.get_chart()
|
||||||
|
assert chart.__class__.__name__ == 'Bar'
|
||||||
|
assert chart.x_labels == ['web', 'mail', 'email']
|
||||||
|
assert chart.raw_series == [([222, 134, 53], {'title': ''})]
|
||||||
|
|
||||||
|
# horizontal bar
|
||||||
|
cell.chart_type = 'horizontal-bar'
|
||||||
|
chart = cell.get_chart()
|
||||||
|
assert chart.__class__.__name__ == 'HorizontalBar'
|
||||||
|
assert chart.x_labels == ['web', 'mail', 'email']
|
||||||
|
assert chart.raw_series == [([222, 134, 53], {'title': ''})]
|
||||||
|
|
||||||
|
# pie
|
||||||
|
cell.chart_type = 'pie'
|
||||||
|
chart = cell.get_chart()
|
||||||
|
assert chart.__class__.__name__ == 'Pie'
|
||||||
|
assert chart.x_labels == ['web', 'mail', 'email']
|
||||||
|
assert chart.raw_series == [
|
||||||
|
([222], {'title': u'web'}),
|
||||||
|
([134], {'title': u'mail'}),
|
||||||
|
([53], {'title': u'email'})
|
||||||
|
]
|
||||||
|
|
||||||
|
# data in Y
|
||||||
|
cell.chart_type = 'bar'
|
||||||
|
cell.data_reference = 'plop:second'
|
||||||
|
cell.save()
|
||||||
|
assert cell.cached_json == VISUALIZATION_JSON[1]
|
||||||
|
|
||||||
|
chart = cell.get_chart()
|
||||||
|
assert chart.x_labels == ['web', 'mail', 'email']
|
||||||
|
assert chart.raw_series == [([222, 134, 53], {'title': ''})]
|
||||||
|
|
||||||
|
# data in X/Y
|
||||||
|
cell.chart_type = 'bar'
|
||||||
|
cell.data_reference = 'plop:third'
|
||||||
|
cell.save()
|
||||||
|
assert cell.cached_json == VISUALIZATION_JSON[2]
|
||||||
|
|
||||||
|
chart = cell.get_chart()
|
||||||
|
assert chart.x_labels == ['web', 'mail', 'email']
|
||||||
|
assert chart.raw_series == [
|
||||||
|
([222, 134, 53], {'title': u'foo'}),
|
||||||
|
([122, 114, 33], {'title': u'bar'}),
|
||||||
|
]
|
||||||
|
|
||||||
|
# single data point
|
||||||
|
cell.chart_type = 'bar'
|
||||||
|
cell.data_reference = 'plop:fourth'
|
||||||
|
cell.save()
|
||||||
|
assert cell.cached_json == VISUALIZATION_JSON[3]
|
||||||
|
|
||||||
|
chart = cell.get_chart()
|
||||||
|
assert chart.x_labels == ['']
|
||||||
|
assert chart.raw_series == [([222], {'title': ''})]
|
||||||
|
|
||||||
|
def test_chartng_cell_view(app, normal_user):
|
||||||
|
page = Page(title='One', slug='index')
|
||||||
|
page.save()
|
||||||
|
|
||||||
|
with override_settings(KNOWN_SERVICES={
|
||||||
|
'bijoe': {'plop': {'title': 'test', 'url': 'https://bijoe.example.com',
|
||||||
|
'secret': 'combo', 'orig': 'combo'}}}):
|
||||||
|
with HTTMock(bijoe_mock):
|
||||||
|
cell = ChartNgCell(page=page, order=1, placeholder='content')
|
||||||
|
cell.data_reference = 'plop:example'
|
||||||
|
cell.save()
|
||||||
|
resp = app.get('/')
|
||||||
|
assert 'min-height: 250px' in resp.text
|
||||||
|
assert '/api/dataviz/graph/1/' in resp.text
|
||||||
|
|
||||||
|
resp = app.get('/api/dataviz/graph/1/?width=400')
|
||||||
|
assert resp.content_type == 'image/svg+xml'
|
||||||
|
|
||||||
|
page.public = False
|
||||||
|
page.save()
|
||||||
|
resp = app.get('/api/dataviz/graph/1/?width=400', status=403)
|
||||||
|
|
||||||
|
page.public = True
|
||||||
|
page.save()
|
||||||
|
group = Group(name='plop')
|
||||||
|
group.save()
|
||||||
|
cell.public = False
|
||||||
|
cell.groups = [group]
|
||||||
|
cell.save()
|
||||||
|
resp = app.get('/api/dataviz/graph/1/?width=400', status=403)
|
||||||
|
|
||||||
|
app = login(app, username='normal-user', password='normal-user')
|
||||||
|
resp = app.get('/api/dataviz/graph/1/?width=400', status=403)
|
||||||
|
|
||||||
|
normal_user.groups = [group]
|
||||||
|
normal_user.save()
|
||||||
|
resp = app.get('/api/dataviz/graph/1/?width=400', status=200)
|
||||||
|
|
||||||
|
# table visualization
|
||||||
|
cell.chart_type = 'table'
|
||||||
|
cell.save()
|
||||||
|
resp = app.get('/')
|
||||||
|
assert '<td>222</td>' in resp.body
|
||||||
|
|
||||||
|
|
||||||
|
def test_chartng_cell_manager(app, admin_user):
|
||||||
|
page = Page(title='One', slug='index')
|
||||||
|
page.save()
|
||||||
|
|
||||||
|
app = login(app)
|
||||||
|
|
||||||
|
with override_settings(KNOWN_SERVICES={
|
||||||
|
'bijoe': {'plop': {'title': 'test', 'url': 'https://bijoe.example.com',
|
||||||
|
'secret': 'combo', 'orig': 'combo'}}}):
|
||||||
|
with HTTMock(bijoe_mock):
|
||||||
|
cell = ChartNgCell(page=page, order=1, placeholder='content')
|
||||||
|
cell.data_reference = 'plop:example'
|
||||||
|
cell.save()
|
||||||
|
resp = app.get('/manage/pages/%s/' % page.id)
|
||||||
|
assert resp.form['cdataviz_chartngcell-%s-data_reference' % cell.id].options == [
|
||||||
|
(u'plop:example', True, u'example visualization (X)'),
|
||||||
|
(u'plop:fourth', False, u'fourth visualization (no axis)'),
|
||||||
|
(u'plop:second', False, u'second visualization (Y)'),
|
||||||
|
(u'plop:third', False, u'third visualization (X/Y)'),
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue