361 lines
14 KiB
Python
361 lines
14 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/>.
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
from collections import OrderedDict
|
|
import hashlib
|
|
import json
|
|
|
|
from django.conf import settings
|
|
from django.utils.encoding import force_text
|
|
from django.utils.text import slugify
|
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
|
from django.views.generic.list import MultipleObjectMixin
|
|
from django.views.generic import DetailView, ListView, View, TemplateView
|
|
from django.shortcuts import redirect
|
|
from django.core.urlresolvers import reverse, reverse_lazy
|
|
from django.http import HttpResponse, Http404
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
|
|
|
from rest_framework import generics
|
|
from rest_framework.response import Response
|
|
|
|
from ..utils import get_warehouses
|
|
from ..engine import Engine
|
|
from . import models, forms, signature
|
|
from .utils import Visualization
|
|
from .. import views
|
|
|
|
|
|
class WarehouseView(views.AuthorizationMixin, TemplateView):
|
|
template_name = 'bijoe/warehouse.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super(WarehouseView, self).get_context_data(**kwargs)
|
|
try:
|
|
warehouse = [warehouse for warehouse in get_warehouses()
|
|
if warehouse.name == self.kwargs['warehouse']][0]
|
|
except IndexError:
|
|
raise Http404
|
|
ctx['warehouse'] = Engine(warehouse)
|
|
ctx['cubes'] = sorted(ctx['warehouse'].cubes, key=lambda cube: cube.label.strip().lower())
|
|
return ctx
|
|
|
|
|
|
class CubeDisplayMixin(object):
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super(CubeDisplayMixin, self).get_context_data(**kwargs)
|
|
ctx['warehouse'] = self.warehouse
|
|
ctx['cube'] = self.cube
|
|
ctx['visualization'] = self.visualization
|
|
return ctx
|
|
|
|
|
|
class CubeMixin(object):
|
|
def visualization(self, request, cube):
|
|
self.form = forms.CubeForm(cube=self.cube, data=request.GET or request.POST)
|
|
if self.form.is_valid():
|
|
return Visualization.from_form(self.cube, self.form)
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
try:
|
|
self.warehouse = Engine([warehouse for warehouse in get_warehouses()
|
|
if warehouse.name == self.kwargs['warehouse']][0])
|
|
except IndexError:
|
|
raise Http404
|
|
try:
|
|
self.cube = self.warehouse[self.kwargs['cube']]
|
|
except KeyError:
|
|
raise Http404
|
|
self.visualization = self.visualization(request, cube)
|
|
return super(CubeMixin, self).dispatch(request, *args, **kwargs)
|
|
|
|
|
|
class CubeView(views.AuthorizationMixin, CubeDisplayMixin, CubeMixin, TemplateView):
|
|
template_name = 'bijoe/cube.html'
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
if 'ods' in self.request.POST:
|
|
return self.ods(self.visualization)
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super(CubeView, self).get_context_data(**kwargs)
|
|
ctx['form'] = self.form
|
|
return ctx
|
|
|
|
|
|
class CreateVisualizationView(views.AuthorizationMixin, CubeMixin, CreateView):
|
|
model = models.Visualization
|
|
form_class = forms.VisualizationForm
|
|
template_name = 'bijoe/create_visualization.html'
|
|
success_url = '/visualization/{id}/'
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if not self.visualization:
|
|
return redirect('homepage')
|
|
return super(CreateVisualizationView, self).get(request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
if not self.visualization:
|
|
return redirect('homepage')
|
|
form.instance.parameters = self.visualization.to_json()
|
|
return super(CreateVisualizationView, self).form_valid(form)
|
|
|
|
|
|
class VisualizationView(views.AuthorizationMixin, CubeDisplayMixin, DetailView):
|
|
model = models.Visualization
|
|
template_name = 'bijoe/visualization.html'
|
|
|
|
def get_object(self):
|
|
named_visualization = super(VisualizationView, self).get_object()
|
|
if not hasattr(self, 'visualization'):
|
|
self.visualization = Visualization.from_json(named_visualization.parameters)
|
|
self.cube = self.visualization.cube
|
|
self.warehouse = self.cube.engine
|
|
return named_visualization
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
named_visualization = self.get_object()
|
|
if 'ods' in self.request.POST:
|
|
return self.ods(self.visualization)
|
|
form = forms.CubeForm(cube=self.cube, data=request.POST)
|
|
if form.is_valid():
|
|
self.visualization = Visualization.from_form(self.cube, form)
|
|
named_visualization.parameters = self.visualization.to_json()
|
|
if 'save' in request.POST:
|
|
named_visualization.save()
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super(VisualizationView, self).get_context_data(**kwargs)
|
|
initial = {
|
|
'representation': self.visualization.representation,
|
|
'measure': self.visualization.measure.name,
|
|
'loop': self.visualization.loop and self.visualization.loop.name,
|
|
'drilldown_x': self.visualization.drilldown_x and self.visualization.drilldown_x.name,
|
|
'drilldown_y': self.visualization.drilldown_y and self.visualization.drilldown_y.name,
|
|
}
|
|
for key, value in self.visualization.filters.iteritems() or []:
|
|
if isinstance(value, list):
|
|
value = tuple(value)
|
|
initial['filter__%s' % key] = value
|
|
ctx['form'] = forms.CubeForm(cube=self.cube, initial=initial)
|
|
path = reverse('visualization-iframe', args=self.args, kwargs=self.kwargs)
|
|
signature = hashlib.sha1(path + settings.SECRET_KEY).hexdigest()
|
|
path += '?signature=' + signature
|
|
ctx['iframe_url'] = path
|
|
return ctx
|
|
|
|
|
|
class SignatureAuthorizationMixin(views.AuthorizationMixin):
|
|
def authorize(self, request):
|
|
if request.user.is_authenticated() and request.user.is_superuser:
|
|
return True
|
|
if 'signature' in request.GET:
|
|
signature = hashlib.sha1(request.path + settings.SECRET_KEY).hexdigest()
|
|
if request.GET['signature'] == signature:
|
|
return True
|
|
return False
|
|
|
|
|
|
class VisualizationIframeView(SignatureAuthorizationMixin, VisualizationView):
|
|
template_name = 'bijoe/cube_raw.html'
|
|
|
|
|
|
class DeleteVisualizationView(views.AuthorizationMixin, DeleteView):
|
|
model = models.Visualization
|
|
template_name = 'bijoe/delete_visualization.html'
|
|
success_url = reverse_lazy('homepage')
|
|
|
|
|
|
class VisualizationsView(views.AuthorizationMixin, ListView):
|
|
template_name = 'bijoe/visualizations.html'
|
|
model = models.Visualization
|
|
context_object_name = 'visualizations'
|
|
paginate_by = settings.PAGE_LENGTH
|
|
|
|
def get_queryset(self):
|
|
return self.model.all_visualizations()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super(VisualizationsView, self).get_context_data(**kwargs)
|
|
ctx['request'] = self.request
|
|
return ctx
|
|
|
|
|
|
class RenameVisualization(views.AuthorizationMixin, UpdateView):
|
|
model = models.Visualization
|
|
form_class = forms.VisualizationForm
|
|
template_name = 'bijoe/rename_visualization.html'
|
|
success_url = '/visualization/{id}/'
|
|
|
|
|
|
class VisualizationsJSONView(MultipleObjectMixin, View):
|
|
model = models.Visualization
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if not request.user.is_authenticated() or not request.user.is_superuser:
|
|
known_services = getattr(settings, 'KNOWN_SERVICES', [])
|
|
if known_services:
|
|
key = None
|
|
for l in known_services.itervalues():
|
|
for service in l.itervalues():
|
|
if service['verif_orig'] == request.GET.get('orig', ''):
|
|
key = service['secret']
|
|
if key is None or not signature.check_query(request.META['QUERY_STRING'], key):
|
|
raise PermissionDenied('signature is missing or wrong')
|
|
data = []
|
|
for visualization in self.get_queryset():
|
|
path = reverse('visualization-iframe', kwargs={'pk': visualization.pk})
|
|
sig = hashlib.sha1(path + settings.SECRET_KEY).hexdigest()
|
|
path += '?signature=' + sig
|
|
data_uri = reverse('visualization-json', kwargs={'pk': visualization.pk})
|
|
data.append({
|
|
'name': visualization.name,
|
|
'slug': visualization.slug,
|
|
'path': request.build_absolute_uri(path),
|
|
'data-url': request.build_absolute_uri(data_uri),
|
|
})
|
|
response = HttpResponse(content_type='application/json')
|
|
response.write(json.dumps(data))
|
|
return response
|
|
|
|
|
|
class CubeIframeView(CubeView):
|
|
template_name = 'bijoe/cube_raw.html'
|
|
|
|
|
|
class VisualizationODSView(views.AuthorizationMixin, DetailView):
|
|
model = models.Visualization
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
instance = self.get_object()
|
|
visualization = Visualization.from_json(instance.parameters)
|
|
response = HttpResponse(content_type='application/vnd.oasis.opendocument.spreadsheet')
|
|
response['Content-Disposition'] = 'attachment; filename=%s.ods' % slugify(instance.name)
|
|
workbook = visualization.ods()
|
|
workbook.save(response)
|
|
return response
|
|
|
|
|
|
class VisualizationGeoJSONView(generics.GenericAPIView):
|
|
permission_classes = ()
|
|
queryset = models.Visualization.objects.all()
|
|
|
|
def get(self, request, pk, format=None):
|
|
instance = self.get_object()
|
|
visualization = Visualization.from_json(instance.parameters)
|
|
visualization.measure = visualization.cube.measures['geolocation']
|
|
geojson = []
|
|
|
|
for row in visualization.data():
|
|
properties = {}
|
|
for cell in row.dimensions:
|
|
properties[cell.dimension.label] = '%s' % (cell,)
|
|
geojson.append({
|
|
'type': 'Feature',
|
|
'geometry': {
|
|
'type': 'MultiPoint',
|
|
'coordinates': [[coord for coord in point] for point in row.measures[0].value],
|
|
},
|
|
'properties': properties,
|
|
})
|
|
return Response(geojson)
|
|
|
|
|
|
class VisualizationJSONView(generics.GenericAPIView):
|
|
permission_classes = ()
|
|
queryset = models.Visualization.objects.all()
|
|
|
|
def get(self, request, pk, format=None):
|
|
def cell_value(cell):
|
|
if cell.measure.type == 'duration' and cell.value is not None:
|
|
return cell.value.total_seconds()
|
|
return cell.value
|
|
|
|
def labels(axis):
|
|
return [x.label.strip() for x in axis]
|
|
|
|
instance = self.get_object()
|
|
loop = []
|
|
all_visualizations = Visualization.from_json(instance.parameters)
|
|
for visualization in all_visualizations:
|
|
drilldowns = visualization.drilldown
|
|
if len(drilldowns) == 2:
|
|
(x_axis, y_axis), grid = visualization.table_2d()
|
|
axis = {
|
|
'x_labels': labels(x_axis),
|
|
'y_labels': labels(y_axis),
|
|
}
|
|
|
|
data = []
|
|
for y in y_axis:
|
|
data.append([cell_value(grid[(x.id, y.id)]) for x in x_axis])
|
|
elif len(drilldowns) == 1:
|
|
x_axis, grid = visualization.table_1d()
|
|
if visualization.drilldown_x:
|
|
axis = {'x_labels': labels(x_axis)}
|
|
else:
|
|
axis = {'y_labels': labels(x_axis)}
|
|
data = [cell_value(grid[x.id]) for x in x_axis]
|
|
elif len(drilldowns) == 0:
|
|
data = cell_value(visualization.data()[0].measures[0])
|
|
axis = {}
|
|
loop.append({
|
|
'data': data,
|
|
'axis': axis
|
|
})
|
|
|
|
if not all_visualizations.loop:
|
|
data = loop[0]['data']
|
|
axis = loop[0]['axis']
|
|
else:
|
|
axis = loop[0]['axis']
|
|
axis['loop'] = [x.label for x in all_visualizations.loop.members(all_visualizations.filters.items())]
|
|
data = [x['data'] for x in loop]
|
|
|
|
unit = 'seconds' if all_visualizations.measure.type == 'duration' else None
|
|
measure = all_visualizations.measure.type
|
|
|
|
return Response({
|
|
'data': data,
|
|
'axis': axis,
|
|
'format': '1',
|
|
'unit': unit, # legacy, prefer measure.
|
|
'measure': measure,
|
|
})
|
|
|
|
|
|
warehouse = WarehouseView.as_view()
|
|
cube = CubeView.as_view()
|
|
cube_iframe = xframe_options_exempt(CubeIframeView.as_view())
|
|
visualizations = VisualizationsView.as_view()
|
|
visualizations_json = VisualizationsJSONView.as_view()
|
|
create_visualization = CreateVisualizationView.as_view()
|
|
delete_visualization = DeleteVisualizationView.as_view()
|
|
rename_visualization = RenameVisualization.as_view()
|
|
visualization = VisualizationView.as_view()
|
|
visualization_iframe = xframe_options_exempt(VisualizationIframeView.as_view())
|
|
visualization_geojson = VisualizationGeoJSONView.as_view()
|
|
visualization_ods = VisualizationODSView.as_view()
|
|
visualization_json = VisualizationJSONView.as_view()
|
|
|
|
cube_iframe.mellon_no_passive = True
|
|
visualization_iframe.mellon_no_passive = True
|