296 lines
11 KiB
Python
296 lines
11 KiB
Python
# bijoe - BI dashboard
|
|
# 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 re
|
|
import json
|
|
import hashlib
|
|
import datetime
|
|
import decimal
|
|
from collections import OrderedDict
|
|
import copy
|
|
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils import six
|
|
from django.core.cache import cache
|
|
from django.http import Http404
|
|
from django.conf import settings
|
|
|
|
from ..utils import get_warehouses, human_join
|
|
from ..engine import Engine
|
|
from .ods import Workbook
|
|
|
|
|
|
class Visualization(object):
|
|
def __init__(self, cube, representation, measure, drilldown_x=None, drilldown_y=None,
|
|
filters=None, loop=None):
|
|
self.cube = cube
|
|
self.representation = representation
|
|
|
|
self.measure = measure
|
|
self.drilldown_x = drilldown_x
|
|
self.drilldown_y = drilldown_y
|
|
self.filters = filters or {}
|
|
self.loop = loop
|
|
|
|
@property
|
|
def drilldown(self):
|
|
drilldown = []
|
|
if self.loop:
|
|
drilldown.append(self.loop)
|
|
if self.drilldown_x:
|
|
drilldown.append(self.drilldown_x)
|
|
if self.drilldown_y:
|
|
drilldown.append(self.drilldown_y)
|
|
return drilldown
|
|
|
|
def to_json(self):
|
|
return {
|
|
'warehouse': self.cube.engine.warehouse.name,
|
|
'cube': self.cube.name,
|
|
'representation': self.representation,
|
|
'measure': self.measure and self.measure.name,
|
|
'drilldown_x': self.drilldown_x and self.drilldown_x.name,
|
|
'drilldown_y': self.drilldown_y and self.drilldown_y.name,
|
|
'filters': self.filters,
|
|
'loop': self.loop and self.loop.name,
|
|
}
|
|
|
|
def copy(self):
|
|
return Visualization(self.cube, self.representation, measure=self.measure,
|
|
drilldown_x=self.drilldown_x, drilldown_y=self.drilldown_y,
|
|
filters=copy.deepcopy(self.filters), loop=self.loop)
|
|
|
|
@classmethod
|
|
def from_json(cls, d, request=None):
|
|
for warehouse in get_warehouses(request=request):
|
|
if d['warehouse'] == warehouse.name:
|
|
break
|
|
else:
|
|
raise Http404('warehouse %s not found' % d['warehouse'])
|
|
engine = Engine(warehouse)
|
|
try:
|
|
cube = engine[d['cube']]
|
|
except KeyError:
|
|
raise Http404('cube %s not found' % d['cube'])
|
|
representation = d['representation']
|
|
measure = cube.measures[d['measure']]
|
|
drilldown_x = cube.dimensions[d['drilldown_x']] if 'drilldown_x' in d else None
|
|
drilldown_y = cube.dimensions[d['drilldown_y']] if 'drilldown_y' in d else None
|
|
filters = d.get('filters', {})
|
|
loop = d.get('loop')
|
|
if loop:
|
|
loop = cube.dimensions[loop]
|
|
return cls(cube, representation, measure, drilldown_x=drilldown_x, drilldown_y=drilldown_y,
|
|
filters=filters, loop=loop)
|
|
|
|
@classmethod
|
|
def from_form(cls, cube, form):
|
|
cleaned_data = form.cleaned_data
|
|
filters = {}
|
|
for kw, values in cleaned_data.iteritems():
|
|
if values and kw.startswith('filter__'):
|
|
dimension_name = kw[8:]
|
|
filters[dimension_name] = values
|
|
measure = cleaned_data.get('measure', [])
|
|
measure = measure and cube.measures[measure]
|
|
drilldown_x = cleaned_data.get('drilldown_x')
|
|
drilldown_x = drilldown_x and cube.dimensions[drilldown_x]
|
|
drilldown_y = cleaned_data.get('drilldown_y')
|
|
drilldown_y = drilldown_y and cube.dimensions[drilldown_y]
|
|
loop = cleaned_data.get('loop')
|
|
loop = loop and cube.dimensions[loop]
|
|
return cls(cube, cleaned_data['representation'],
|
|
measure,
|
|
drilldown_x=drilldown_x,
|
|
drilldown_y=drilldown_y,
|
|
filters=filters, loop=loop)
|
|
|
|
@property
|
|
def key(self):
|
|
keys = [self.cube.engine.warehouse.name, self.cube.name]
|
|
if self.loop:
|
|
keys.append(self.loop.name)
|
|
for kw, value in self.filters.iteritems():
|
|
if value is None:
|
|
continue
|
|
elif isinstance(value, (dict, list, tuple)):
|
|
# multiple values
|
|
if isinstance(value, dict):
|
|
value = value.items()
|
|
keys.append('$'.join([kw] + sorted(map(six.text_type, value))))
|
|
else:
|
|
# scalar values
|
|
keys.append(u'%s$%s' % (kw, six.text_type(value)))
|
|
keys += [dim.name for dim in self.drilldown]
|
|
keys += [self.measure.name]
|
|
key = '$'.join(v.encode('utf8') for v in keys)
|
|
return hashlib.md5(key).hexdigest()
|
|
|
|
def stringified(self):
|
|
data = self.cached()
|
|
for row in data:
|
|
for cell in row:
|
|
value = cell['value']
|
|
if cell['type'] == 'percent':
|
|
try:
|
|
value = ('%4.2f' % float(value)).replace('.', ',') + u' %'
|
|
except:
|
|
value = _('Not applicable')
|
|
elif value is not None and cell['type'] == 'duration':
|
|
s = ''
|
|
if value.days:
|
|
s += '%d jour(s)' % value.days
|
|
if value.seconds / 3600:
|
|
s += ' %d heure(s)' % (value.seconds / 3600)
|
|
if not s:
|
|
s = 'moins d\'1 heure'
|
|
value = s
|
|
cell['value'] = value
|
|
return data
|
|
|
|
def data(self):
|
|
return self.cube.query(self.filters.items(),
|
|
[dim.name for dim in self.drilldown],
|
|
[self.measure.name])
|
|
|
|
def cached(self):
|
|
key = self.key
|
|
data = cache.get(key)
|
|
if data is None:
|
|
data = list(self.data())
|
|
if settings.BIJOE_CACHE:
|
|
cache.set(key, data)
|
|
return data
|
|
|
|
def table(self):
|
|
table = []
|
|
if len(self.drilldown) == 2:
|
|
x_labels = [x.label for x in self.drilldown_x.members]
|
|
y_labels = [y.label for y in self.drilldown_y.members]
|
|
used_x_label = set()
|
|
used_y_label = set()
|
|
|
|
grid = {(x, y): None for x in x_labels for y in y_labels}
|
|
|
|
for row in self.stringified():
|
|
x_label = unicode(row[0]['value'])
|
|
y_label = unicode(row[1]['value'])
|
|
used_x_label.add(x_label)
|
|
used_y_label.add(y_label)
|
|
grid[(x_label, y_label)] = row[2]['value']
|
|
|
|
table.append([''] + [x for x in x_labels if x in used_x_label])
|
|
for y in y_labels:
|
|
if y not in used_y_label:
|
|
continue
|
|
table.append([y] + [grid[(x, y)] for x in x_labels if x in used_x_label])
|
|
if self.measure.expression.lower().startswith('count('):
|
|
# ajout des totaux horizontaux
|
|
table[0].append(_('Total'))
|
|
for row in table[1:]:
|
|
row.append(sum(v or 0 for v in row[1:]))
|
|
table.append([_('Total')])
|
|
for i in range(1, len(table[0])):
|
|
table[-1].append(sum([
|
|
row[i] or 0 for row in table[1:-1]]))
|
|
return table
|
|
elif self.drilldown_x:
|
|
table.append([self.drilldown_x.label])
|
|
table.append([self.measure.label])
|
|
for row in self.stringified():
|
|
table[0].append(row[0]['value'])
|
|
table[1].append(row[1]['value'])
|
|
elif self.drilldown_y:
|
|
table.append([self.drilldown_y.label, self.measure.label])
|
|
for row in self.stringified():
|
|
table.append([
|
|
row[0]['value'],
|
|
row[1]['value']
|
|
])
|
|
else:
|
|
value = self.stringified()[0][0]['value']
|
|
table.append([self.measure.label, value])
|
|
return table
|
|
|
|
def javascript(self):
|
|
l = []
|
|
l.append('var measure = %s;' % json.dumps(self.measure.to_json()))
|
|
l.append('var loop = %s;' % json.dumps(self.loop.to_json() if self.loop else None))
|
|
l.append('var drilldown = %s;' % json.dumps([dim.to_json() for dim in self.drilldown]))
|
|
l.append('var data = %s;' % json.dumps(self.json_data()))
|
|
return mark_safe('\n'.join(l))
|
|
|
|
def json_data(self):
|
|
json_data = []
|
|
for row in self.data():
|
|
coords = []
|
|
for cell in row[:len(self.drilldown)]:
|
|
coords.append(cell)
|
|
measures = []
|
|
for cell in row[len(self.drilldown):]:
|
|
if isinstance(cell['value'], decimal.Decimal):
|
|
cell['value'] = float(cell['value'])
|
|
if isinstance(cell['value'], datetime.timedelta):
|
|
cell['value'] = cell['value'].days + cell['value'].seconds / 86400.
|
|
measures.append(cell)
|
|
json_data.append({'coords': coords, 'measures': measures})
|
|
return json_data
|
|
|
|
def ods(self):
|
|
workbook = Workbook()
|
|
|
|
full_title = self.title()
|
|
|
|
for table in self:
|
|
sheet_name = re.sub('[^a-zA-Z ]', '', table.table_title)
|
|
sheet = workbook.add_sheet(sheet_name)
|
|
|
|
sheet.write(0, 0, full_title)
|
|
for j, row in enumerate(table.table()):
|
|
for i, value in enumerate(row):
|
|
sheet.write(j + 1, i, unicode(0 if value is None else value))
|
|
return workbook
|
|
|
|
def title(self):
|
|
l = []
|
|
if self.measure:
|
|
l.append(self.measure.label)
|
|
if self.drilldown_x:
|
|
l.append(self.drilldown_x.label)
|
|
if self.drilldown_y:
|
|
l.append(self.drilldown_y.label)
|
|
if self.loop:
|
|
l.append(self.loop.label)
|
|
return u', '.join(l)
|
|
|
|
def __iter__(self):
|
|
if self.loop:
|
|
members = list(self.loop.members)
|
|
d = list(self.cube.query(self.filters.items(), [self.loop.name],
|
|
[self.measure.name]))
|
|
names = [unicode(x[0]['value']) for x in d]
|
|
members = [m for m in members if unicode(m.label) in names]
|
|
for member in members:
|
|
table = self.copy()
|
|
table.loop = None
|
|
table.filters[self.loop.name] = [member.id]
|
|
table.table_title = unicode(member.label)
|
|
yield table
|
|
else:
|
|
self.table_title = self.title()
|
|
yield self
|