bijoe/bijoe/visualization/views.py

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