# combo - content management system # Copyright (C) 2014-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 . import copy import os from django.core.urlresolvers import reverse from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings from jsonfield import JSONField import pygal from combo.data.models import CellBase from combo.data.library import register_cell_class from combo.utils import get_templated_url, requests @register_cell_class class Gauge(CellBase): title = models.CharField(_('Title'), max_length=150, blank=True, null=True) url = models.CharField(_('URL'), max_length=150, blank=True, null=True) data_source = models.CharField(_('Data Source'), max_length=150, blank=True, null=True) jsonp_data_source = models.BooleanField(_('Use JSONP to get data'), default=True) max_value = models.PositiveIntegerField(_('Max Value'), blank=True, null=True) template_name = 'combo/gauge-cell.html' class Media: js = ('js/gauge.min.js', 'js/combo.gauge.js') class Meta: verbose_name = _('Gauge') def get_additional_label(self): return self.title def is_relevant(self, context): return bool(self.data_source) def get_cell_extra_context(self, context): if self.jsonp_data_source: data_source_url = get_templated_url(self.data_source) else: data_source_url = reverse('combo-ajax-gauge-count', kwargs={'cell': self.id}) return {'cell': self, 'title': self.title, 'url': get_templated_url(self.url) if self.url else None, 'max_value': self.max_value, 'data_source_url': data_source_url, 'jsonp': self.jsonp_data_source, } @register_cell_class class ChartCell(CellBase): template_name = 'combo/dataviz-chart.html' title = models.CharField(_('Title'), max_length=150, blank=True, null=True) url = models.URLField(_('URL'), max_length=250, blank=True, null=True) class Meta: verbose_name = _('Chart (legacy)') @classmethod def is_enabled(self): return settings.LEGACY_CHART_CELL_ENABLED and hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('bijoe') def get_default_form_class(self): from .forms import ChartForm return ChartForm def get_additional_label(self): if self.title: return self.title return '' def get_cell_extra_context(self, context): context = super(ChartCell, self).get_cell_extra_context(context) context['title'] = self.title context['url'] = self.url 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 is_relevant(self, context=None): return bool(self.data_reference) 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('', '
') 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