bijoe/bijoe/visualization/views.py

441 lines
17 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
import hashlib
import json
from django.conf import settings
from django.contrib import messages
from django.utils.encoding import force_bytes, force_text
from django.utils.text import slugify
from django.utils.translation import ungettext, ugettext as _
from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView
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 bijoe.utils import get_warehouses, import_site, export_site
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())
ctx['visualizations_exist'] = models.Visualization.objects.exists()
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 SaveAsVisualizationView(views.AuthorizationMixin, DetailView, CreateView):
model = models.Visualization
form_class = forms.VisualizationForm
template_name = 'bijoe/create_visualization.html'
success_url = '/visualization/{id}/'
def form_valid(self, form):
form.instance.parameters = self.get_object().parameters
return super(SaveAsVisualizationView, self).form_valid(form)
def get_initial(self):
return {
'name': '%s %s' % (self.get_object().name, _('(Copy)'))
}
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.items() 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 = path + settings.SECRET_KEY
signature = hashlib.sha1(force_bytes(signature)).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 = request.path + settings.SECRET_KEY
signature = hashlib.sha1(force_bytes(signature)).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 = path + settings.SECRET_KEY
sig = hashlib.sha1(force_bytes(sig)).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,)
points = row.measures[0].value or []
geojson.append({
'type': 'Feature',
'geometry': {
'type': 'MultiPoint',
'coordinates': [[coord for coord in point] for point in points],
},
'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,
})
class ExportVisualizationView(views.AuthorizationMixin, DetailView):
model = models.Visualization
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
json.dump({'visualizations': [self.get_object().export_json()]}, response, indent=2)
return response
class VisualizationsImportView(views.AuthorizationMixin, FormView):
form_class = forms.VisualizationsImportForm
template_name = 'bijoe/visualizations_import.html'
success_url = reverse_lazy('homepage')
def form_valid(self, form):
try:
visualizations_json = json.loads(
force_text(self.request.FILES['visualizations_json'].read()))
except ValueError:
form.add_error('visualizations_json', _('File is not in the expected JSON format.'))
return self.form_invalid(form)
results = import_site(visualizations_json)
if results.get('created') == 0 and results.get('updated') == 0:
messages.info(self.request, _('No visualizations were found.'))
else:
if results.get('created') == 0:
message1 = _('No visualization created.')
else:
message1 = ungettext(
'A visualization has been created.',
'%(count)d visualizations have been created.',
results['created']) % {'count': results['created']}
if results.get('updated') == 0:
message2 = _('No visualization updated.')
else:
message2 = ungettext(
'A visualization has been updated.',
'%(count)d visualizations have been updated.',
results['updated']) % {'count': results['updated']}
messages.info(self.request, u'%s %s' % (message1, message2))
return super(VisualizationsImportView, self).form_valid(form)
class VisualizationsExportView(views.AuthorizationMixin, View):
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
json.dump(export_site(), response, indent=2)
return response
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()
export_visualization = ExportVisualizationView.as_view()
save_as_visualization = SaveAsVisualizationView.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()
visualizations_import = VisualizationsImportView.as_view()
visualizations_export = VisualizationsExportView.as_view()
cube_iframe.mellon_no_passive = True
visualization_iframe.mellon_no_passive = True