Commit f4a4fad8 authored by Philip ABBET's avatar Philip ABBET
Browse files

Merge branch 'master' into 'master'

Reports overhaul

Closes #12, #47, #46, #45, #43, #44, #42, #465, #470, #468, #38, #39, #40, #33, #35, #32, #31, #15, and #11

See merge request !223
parents e263080b b69f4881
Pipeline #11734 failed with stage
in 13 minutes and 28 seconds
......@@ -26,3 +26,4 @@ doc/api/api/
html/
*.tar.bz2
nohup.out
*.log
{
"env": {
"browser": true,
"es6": true
},
"extends": "angular",
"rules": {
"indent": [
"error",
"tab",
{ "SwitchCase": 1 }
],
"linebreak-style": [
"error",
"unix"
],
"quotes": 0,
"semi": [
"error",
"always"
],
"no-var": 1,
"angular/di": 0,
"angular/definedundefined" : 0,
"angular/angularelement" : 0,
"angular/definedundefined" : 0,
"angular/document-service" : 0,
"angular/foreach" : 0,
"angular/interval-service" : 0,
"angular/json-functions" : 0,
"angular/log" : 0,
"angular/no-angular-mock" : 0,
"angular/no-jquery-angularelement" : 0,
"angular/timeout-service" : 0,
"angular/typecheck-array" : 0,
"angular/typecheck-date" : 0,
"angular/typecheck-function" : 0,
"angular/typecheck-number" : 0,
"angular/typecheck-object" : 0,
"angular/typecheck-string" : 0,
"angular/window-service" : 0
}
}
{
"libs": [
"browser",
"jquery"
],
"plugins": {
"angular": {}
}
}
......@@ -91,9 +91,10 @@ class ListPlotterParameterView(ListContributionView):
dataformat__name = name,
dataformat__version = version)
else:
author_name = 'plot' if self.request.user.is_anonymous() else self.request.user.username
#return self.model.objects.all()
#from author and public and get latest version only
objects = self.model.objects.from_author_and_public(self.request.user, self.request.user.username).order_by('-version')
objects = self.model.objects.from_author_and_public(self.request.user, author_name).order_by('-version')
filtered_list = []
filtered_list_id = []
for the_item in objects:
......@@ -104,7 +105,7 @@ class ListPlotterParameterView(ListContributionView):
if check == False:
filtered_list.append(the_item)
filtered_list_id.append(the_item.id)
objects = self.model.objects.from_author_and_public(self.request.user, self.request.user.username).order_by('-version').filter(id__in=filtered_list_id)
objects = self.model.objects.from_author_and_public(self.request.user, author_name).order_by('-version').filter(id__in=filtered_list_id)
return objects
class ListDefaultPlotterView(generics.ListAPIView):
......
......@@ -68,6 +68,8 @@ from ..common.responses import BadRequestResponse, ForbiddenResponse
import re
from django.utils.encoding import force_bytes, force_text
import simplejson as json
......@@ -202,13 +204,6 @@ class ReportDetailView(generics.RetrieveUpdateDestroyAPIView):
if self.kwargs.has_key('number') and report.status == Report.LOCKED:
data["anonymous"] = True
data["experiments"] = map(lambda x:data["content"]["alias_experiments"][x], data["experiments"])
data_alias_experiments = {}
for experiment in data["experiments"]:
data_alias_experiments[experiment] = experiment
data["content"]["alias_experiments"] = data_alias_experiments
return Response(data)
......@@ -546,3 +541,53 @@ class ReportResultsAllExperimentsView(CommonContextMixin, generics.RetrieveAPIVi
results[experiment.fullname()] = serializer.data
return Response(results)
#----------------------------------------------------------
class ReportRSTCompileView(BaseReportActionView):
permission_classes = BaseReportActionView.permission_classes + [IsEditable]
def get_queryset(self):
owner_name = self.kwargs.get('owner_name')
report_name = self.kwargs.get('report_name')
report = get_object_or_404(Report, author__username=owner_name, name=report_name)
self.check_object_permissions(self.request, report)
return report
def post(self, request, owner_name, report_name):
report = self.get_queryset()
result = {}
result['html_str'] = report.compileTextItem(request.data['raw'])
return Response(result)
#----------------------------------------------------------
class ReportRSTCompileAnonView(views.APIView):
permission_classes = [permissions.AllowAny]
def get_queryset(self):
number = self.kwargs.get('number')
report = get_object_or_404(Report, number=int(number))
self.check_object_permissions(self.request, report)
return report
def post(self, request, number):
report = self.get_queryset()
result = {}
result['html_str'] = report.compileTextItem(request.data['raw'])
return Response(result)
......@@ -30,6 +30,18 @@ from . import api
urlpatterns = [
url(
r'^(?P<owner_name>\w+)/(?P<report_name>[\w\W]+)/rst/$',
api.ReportRSTCompileView.as_view(),
name='rst_compiler'
),
url(
r'^(?P<number>\d+)/rst/$',
api.ReportRSTCompileAnonView.as_view(),
name='rst_compiler'
),
url(
r'^$',
api.ReportListView.as_view(),
......
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2017-03-13 15:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('reports', '0002_report_expiration_date'),
]
operations = [
migrations.AddField(
model_name='report',
name='last_edited_date',
field=models.DateTimeField(null=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2017-04-10 11:21
from __future__ import unicode_literals
from django.db import migrations
import json
import re
# parses a table from the old representation (a list of column objects)
# and returns a table in the new representation
def parse_table(table, precision):
conv_table = {
'itemName': 'Table',
'fields': ['Experiment'],
'precision': precision
}
for row in table:
if not row['selected']:
continue
name = row['name']
name = re.sub(r'\[.*\]$', '', name)
if name == 'experiment':
continue
if name == 'experiment.execution_time' or name == 'execution_time':
name = 'total_execution_time'
elif re.match(r'^execution_time\.', name):
name = re.sub(r'execution_time', 'linear_execution_time', name)
if name.startswith('experiment.'):
name = re.sub(r'experiment\.', '', name)
if '.' in name:
segs = name.split('.')
name = segs[1] + '.' + segs[0]
conv_table['fields'].append(name)
return conv_table
# parses a plot from the old representation
# and returns a plot in the new representation
def parse_plot(plot):
conv_plot = {
'itemName': plot['required_plotter'][0],
'name': plot['data']['output'][0],
'type': plot['required_plotter'][0]
}
return conv_plot
# helper func to build the experiment's full name
def experiment_fullname(exp):
return '%s/%s/%s/%s/%s' % (exp.author.username, exp.toolchain.author.username, exp.toolchain.name, exp.toolchain.version, exp.name)
# helper func to build the analyzer's full name
def analyzer_fullname(report):
return '%s/%s/%s' % (report.analyzer.author.username, report.analyzer.name, report.analyzer.version)
# converts an old report into the new report format
def move_content_to_groups_format(apps, schema_editor):
Report = apps.get_model('reports', 'Report')
for report in Report.objects.all():
# all of the changes are in the report's content field
report_content = json.loads(report.content)
# convert to groups format, but don't touch any report thats
# already using the new system
if 'groups' not in report_content:
# format:
# ----
# groups: {
# group1 : {
# experiments: [],
# reportItems: [],
# analyzer: '',
# aliases: {},
# idx: 1
# },
# ...
# }
# ----
exps = report.experiments.all()
obj = {}
groups = {}
# default to just one group that contains all experiments/items
group1 = {
# list of experiments in the group
'experiments': [ experiment_fullname(e) for e in exps ],
'reportItems': [],
# analyzer of the report
'analyzer': analyzer_fullname(report) if report.analyzer else '',
'aliases': {},
'idx': 1
}
old_aliases = report_content['alias_experiments'] if 'alias_experiments' in report_content else {}
# assign aliases
get_alias = lambda exp_name: old_aliases[exp_name] if exp_name in old_aliases else None
for e in exps:
fullname = experiment_fullname(e)
group1['aliases'][fullname] = get_alias(fullname) or e.name
count_tables = 0
count_plots = 0
for item_name in report_content:
if item_name == 'floating_point_precision' or item_name == 'alias_experiments':
continue
item = report_content[item_name]
item_type = 'table' if item_name.startswith('table') else 'plot'
fpp = report_content['floating_point_precision'] if 'floating_point_precision' in report_content else 10
converted_content = parse_table(item, fpp) if item_type == 'table' else parse_plot(item)
converted_id = ''
if item_type == 'table':
converted_id = 'table_' + str(count_tables)
count_tables += 1
else:
converted_id = 'plot_' + str(count_plots)
count_plots += 1
converted_item = {
'id': converted_id,
'content': converted_content
}
group1['reportItems'].append(converted_item)
groups['group1'] = group1
obj['groups'] = groups
report.content = json.dumps(obj)
report.save()
class Migration(migrations.Migration):
dependencies = [
('reports', '0003_report_last_edited_date'),
]
operations = [
migrations.RunPython(move_content_to_groups_format)
]
......@@ -28,6 +28,11 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.utils.encoding import force_bytes, force_text
from django.conf import settings
from ..common.utils import validate_restructuredtext
from ..ui.templatetags.markup import restructuredtext
from ..algorithms.models import Algorithm
from ..experiments.models import Experiment
......@@ -59,6 +64,7 @@ class ReportManager(models.Manager):
else:
report.content = content
report.creation_date = datetime.now()
report.last_edited_date = datetime.now()
report.publication_date = None
report.expiration_date = None
report.status = self.model.EDITABLE
......@@ -108,6 +114,7 @@ class Report(models.Model):
author = models.ForeignKey(User, related_name='%(class)ss')
experiments = models.ManyToManyField(Experiment, related_name='reports', blank=True)
creation_date = models.DateTimeField()
last_edited_date = models.DateTimeField(null=True)
expiration_date = models.DateTimeField(null=True, blank=True)
publication_date = models.DateTimeField(null=True, blank=True)
short_description = models.CharField(max_length=100, default='', blank=True, help_text=Messages['short_description'])
......@@ -192,6 +199,8 @@ class Report(models.Model):
report_content = json.loads(self.content)
report_content_charts = dict(filter(lambda item: item[0].startswith("chart"),report_content.iteritems()))
self.last_edited_date = datetime.now()
super(Report, self).save(*args, **kwargs)
self.referenced_plotters.clear()
......@@ -214,7 +223,6 @@ class Report(models.Model):
}
# Process the list of experiments
common_analyzers = None
accessible_experiments = []
inaccessible_experiments = []
......@@ -229,36 +237,11 @@ class Report(models.Model):
accessible_experiments.append(experiment)
if self.analyzer is None:
if common_analyzers is None:
common_analyzers = map(lambda x: x.algorithm, experiment.blocks.filter(analyzer=True))
else:
analyzers = map(lambda x: x.algorithm, experiment.blocks.filter(analyzer=True))
common_analyzers = filter(lambda x: x in analyzers, common_analyzers)
if len(common_analyzers) == 0:
return {
'success': False,
'error': "No common analyzer",
}
# Check that we have common analyzers (if necessary)
if (self.analyzer is None) and (common_analyzers is not None):
if len(common_analyzers) == 1:
self.analyzer = common_analyzers[0]
self.save()
elif len(common_analyzers) > 1:
return {
'success': False,
'common_analyzers': map(lambda x: x.fullname(), common_analyzers),
}
# Add the experiments to the report
incompatible_experiments = []
for experiment in accessible_experiments:
if len(experiment.blocks.filter(analyzer=True, algorithm=self.analyzer)) >= 1:
if len(experiment.blocks.filter(analyzer=True)) >= 1:
self.experiments.add(experiment)
else:
incompatible_experiments.append(experiment.fullname())
......@@ -334,3 +317,27 @@ class Report(models.Model):
alias_list = map(lambda x: report_content["alias_experiments"][x], experiments_list)
return experiments_list, alias_list
# the itemStr can either be:
def compileTextItem(self, itemStr):
content = json.loads(self.content)
rstStr = ''
try:
textBlockMap = itemStr.split('|')
rstStr = content['groups'][textBlockMap[0]]['reportItems'][int(textBlockMap[1])]['content']['text']
except KeyError:
rstStr = itemStr
result = {}
try:
from docutils.core import publish_parts
except ImportError:
if settings.DEBUG:
raise template.TemplateSyntaxError("Error in ReportRSTCompileView: The Python docutils library isn't installed.")
return rstStr
else:
docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {})
parts = publish_parts(source=force_bytes(rstStr), writer_name="html4css1", settings_overrides=docutils_settings)
return force_text(parts["fragment"])
......@@ -73,6 +73,20 @@ class IsLocked(permissions.BasePermission):
#----------------------------------------------------------
class IsPublished(permissions.BasePermission):
"""
Object level permission that returns true if the
given object status is Report.PUBLISHED
"""
message = 'This report is not published'
def has_object_permission(self, request, view, obj):
return obj.status == Report.PUBLISHED
#----------------------------------------------------------
class IsAuthorOrPublished(permissions.BasePermission):
"""
The logged in user should also be the author or
......
......@@ -108,7 +108,7 @@ class BasicReportSerializer(serializers.ModelSerializer):
class SimpleReportSerializer(BasicReportSerializer):
class Meta(BasicReportSerializer.Meta):
fields = ['name', 'number', 'short_description', 'is_owner', 'author','status', 'description', 'creation_date', 'html_description', 'add_url']
fields = ['name', 'number', 'short_description', 'is_owner', 'author','status', 'description', 'creation_date', 'html_description', 'add_url', 'content']
#----------------------------------------------------------
......
/*
* Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/
* Contact: beat.support@idiap.ch
*
*
* This file is part of the beat.web module of the BEAT platform.
*
*
* Commercial License Usage
* Licensees holding valid commercial BEAT licenses may use this file in
* accordance with the terms contained in a written agreement between you
* and Idiap. For further information contact tto@idiap.ch
*
*
* Alternatively, this file may be used under the terms of the GNU Affero
* Public License version 3 as published by the Free Software and appearing
* in the file LICENSE.AGPL included in the packaging of this file.
* The BEAT platform 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.
*
*
* You should have received a copy of the GNU Affero Public License along
* with the BEAT platform. If not, see http://www.gnu.org/licenses/.
*/
var app = angular.module('reportApp');
app.config(function configureStartEndSymbol($interpolateProvider) {
$interpolateProvider.startSymbol('{$').endSymbol('$}');
}
*/
angular.module('reportApp').config(function configureStartEndSymbol($interpolateProvider) {
$interpolateProvider.startSymbol('{$').endSymbol('$}');
}
);
app.config(function configHttp($httpProvider) {
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
$httpProvider.defaults.withCredentials = true;
}
angular.module('reportApp').config(function configHttp($httpProvider) {
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
$httpProvider.defaults.withCredentials = true;
}
);
/*
* Copyright (c) 2016 Idiap Research Institute, http://www.idiap.ch/
* Contact: beat.support@idiap.ch
*
*
* This file is part of the beat.web module of the BEAT platform.
*
*
* Commercial License Usage
* Licensees holding valid commercial BEAT licenses may use this file in
* accordance with the terms contained in a written agreement between you
* and Idiap. For further information contact tto@idiap.ch
*
*
* Alternatively, this file may be used under the terms of the GNU Affero
* Public License version 3 as published by the Free Software and appearing
* in the file LICENSE.AGPL included in the packaging of this file.
* The BEAT platform 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.
*
*
* You should have received a copy of the GNU Affero Public License along
* with the BEAT platform. If not, see http://www.gnu.org/licenses/.
*/
var app = angular.module('reportApp', ['ui.router', 'angular.filter']);
*/
angular.module('reportApp', ['ui.router', 'angular.filter', 'ui.sortable', 'ui.codemirror']);
app.config(function ($stateProvider, $urlRouterProvider){
angular.module('reportApp').config(function ($stateProvider, $urlRouterProvider){
$urlRouterProvider
.otherwise('/');
$urlRouterProvider
.otherwise('/');
$stateProvider
.state('report', {
url: '/',
views: {
//'myReportInfo': {
// //templateUrl: '/platform/reports/partials/reportInfo/',
// templateUrl: function(params)
// {
// console.log("ici");
// console.log(params);
// console.log($scope);
// },
// //templateUrl: '/reports/partials/reportGeneralInfo.html',
// controller: 'reportController',
// //controller: function($scope)
// //{
// // console.log($scope);
// //},
//}
}