allow saving cube display and naming them (fixes #12969)
This commit is contained in:
parent
25c8566c8b
commit
4ca7b774f6
|
@ -4,3 +4,4 @@ include tox.ini
|
|||
recursive-include tests *.py
|
||||
recursive-include bijoe/templates *.html
|
||||
recursive-include bijoe/static *.js *.css
|
||||
recursive-include bijoe/locale *.po *.mo
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
# bijoe - BI dashboard
|
||||
# Copyright (C) 2016 Entr'ouvert
|
||||
# This file is distributed under the same license as the bijoe package.
|
||||
# Benjamin Dauvergne <bdauvergne@entrouvert.com>, 2016.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: bijoe 0.x\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-08-26 14:57+0000\n"
|
||||
"PO-Revision-Date: 2016-08-26 16:50\n"
|
||||
"Last-Translator: Benjamin Dauvergne <bdauvergne@entrouvert.com>\n"
|
||||
"Language-Team: fr <fr@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: templates/bijoe/base.html:10 templates/bijoe/base.html.py:14
|
||||
#: templates/bijoe/warehouse.html:5 views.py:79
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiques"
|
||||
|
||||
#: templates/bijoe/create_visualization.html:6
|
||||
msgid "Save visualization"
|
||||
msgstr "Enregistrer une visualisation"
|
||||
|
||||
#: templates/bijoe/create_visualization.html:17 templates/bijoe/cube.html:59
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: templates/bijoe/create_visualization.html:18
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: templates/bijoe/cube.html:20 templates/bijoe/homepage.html:6
|
||||
#: templates/bijoe/visualization.html:9 templates/bijoe/warehouse.html:10
|
||||
msgid "Homepage"
|
||||
msgstr "Accueil"
|
||||
|
||||
#: templates/bijoe/cube.html:57 templates/bijoe/visualization.html:17
|
||||
msgid "URL for IFRAME"
|
||||
msgstr "URL pour IFRAME"
|
||||
|
||||
#: templates/bijoe/cube.html:62
|
||||
msgid "Please choose some measures and groupings."
|
||||
msgstr "Veuillez choisir des mesures et des regroupements."
|
||||
|
||||
#: templates/bijoe/cube_table.html:18 templates/bijoe/cube_table.html.py:20
|
||||
msgid "None"
|
||||
msgstr "Aucun(e)"
|
||||
|
||||
#: templates/bijoe/homepage.html:11 templates/bijoe/visualizations.html:6
|
||||
msgid "Visualizations"
|
||||
msgstr "Visualisation"
|
||||
|
||||
#: templates/bijoe/homepage.html:20
|
||||
msgid "Data sources"
|
||||
msgstr "Sources de données"
|
||||
|
||||
#: templates/bijoe/rename_visualization.html:6
|
||||
msgid "Rename visualization"
|
||||
msgstr "Renommer une visualisation"
|
||||
|
||||
#: templates/bijoe/rename_visualization.html:10
|
||||
#: templates/bijoe/visualization.html:16
|
||||
msgid "Rename"
|
||||
msgstr "Renommer"
|
||||
|
||||
#: templates/bijoe/visualizations.html:12 templates/bijoe/warehouse.html:18
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: templates/bijoe/warehouse.html:19
|
||||
msgid "Fact count"
|
||||
msgstr "Nombre de faits"
|
||||
|
||||
#: utils.py:30
|
||||
#, python-brace-format
|
||||
msgid "{0} and {1}"
|
||||
msgstr "{0} et {1}"
|
||||
|
||||
#: views.py:44
|
||||
msgid "You must be superuser"
|
||||
msgstr "Vous devez être super-utilisateur"
|
||||
|
||||
#: visualization/forms.py:50
|
||||
msgid "start"
|
||||
msgstr "début"
|
||||
|
||||
#: visualization/forms.py:52
|
||||
msgid "end"
|
||||
msgstr "fin"
|
||||
|
||||
#: visualization/forms.py:83
|
||||
msgid "Presentation"
|
||||
msgstr "Représentation"
|
||||
|
||||
#: visualization/forms.py:84
|
||||
msgid "table"
|
||||
msgstr "tableau"
|
||||
|
||||
#: visualization/forms.py:85
|
||||
msgid "chart"
|
||||
msgstr "graphique"
|
||||
|
||||
#: visualization/forms.py:111
|
||||
msgid "Group by"
|
||||
msgstr "Regroupement(s)"
|
||||
|
||||
#: visualization/forms.py:117
|
||||
msgid "Measures"
|
||||
msgstr "Mesure(s)"
|
||||
|
||||
#: visualization/models.py:23
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: visualization/models.py:24
|
||||
msgid "parameters"
|
||||
msgstr "paramètres"
|
||||
|
||||
#: visualization/models.py:28
|
||||
msgid "visualization"
|
||||
msgstr "visualisation"
|
||||
|
||||
#: visualization/models.py:29
|
||||
msgid "visualizations"
|
||||
msgstr "visualisations"
|
||||
|
||||
#: visualization/utils.py:98
|
||||
msgid "Not applicable"
|
||||
msgstr "Non applicable"
|
||||
|
||||
#: visualization/utils.py:180
|
||||
#, python-brace-format
|
||||
msgid "{0} by {1}"
|
||||
msgstr "{0} par {1}"
|
|
@ -42,6 +42,7 @@ INSTALLED_APPS = (
|
|||
'django_select2',
|
||||
'gadjo',
|
||||
'bijoe',
|
||||
'bijoe.visualization',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
|
@ -75,7 +76,7 @@ DATABASES = {
|
|||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
/* Table of forms */
|
||||
.bijoe-form-label {
|
||||
.main .bijoe-visualization-name, .main .bijoe-form-label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -30,10 +30,11 @@
|
|||
|
||||
/* make a form a column on the right if not on mobile */
|
||||
@media screen and (min-width: 800px) {
|
||||
form {
|
||||
.cube-form {
|
||||
float: right;
|
||||
width: 20em;
|
||||
padding-left: 2em;
|
||||
clear: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,10 +46,10 @@ input[type=date] {
|
|||
select {
|
||||
width: 100%;
|
||||
}
|
||||
form p { /* reduce spacing between fields */
|
||||
.cube-form p { /* reduce spacing between fields */
|
||||
margin-bottom: 0.5ex;
|
||||
}
|
||||
form h3 { /* reduce spacing between field groups headers */
|
||||
.cube-form h3 { /* reduce spacing between field groups headers */
|
||||
margin-top: 0.5ex;
|
||||
margin-bottom: 0.5ex;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
{% extends "bijoe/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Save visualization" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
</div>
|
||||
{% block buttons %}
|
||||
<div class="buttons">
|
||||
<button>{% block main-button-name %}{% trans "Save" %}{% endblock %}</button>
|
||||
<a class="cancel" href="{% url 'homepage' %}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -17,14 +17,14 @@
|
|||
{% url 'homepage' as homepage_url %}
|
||||
{% url 'warehouse' warehouse=warehouse.name as warehouse_url %}
|
||||
|
||||
<a href="{{ homepage_url }}">{% trans "Accueil" %}</a>
|
||||
<a href="{{ homepage_url }}">{% trans "Homepage" %}</a>
|
||||
<a href="{{ warehouse_url }}">{{ warehouse.label }}</a>
|
||||
<a>{{ cube.label }}</a>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<form class="cube-form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="ODS" name="ods" id="ods"/>
|
||||
<h3>Représentation</h3>
|
||||
|
@ -40,20 +40,26 @@
|
|||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
<input type="submit">
|
||||
</form>
|
||||
<div id="data">
|
||||
{% if data %}
|
||||
{% if representation == 'table' %}
|
||||
{% if visualization %}
|
||||
{% if visualization.representation == 'table' %}
|
||||
{% include "bijoe/cube_table.html" %}
|
||||
{% else %}
|
||||
{% include "bijoe/cube_chart.html" %}
|
||||
{% endif %}
|
||||
{% if data_title %}
|
||||
<a href="?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}" title="{{ data_title }}" class="bijoe-button">URL</a>
|
||||
<a href="iframe/?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}" title="{{ data_title }}" class="bijoe-button">IFRAME</a>
|
||||
{% endif %}
|
||||
{% block actions %}
|
||||
<a href="?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}" title="{{
|
||||
visualization.title }}" class="bijoe-button">URL</a>
|
||||
|
||||
<a href="iframe/?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}"
|
||||
title="{{ visualization.title }}" class="bijoe-button">{% trans "URL for IFRAME" %}</a>
|
||||
<a rel="popup" href="{% url "create-visualization" warehouse=warehouse.name cube=cube.name %}?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}"
|
||||
title="{{ visualization.title }}" class="bijoe-button">{% trans "Save" %}</a>
|
||||
{% endblock %}
|
||||
{% else %}
|
||||
<div class="big-msg-info">Veuillez choisir des mesures et des regroupements</div>
|
||||
<div class="big-msg-info">{% trans "Please choose some measures and groupings." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{% for measure in measures %}
|
||||
{% for measure in visualization.measures %}
|
||||
<a href="#" target="none" class="bijoe-button bijoe-png-button">PNG</a>
|
||||
<canvas id="canvas-{{ measure.name }}"></canvas>
|
||||
<script>
|
||||
|
|
|
@ -14,16 +14,15 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="data">
|
||||
{% if data %}
|
||||
{% if representation == 'table' %}
|
||||
{% include "bijoe/cube_table.html" %}
|
||||
{% else %}
|
||||
{% include "bijoe/cube_chart.html" %}
|
||||
{% if visualization %}
|
||||
{% if visualization.representation == 'table' %}
|
||||
{% include "bijoe/cube_table.html" %}
|
||||
{% else %}
|
||||
{% include "bijoe/cube_chart.html" %}
|
||||
{% endif %}
|
||||
<a href="?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}" title="{{
|
||||
visualization.title }}" class="bijoe-button">URL</a>
|
||||
{% endif %}
|
||||
{% if data_title %}
|
||||
<a href="?{% firstof view.request.POST.urlencode view.request.GET.urlencode %}" title="{{ data_title }}" class="bijoe-button">URL</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,22 +2,22 @@
|
|||
|
||||
<table class="bijoe-table main">
|
||||
<thead>
|
||||
{% for dimension in drilldown %}
|
||||
{% for dimension in visualization.drilldown %}
|
||||
<th class="bijoe-drilldown"><span>{{ dimension.label.capitalize }}</span></th>
|
||||
{% endfor %}
|
||||
{% for measure in measures %}
|
||||
{% for measure in visualization.measures %}
|
||||
<th class="bijoe-measure"><span>{{ measure.label.capitalize }}</span></th>
|
||||
{% endfor %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in grouped_data %}
|
||||
{% for row in visualization.grouped_data %}
|
||||
<tr>
|
||||
{% for cell, count in row %}
|
||||
<td {% if count > 1 %}rowspan="{{ count }}"{% endif %} {% if count > 0 %}class="bijoe-drilldown"{% elif count == 0 %}class="bijoe-measure"{% endif %}>
|
||||
{% if count == 0 %}
|
||||
{% if cell.value == None %}{% trans "Aucun(e)" %}{% else %}{{ cell.value }}{% endif %}
|
||||
{% if cell.value == None %}{% trans "None" %}{% else %}{{ cell.value }}{% endif %}
|
||||
{% else %}
|
||||
{% if cell == None %}{% trans "Aucun(e)" %}{% else %}{{ cell }}{% endif %}
|
||||
{% if cell == None %}{% trans "None" %}{% else %}{{ cell }}{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
|
|
@ -7,10 +7,22 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<ul class="bijoe-warehouses">
|
||||
{% for warehouse in warehouses %}
|
||||
{% url 'warehouse' warehouse=warehouse.name as warehouse_url %}
|
||||
<li><a href="{{ warehouse_url }}">{{ warehouse.label }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if visualizations %}
|
||||
<h2>{% trans "Visualizations" %}</h2>
|
||||
<ul class="bijoe-visualizations">
|
||||
{% for visualization in visualizations %}
|
||||
{% url 'visualization' pk=visualization.pk as visualization_url %}
|
||||
<li><a href="{{ visualization_url }}">{{ visualization.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if warehouses %}
|
||||
<h2>{% trans "Data sources" %}</h2>
|
||||
<ul class="bijoe-warehouses">
|
||||
{% for warehouse in warehouses %}
|
||||
{% url 'warehouse' warehouse=warehouse.name as warehouse_url %}
|
||||
<li><a href="{{ warehouse_url }}">{{ warehouse.label }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "bijoe/create_visualization.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Rename visualization" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block main-button-name %}
|
||||
{% trans "Rename" %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "bijoe/cube.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{% url 'homepage' as homepage_url %}
|
||||
{% url 'warehouse' warehouse=warehouse.name as warehouse_url %}
|
||||
{% url 'cube' warehouse=warehouse.name cube=cube.name as cube_url %}
|
||||
|
||||
<a href="{{ homepage_url }}">{% trans "Homepage" %}</a>
|
||||
<a href="{{ warehouse_url }}">{{ warehouse.label }}</a>
|
||||
<a href="{{ cube_url }}">{{ cube.label }}</a>
|
||||
<a>{{ object.name }}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
<a rel="popup" class="bijoe-button" href="{% url "rename-visualization" pk=object.pk %}">{% trans "Rename" %}</a>
|
||||
<a href="{{ iframe_url }}" class="bijoe-button">{% trans "URL for IFRAME" %}</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "bijoe/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a>{% trans "Visualizations" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<table class="main">
|
||||
<thead>
|
||||
<th class="bijoe-visualization-name">{% trans "Name" %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for visualization in object_list %}
|
||||
<tr>
|
||||
<td class="bijoe-visualization-name">
|
||||
<a href="{% url "visualization" pk=visualization.pk %}">{{ visualization.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -2,7 +2,7 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% block page-title %}
|
||||
{% trans "Statistiques" %} - {{ warehouse.label }}
|
||||
{% trans "Statistics" %} - {{ warehouse.label }}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
|
@ -15,8 +15,8 @@
|
|||
<table class="bijoe-table bijoe-form-table main">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="bijoe-form-label">{% trans "Label" %}</th>
|
||||
<th class="bijoe-form-count">{% trans "Nombre de demandes" %}</th></tr>
|
||||
<th class="bijoe-form-label">{% trans "Name" %}</th>
|
||||
<th class="bijoe-form-count">{% trans "Fact count" %}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cube in cubes %}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# 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 django import template
|
||||
|
||||
register = template.Library()
|
||||
|
|
|
@ -27,9 +27,7 @@ urlpatterns = patterns(
|
|||
url(r'^accounts/login/$', views.login, name='login'),
|
||||
url(r'^accounts/logout/$', views.logout, name='logout'),
|
||||
url(r'^manage/menu.json$', views.menu_json, name='menu-json'),
|
||||
url(r'^(?P<warehouse>[^/]*)/$', views.warehouse, name='warehouse'),
|
||||
url(r'^(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/$', views.cube, name='cube'),
|
||||
url(r'^(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/iframe/$', views.cube_iframe, name='cube-iframe'),
|
||||
url(r'^visualization/', include('bijoe.visualization.urls')),
|
||||
)
|
||||
|
||||
if 'mellon' in settings.INSTALLED_APPS:
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
# 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 os
|
||||
import glob
|
||||
import json
|
||||
|
@ -27,4 +43,4 @@ def human_join(l):
|
|||
return l[0]
|
||||
if len(l) > 2:
|
||||
l = u', '.join(l[:-1]), l[-1]
|
||||
return _(u'{0} et {1}').format(l[0], l[1])
|
||||
return _(u'{0} and {1}').format(l[0], l[1])
|
||||
|
|
226
bijoe/views.py
226
bijoe/views.py
|
@ -15,41 +15,40 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import datetime
|
||||
import decimal
|
||||
import urllib
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
|
||||
from django.shortcuts import resolve_url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.generic import TemplateView, FormView, View
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import TemplateView, View
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.core.cache import cache
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
try:
|
||||
from mellon.utils import get_idps
|
||||
except ImportError:
|
||||
get_idps = lambda: []
|
||||
|
||||
from .utils import get_warehouses, human_join
|
||||
from .utils import get_warehouses
|
||||
from .engine import Engine
|
||||
from .forms import CubeForm
|
||||
from .ods import Workbook
|
||||
from .visualization.models import Visualization
|
||||
|
||||
|
||||
class AuthorizationMixin(object):
|
||||
def authorize(self, request):
|
||||
if request.user.is_authenticated():
|
||||
if not request.user.is_superuser:
|
||||
raise PermissionDenied(_('You must be superuser'))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if self.request.user.is_authenticated():
|
||||
if self.request.user.is_superuser:
|
||||
return super(AuthorizationMixin, self).dispatch(request, *args, **kwargs)
|
||||
return render(request, 'bijoe/unauthorized.html', status=403)
|
||||
if self.authorize(request):
|
||||
return super(AuthorizationMixin, self).dispatch(request, *args, **kwargs)
|
||||
else:
|
||||
return redirect_to_login(request.build_absolute_uri())
|
||||
|
||||
|
@ -59,203 +58,26 @@ class HomepageView(AuthorizationMixin, TemplateView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(HomepageView, self).get_context_data(**kwargs)
|
||||
ctx['visualizations'] = Visualization.objects.all()
|
||||
ctx['warehouses'] = sorted((Engine(w) for w in get_warehouses(self.request)),
|
||||
key=lambda w: w.label)
|
||||
return ctx
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
ctx = self.get_context_data()['warehouses']
|
||||
if len(ctx) == 1:
|
||||
return HttpResponseRedirect(reverse('warehouse', kwargs={'warehouse': ctx[0].name}),
|
||||
ctx = self.get_context_data()
|
||||
if not len(ctx['visualizations']) and len(ctx['warehouses']) == 1:
|
||||
return HttpResponseRedirect(reverse('warehouse',
|
||||
kwargs={'warehouse': ctx['warehouses'][0].name}),
|
||||
status=307)
|
||||
return super(HomepageView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class WarehouseView(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(self.request)
|
||||
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 CubeMixin(object):
|
||||
def get_key(self, cleaned_data, stringify=True):
|
||||
measures = cleaned_data.get('measures', [])
|
||||
drilldown = cleaned_data.get('drilldown', [])
|
||||
l = []
|
||||
for kw, values in cleaned_data.iteritems():
|
||||
if values and kw.startswith('filter__'):
|
||||
l.append('$'.join([kw] + sorted(map(str, values))))
|
||||
l += drilldown + sorted(measures)
|
||||
return hashlib.md5('$'.join(l) + str(stringify)).hexdigest()
|
||||
|
||||
def get_data(self, cleaned_data, stringify=True):
|
||||
key = self.get_key(cleaned_data, stringify=stringify)
|
||||
data = cache.get(key)
|
||||
if data is not None:
|
||||
return data
|
||||
filters = []
|
||||
for kw, values in cleaned_data.iteritems():
|
||||
if values and kw.startswith('filter__'):
|
||||
dimension_name = kw[8:]
|
||||
filters.append((dimension_name, values))
|
||||
measures = cleaned_data.get('measures', [])
|
||||
drilldown = cleaned_data.get('drilldown', [])
|
||||
data = []
|
||||
for row in self.cube.query(filters, drilldown, measures):
|
||||
for cell in row:
|
||||
value = cell['value']
|
||||
if stringify and cell['type'] == 'percent':
|
||||
# FIXME find how to format decimal number using locale with Django
|
||||
try:
|
||||
value = ('%05.2f' % float(value)).replace('.', ',') + u' %'
|
||||
except:
|
||||
value = _('Non applicable')
|
||||
elif value is not None and cell['type'] == 'duration':
|
||||
if stringify:
|
||||
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
|
||||
else:
|
||||
# convert duration for float number of days
|
||||
value = value.days + value.seconds / 86400.
|
||||
cell['value'] = value
|
||||
data.append(row)
|
||||
cache.set(key, data)
|
||||
return data
|
||||
|
||||
def get_grouped_data(self, cleaned_data, stringify=True):
|
||||
data = self.get_data(cleaned_data, stringify=stringify)
|
||||
dims = len(cleaned_data.get('drilldown', []))
|
||||
if not dims:
|
||||
return ([(v, 0) for v in row] for row in data)
|
||||
grouped = OrderedDict()
|
||||
for row in data:
|
||||
d = grouped
|
||||
for cell in row[:dims - 1]:
|
||||
dim = cell['value']
|
||||
if dim not in d:
|
||||
d[dim] = OrderedDict(), 1
|
||||
else:
|
||||
d[dim] = d[dim][0], d[dim][1] + 1
|
||||
d, count = d[dim]
|
||||
d[row[dims - 1]['value']] = row[dims:], 0
|
||||
|
||||
def helper(d, prefix=[]):
|
||||
for key in d:
|
||||
row, count = d[key]
|
||||
if count == 0:
|
||||
yield prefix + [(key, 1)] + [(v, 0) for v in row]
|
||||
else:
|
||||
for r in helper(row, prefix + [(key, count)]):
|
||||
yield r
|
||||
prefix = []
|
||||
return helper(grouped)
|
||||
|
||||
|
||||
class CubeView(AuthorizationMixin, CubeMixin, FormView):
|
||||
template_name = 'bijoe/cube.html'
|
||||
form_class = CubeForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.warehouse = Engine([warehouse for warehouse in get_warehouses(self.request)
|
||||
if warehouse.name == self.kwargs['warehouse']][0])
|
||||
except IndexError:
|
||||
raise Http404
|
||||
try:
|
||||
self.cube = self.warehouse[self.kwargs['cube']]
|
||||
except KeyError:
|
||||
raise Http404
|
||||
return super(CubeView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(CubeView, self).get_form_kwargs()
|
||||
kwargs['cube'] = self.cube
|
||||
if self.request.method == 'GET' and self.request.GET:
|
||||
kwargs.update({
|
||||
'data': self.request.GET,
|
||||
})
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
if 'ods' in self.request.POST:
|
||||
return self.ods(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def ods(self, form):
|
||||
workbook = Workbook()
|
||||
sheet = workbook.add_sheet(self.cube.label)
|
||||
ctx = self.get_context_data(form=form)
|
||||
for j, m in enumerate(ctx['drilldown'] + ctx['measures']):
|
||||
sheet.write(0, j, m.label)
|
||||
for i, row in enumerate(ctx['data']):
|
||||
for j, cell in enumerate(row):
|
||||
sheet.write(i + 1, j, unicode(cell['value']))
|
||||
response = HttpResponse(content_type='application/vnd.oasis.opendocument.spreadsheet')
|
||||
response['Content-Disposition'] = 'attachment; filename=%s.ods' % self.cube.name
|
||||
workbook.save(response)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(CubeView, self).get_context_data(**kwargs)
|
||||
ctx['warehouse'] = self.warehouse
|
||||
ctx['cube'] = self.cube
|
||||
|
||||
form = ctx['form']
|
||||
ctx['data'] = False
|
||||
if form.is_valid():
|
||||
ctx['representation'] = form.cleaned_data['representation']
|
||||
ctx['data'] = self.get_data(form.cleaned_data)
|
||||
ctx['grouped_data'] = list(self.get_grouped_data(form.cleaned_data))
|
||||
ctx['measures'] = measures = [self.cube.measures[measure] for measure in
|
||||
form.cleaned_data['measures']]
|
||||
ctx['measures_json'] = json.dumps([measure.to_json() for measure in measures])
|
||||
ctx['drilldown'] = dimensions = [self.cube.dimensions[dimension] for dimension in
|
||||
form.cleaned_data['drilldown']]
|
||||
ctx['drilldown_json'] = json.dumps([dim.to_json() for dim in dimensions])
|
||||
if dimensions:
|
||||
ctx['data_title'] = _('{0} par {1}').format(
|
||||
human_join([measure.label for measure in measures]),
|
||||
human_join([dimension.label for dimension in dimensions]))
|
||||
else:
|
||||
ctx['data_title'] = human_join([measure.label for measure in measures])
|
||||
json_data = []
|
||||
for row in self.get_data(form.cleaned_data, stringify=False):
|
||||
coords = []
|
||||
for dimension, cell in zip(ctx['drilldown'], row):
|
||||
coords.append(cell)
|
||||
measures = []
|
||||
for measure, cell in zip(ctx['measures'], row[len(ctx['drilldown']):]):
|
||||
if isinstance(cell['value'], decimal.Decimal):
|
||||
cell['value'] = float(cell['value'])
|
||||
measures.append(cell)
|
||||
json_data.append({'coords': coords, 'measures': measures})
|
||||
|
||||
ctx['json'] = json.dumps(json_data, indent=2)
|
||||
return ctx
|
||||
|
||||
|
||||
class MenuJSONView(AuthorizationMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = HttpResponse(content_type='application/json')
|
||||
menu = [
|
||||
{
|
||||
'label': _('Statistiques'),
|
||||
'label': _('Statistics'),
|
||||
'slug': 'statistics',
|
||||
'url': request.build_absolute_uri(reverse('homepage')),
|
||||
}
|
||||
|
@ -269,14 +91,8 @@ class MenuJSONView(AuthorizationMixin, View):
|
|||
response.write(json_str)
|
||||
return response
|
||||
|
||||
class CubeIframeView(CubeView):
|
||||
template_name = 'bijoe/cube_raw.html'
|
||||
|
||||
|
||||
homepage = HomepageView.as_view()
|
||||
warehouse = WarehouseView.as_view()
|
||||
cube = CubeView.as_view()
|
||||
cube_iframe = xframe_options_exempt(CubeIframeView.as_view())
|
||||
menu_json = MenuJSONView.as_view()
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# 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 django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class VisualizationAdmin(admin.ModelAdmin):
|
||||
list_display = ['name']
|
||||
|
||||
admin.site.register(models.Visualization, VisualizationAdmin)
|
|
@ -15,11 +15,9 @@
|
|||
# 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 datetime
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from django.forms import ModelForm, TextInput
|
||||
try:
|
||||
from django_select2.forms import Select2MultipleWidget
|
||||
|
||||
|
@ -32,14 +30,26 @@ except ImportError:
|
|||
return Select2MultipleWidget(select2_options={'width': '100%'})
|
||||
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class VisualizationForm(ModelForm):
|
||||
class Meta:
|
||||
model = models.Visualization
|
||||
exclude = ('parameters',)
|
||||
widgets = {
|
||||
'name': TextInput,
|
||||
}
|
||||
|
||||
|
||||
class DateRangeWidget(forms.MultiWidget):
|
||||
def __init__(self, attrs=None):
|
||||
attrs = attrs.copy() if attrs else {}
|
||||
attrs.update({'type': 'date', 'autocomplete': 'off'})
|
||||
attrs1 = attrs.copy()
|
||||
attrs1['placeholder'] = _(u'début')
|
||||
attrs1['placeholder'] = _(u'start')
|
||||
attrs2 = attrs.copy()
|
||||
attrs2['placeholder'] = _(u'fin')
|
||||
attrs2['placeholder'] = _(u'end')
|
||||
widgets = (
|
||||
forms.DateInput(attrs=attrs1),
|
||||
forms.DateInput(attrs=attrs2),
|
||||
|
@ -70,9 +80,9 @@ class DateRangeField(forms.MultiValueField):
|
|||
|
||||
class CubeForm(forms.Form):
|
||||
representation = forms.ChoiceField(
|
||||
label=_(u'Représentation'),
|
||||
choices=[('table', _('tableau')),
|
||||
('graphical', _('graphique')),],
|
||||
label=_(u'Presentation'),
|
||||
choices=[('table', _('table')),
|
||||
('graphical', _('chart'))],
|
||||
widget=forms.RadioSelect())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Visualization',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.TextField(verbose_name='name')),
|
||||
('parameters', jsonfield.fields.JSONField(default=dict, verbose_name='parameters')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('name', 'id'),
|
||||
'verbose_name': 'visualization',
|
||||
'verbose_name_plural': 'visualizations',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
# 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 django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from jsonfield import JSONField
|
||||
|
||||
|
||||
class Visualization(models.Model):
|
||||
name = models.TextField(verbose_name=_('name'))
|
||||
parameters = JSONField(verbose_name=_('parameters'))
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'id')
|
||||
verbose_name = _('visualization')
|
||||
verbose_name_plural = _('visualizations')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
|
@ -0,0 +1,95 @@
|
|||
# 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 datetime
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
import urllib
|
||||
import random
|
||||
import urlparse
|
||||
|
||||
'''Simple signature scheme for query strings'''
|
||||
# from http://repos.entrouvert.org/portail-citoyen.git/tree/portail_citoyen/apps/data_source_plugin/signature.py
|
||||
|
||||
|
||||
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
|
||||
parsed = urlparse.urlparse(url)
|
||||
new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
|
||||
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
|
||||
|
||||
|
||||
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
|
||||
if timestamp is None:
|
||||
timestamp = datetime.datetime.utcnow()
|
||||
timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
if nonce is None:
|
||||
nonce = hex(random.getrandbits(128))[2:]
|
||||
new_query = query
|
||||
if new_query:
|
||||
new_query += '&'
|
||||
new_query += urllib.urlencode((
|
||||
('algo', algo),
|
||||
('timestamp', timestamp),
|
||||
('nonce', nonce)))
|
||||
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
|
||||
new_query += '&signature=' + urllib.quote(signature)
|
||||
return new_query
|
||||
|
||||
|
||||
def sign_string(s, key, algo='sha256', timedelta=30):
|
||||
digestmod = getattr(hashlib, algo)
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
hash = hmac.HMAC(key, digestmod=digestmod, msg=s)
|
||||
return hash.digest()
|
||||
|
||||
|
||||
def check_url(url, key, known_nonce=None, timedelta=30):
|
||||
parsed = urlparse.urlparse(url, 'https')
|
||||
return check_query(parsed.query, key)
|
||||
|
||||
|
||||
def check_query(query, key, known_nonce=None, timedelta=30):
|
||||
parsed = urlparse.parse_qs(query)
|
||||
signature = base64.b64decode(parsed['signature'][0])
|
||||
algo = parsed['algo'][0]
|
||||
timestamp = parsed['timestamp'][0]
|
||||
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
|
||||
nonce = parsed['nonce']
|
||||
unsigned_query = query.split('&signature=')[0]
|
||||
if known_nonce is not None and known_nonce(nonce):
|
||||
return False
|
||||
if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta):
|
||||
return False
|
||||
return check_string(unsigned_query, signature, key, algo=algo)
|
||||
|
||||
|
||||
def check_string(s, signature, key, algo='sha256'):
|
||||
# constant time compare
|
||||
signature2 = sign_string(s, key, algo=algo)
|
||||
if len(signature2) != len(signature):
|
||||
return False
|
||||
res = 0
|
||||
for a, b in zip(signature, signature2):
|
||||
res |= ord(a) ^ ord(b)
|
||||
return res == 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
key = '12345'
|
||||
signed_query = sign_query('NameId=_12345&orig=montpellier', key)
|
||||
assert check_query(signed_query, key, timedelta=0) is False
|
||||
assert check_query(signed_query, key) is True
|
|
@ -0,0 +1,37 @@
|
|||
# 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 django.conf.urls import patterns, url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$',
|
||||
views.visualizations, name='visualizations'),
|
||||
url(r'^json/$',
|
||||
views.visualizations_json, name='visualizations-json'),
|
||||
url(r'^warehouse/(?P<warehouse>[^/]*)/$', views.warehouse, name='warehouse'),
|
||||
url(r'^warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/$', views.cube, name='cube'),
|
||||
url(r'^warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/iframe/$', views.cube_iframe,
|
||||
name='cube-iframe'),
|
||||
url(r'warehouse/(?P<warehouse>[^/]*)/(?P<cube>[^/]*)/save/$',
|
||||
views.create_visualization, name='create-visualization'),
|
||||
url(r'(?P<pk>\d+)/$', views.visualization, name='visualization'),
|
||||
url(r'(?P<pk>\d+)/iframe/$', views.visualization_iframe, name='visualization-iframe'),
|
||||
url(r'(?P<pk>\d+)/rename/$', views.rename_visualization, name='rename-visualization'),
|
||||
url(r'(?P<pk>\d+)/delete/$', views.visualization, name='delete-visualization'),
|
||||
)
|
|
@ -0,0 +1,184 @@
|
|||
# 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 hashlib
|
||||
import datetime
|
||||
import decimal
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from ..utils import get_warehouses, human_join
|
||||
from ..engine import Engine
|
||||
from .ods import Workbook
|
||||
|
||||
|
||||
class Visualization(object):
|
||||
def __init__(self, cube, representation, measures, drilldown=None, filters=None):
|
||||
self.cube = cube
|
||||
self.representation = representation
|
||||
|
||||
self.measures = measures
|
||||
self.drilldown = drilldown or []
|
||||
self.filters = filters or {}
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
'warehouse': self.cube.engine.warehouse.name,
|
||||
'cube': self.cube.name,
|
||||
'representation': self.representation,
|
||||
'measures': [measure.name for measure in self.measures],
|
||||
'drilldown': [drilldown.name for drilldown in self.drilldown],
|
||||
'filters': self.filters,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, d):
|
||||
for warehouse in get_warehouses():
|
||||
if d['warehouse'] == warehouse.name:
|
||||
break
|
||||
else:
|
||||
raise LookupError('warehouse %s not found' % d['warehouse'])
|
||||
cube = Engine(warehouse)[d['cube']]
|
||||
representation = d['representation']
|
||||
measures = [cube.measures[name] for name in d['measures']]
|
||||
drilldown = [cube.dimensions[name] for name in d.get('drilldown', [])]
|
||||
filters = d.get('filters', {})
|
||||
return cls(cube, representation, measures, drilldown=drilldown, filters=filters)
|
||||
|
||||
@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
|
||||
measures = cleaned_data.get('measures', [])
|
||||
measures = [cube.measures[name] for name in measures]
|
||||
drilldown = cleaned_data.get('drilldown', [])
|
||||
drilldown = [cube.dimensions[name] for name in drilldown]
|
||||
return cls(cube, cleaned_data['representation'], measures, drilldown=drilldown,
|
||||
filters=filters)
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
l = []
|
||||
for kw, values in self.filters.iteritems():
|
||||
if values:
|
||||
l.append('$'.join([kw] + sorted(map(unicode, values))))
|
||||
l += [dim.name for dim in self.drilldown]
|
||||
l += [measure.name for measure in self.measures]
|
||||
key = '$'.join(v.encode('utf8') for v in l)
|
||||
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 = ('%05.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.iteritems(),
|
||||
[dim.name for dim in self.drilldown],
|
||||
[measure.name for measure in self.measures])
|
||||
|
||||
def cached(self):
|
||||
key = self.key
|
||||
data = cache.get(key)
|
||||
if data is None:
|
||||
data = list(self.data())
|
||||
cache.set(key, data)
|
||||
return data
|
||||
|
||||
def grouped_data(self):
|
||||
data = self.stringified()
|
||||
dims = len(self.drilldown)
|
||||
if not dims:
|
||||
return ([(v, 0) for v in row] for row in data)
|
||||
grouped = OrderedDict()
|
||||
for row in data:
|
||||
d = grouped
|
||||
for cell in row[:dims - 1]:
|
||||
dim = cell['value']
|
||||
if dim not in d:
|
||||
d[dim] = OrderedDict(), 1
|
||||
else:
|
||||
d[dim] = d[dim][0], d[dim][1] + 1
|
||||
d, count = d[dim]
|
||||
d[row[dims - 1]['value']] = row[dims:], 0
|
||||
|
||||
def helper(d, prefix=[]):
|
||||
for key in d:
|
||||
row, count = d[key]
|
||||
if count == 0:
|
||||
yield prefix + [(key, 1)] + [(v, 0) for v in row]
|
||||
else:
|
||||
for r in helper(row, prefix + [(key, count)]):
|
||||
yield r
|
||||
prefix = []
|
||||
return helper(grouped)
|
||||
|
||||
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()
|
||||
sheet = workbook.add_sheet(self.cube.label)
|
||||
for j, m in enumerate(self.drilldown + self.measures):
|
||||
sheet.write(0, j, m.label)
|
||||
for i, row in enumerate(self.stringified()):
|
||||
for j, cell in enumerate(row):
|
||||
sheet.write(i + 1, j, unicode(cell['value']))
|
||||
return workbook
|
||||
|
||||
def title(self):
|
||||
if self.drilldown:
|
||||
return _('{0} by {1}').format(human_join([measure.label for measure in self.measures]),
|
||||
human_join([dimension.label for dimension in
|
||||
self.drilldown]))
|
||||
else:
|
||||
return human_join([measure.label for measure in self.measures])
|
|
@ -0,0 +1,237 @@
|
|||
# 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 hashlib
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
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
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
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(self.request)
|
||||
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
|
||||
|
||||
if self.visualization:
|
||||
ctx['visualization'] = self.visualization
|
||||
ctx['measures_json'] = json.dumps(
|
||||
[measure.to_json() for measure in self.visualization.measures])
|
||||
ctx['drilldown_json'] = json.dumps(
|
||||
[dim.to_json() for dim in self.visualization.drilldown])
|
||||
ctx['json'] = json.dumps(self.visualization.json_data(), indent=2)
|
||||
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(self.request)
|
||||
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 ODSMixin(object):
|
||||
def ods(self, visualization):
|
||||
response = HttpResponse(content_type='application/vnd.oasis.opendocument.spreadsheet')
|
||||
response['Content-Disposition'] = 'attachment; filename=%s.ods' % self.cube.name
|
||||
workbook = visualization.ods()
|
||||
workbook.save(response)
|
||||
return response
|
||||
|
||||
|
||||
class CubeView(CubeDisplayMixin, ODSMixin, 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)s/'
|
||||
|
||||
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, ODSMixin, CubeDisplayMixin,
|
||||
DetailView):
|
||||
model = models.Visualization
|
||||
template_name = 'bijoe/visualization.html'
|
||||
|
||||
def get_object(self):
|
||||
named_visualization = super(VisualizationView, self).get_object()
|
||||
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()
|
||||
named_visualization.save()
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(VisualizationView, self).get_context_data(**kwargs)
|
||||
ctx['form'] = forms.CubeForm(cube=self.cube, initial={
|
||||
'representation': self.visualization.representation,
|
||||
'measures': [m.name for m in self.visualization.measures],
|
||||
'drilldown': [d.name for d in self.visualization.drilldown],
|
||||
})
|
||||
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 = 'homepage'
|
||||
|
||||
|
||||
class VisualizationsView(views.AuthorizationMixin, ListView):
|
||||
template_name = 'bijoe/visualizations.html'
|
||||
model = models.Visualization
|
||||
|
||||
|
||||
class RenameVisualization(views.AuthorizationMixin, UpdateView):
|
||||
model = models.Visualization
|
||||
form_class = forms.VisualizationForm
|
||||
template_name = 'bijoe/rename_visualization.html'
|
||||
success_url = '/visualization/%(id)s/'
|
||||
|
||||
|
||||
class VisualizationsJSONView(MultipleObjectMixin, View):
|
||||
model = models.Visualization
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
known_services = getattr(settings, 'KNOWN_SERVICES', [])
|
||||
if known_services:
|
||||
key = None
|
||||
for service in known_services:
|
||||
if service['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.append({
|
||||
'name': visualization.name,
|
||||
'path': request.build_absolute_uri(path),
|
||||
})
|
||||
response = HttpResponse(content_type='application/json')
|
||||
response.write(json.dumps(data))
|
||||
return response
|
||||
|
||||
|
||||
class CubeIframeView(CubeView):
|
||||
template_name = 'bijoe/cube_raw.html'
|
||||
|
||||
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 = VisualizationIframeView.as_view()
|
2
setup.py
2
setup.py
|
@ -55,6 +55,6 @@ setup(name="bijoe",
|
|||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['requests', 'django', 'psycopg2', 'isodate', 'Django-Select2<5',
|
||||
'XStatic-ChartNew.js', 'gadjo'],
|
||||
'XStatic-ChartNew.js', 'gadjo', 'django-jsonfield<1.0.0'],
|
||||
scripts=['bijoe-ctl'],
|
||||
cmdclass={'sdist': eo_sdist})
|
||||
|
|
Loading…
Reference in New Issue