# 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 . 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