diff --git a/beat/web/reports/serializers.py b/beat/web/reports/serializers.py index 8d70261f2c1b1538121d6275280676d59eeb1f76..158db1c814a3dd26cbf96c0a891588660c9a6704 100644 --- a/beat/web/reports/serializers.py +++ b/beat/web/reports/serializers.py @@ -50,6 +50,7 @@ class BasicReportSerializer(serializers.ModelSerializer): content = serializers.SerializerMethodField() author = serializers.SerializerMethodField() experiments = serializers.SerializerMethodField() + experiment_access_map = serializers.SerializerMethodField() analyzer = serializers.SerializerMethodField() html_description = serializers.SerializerMethodField() add_url = serializers.SerializerMethodField() @@ -86,6 +87,12 @@ class BasicReportSerializer(serializers.ModelSerializer): def get_experiments(self, obj): return map(lambda x: x.fullname(), obj.experiments.iterator()) + def get_experiment_access_map(self, obj): + user = self.context['request'].user + access_map = list(map(lambda x: x.accessibility_for(user)[0], + obj.experiments.iterator())) + return access_map + def get_analyzer(self, obj): if obj.analyzer is not None: return obj.analyzer.fullname() @@ -118,7 +125,7 @@ class FullReportSerializer(BasicReportSerializer): class Meta(BasicReportSerializer.Meta): - fields = ['name', 'number', 'short_description', 'description', 'is_owner', 'author','status', 'creation_date', 'publication_date', 'experiments', 'analyzer', 'content', 'html_description'] + fields = ['name', 'number', 'short_description', 'description', 'is_owner', 'author','status', 'creation_date', 'publication_date', 'experiments', 'analyzer', 'content', 'html_description', 'experiment_access_map'] #---------------------------------------------------------- diff --git a/beat/web/reports/static/reports/app/controllers/groupsController.js b/beat/web/reports/static/reports/app/controllers/groupsController.js index 67f6fc41475a40a60e982ee1db14599ca2945afa..0e8d7e2b8792d568e3a4cb59f122d3cb13e7fffe 100644 --- a/beat/web/reports/static/reports/app/controllers/groupsController.js +++ b/beat/web/reports/static/reports/app/controllers/groupsController.js @@ -25,9 +25,33 @@ * provides access to the groups data to Django templates, * used for handling the removal of experiments from the report */ -angular.module('reportApp').controller('GroupsController', ['$http', 'UrlService', function ($http, UrlService){ +angular.module('reportApp').controller('GroupsController', + ['$scope', '$http', 'UrlService', 'GroupsService', 'ReportService', 'reportFactory', 'ErrorService', 'ExperimentsService', + function (scope, $http, UrlService, GroupsService, ReportService, reportFactory, ErrorService, ExperimentsService){ let vm = this; + vm.saveReport = () => { + // save the serialized group data... + // the rest of the state is reconstructed from it and the URL + let saveData = { + content: { + 'groups': GroupsService.serializeGroups() + } + }; + + return reportFactory.removeExperiments(ExperimentsService.cachedDeletedExperiments) + .then(() => reportFactory.updateReport(ReportService.author, ReportService.name, saveData, '')) + .then(() => { + const lastEditedEl = document.querySelector('.lastEdited'); + lastEditedEl.classList.remove('lastEditedAnimating'); + void lastEditedEl.offsetWidth; + lastEditedEl.classList.add('lastEditedAnimating'); + }) + .catch(e => { + ErrorService.logError(e, `Could not save the report.`); + }); + }; + vm.expNamesToRemove = []; vm.toggleExpName = (expName) => { let idx = vm.expNamesToRemove.indexOf(expName); diff --git a/beat/web/reports/static/reports/app/directives/experimentsTable.js b/beat/web/reports/static/reports/app/directives/experimentsTable.js index ae111e80d05e0b7bcd4b12abda05f2c7f1bb7d7a..e9724588ce114d3ce05809b34db2e9f4420eed82 100644 --- a/beat/web/reports/static/reports/app/directives/experimentsTable.js +++ b/beat/web/reports/static/reports/app/directives/experimentsTable.js @@ -61,6 +61,16 @@ angular.module('reportApp') scope.deleteExpFromReport = (expName) => { ExperimentsService.deleteExperiment(expName); }; + + const getUnusedExperiments = (expNames, groups) => { + const usedExps = Array.from(new Set([].concat.apply([], groups.map(g => g.experiments)))); + const unusedExps = expNames.filter(n => !usedExps.includes(n)); + scope.unusedExps = unusedExps; + }; + + scope.$watchCollection('expNames', (names) => {getUnusedExperiments(names, scope.groups);}); + scope.$watch('groups', (gs) => {getUnusedExperiments(scope.expNames, gs);}, true); + getUnusedExperiments(scope.expNames, scope.groups); }, template: ` <div id='{{ domId }}' class='panel panel-default'> @@ -85,6 +95,7 @@ angular.module('reportApp') <table ng-if='expNames.length > 0' class="table table-striped table-hover"> <thead> <tr> + <th ng-if='!isViewmode()'></th> <th ng-if='!isViewmode()'></th> <th ng-if='isViewmode() && groups.length == 1'>Alias</th> <th>Experiment</th> @@ -107,6 +118,14 @@ angular.module('reportApp') </span> </div> </td> + <td ng-if='!isViewmode()'> + <span + ng-if='unusedExps.includes(expName)' + style='cursor: help;' + title="Experiment needs to be added to a group"> + <i class="fa fa-flag fa-lg text-warning"></i> + </span> + </td> <td ng-if='isViewmode() && groups.length == 1'> <span ng-if='groups[0].experiments.includes(expName)'> {{ groups[0].aliases[expName] }} diff --git a/beat/web/reports/static/reports/app/directives/layout.js b/beat/web/reports/static/reports/app/directives/layout.js index 1e9e949082401340c190d4d2c9d0b028a9896c0d..558f3e2934aa512ec50becdd8cd6ddcb00bcee9c 100644 --- a/beat/web/reports/static/reports/app/directives/layout.js +++ b/beat/web/reports/static/reports/app/directives/layout.js @@ -42,7 +42,7 @@ angular.module('reportApp').directive("groupsLayout", ['GroupsService', 'UrlServ }; }, template: ` -<experiments-table ng-if='!isLocked()'></experiments-table> +<experiments-table ng-if='!isViewmode() || GroupsService.groups.length > 1'></experiments-table> <div ng-if='!isViewmode()' group-add-group-menu class='panel'></div> <div id='groupsLayout' class='panel-group'> @@ -60,6 +60,9 @@ angular.module('reportApp').directive("groupsLayout", ['GroupsService', 'UrlServ </div> </div> <div ng-if='isViewmode() && GroupsService.groups.length == 1'> + <div class='panel-group'> + <div group-panel-experiments group='GroupsService.groups[0]' class='panel panel-default'></div> + </div> <group-panel-items group='GroupsService.groups[0]'></group-panel-items> </div> </div> diff --git a/beat/web/reports/static/reports/app/directives/lock.js b/beat/web/reports/static/reports/app/directives/lock.js index 73b7f687c961e04996f1f8e8b405cd608f0f2cea..d6794b1c47614f9ea38349bd6cb098fea8cae4cd 100644 --- a/beat/web/reports/static/reports/app/directives/lock.js +++ b/beat/web/reports/static/reports/app/directives/lock.js @@ -26,25 +26,36 @@ * Displays a modal for locking the current report. */ angular.module('reportApp') -.directive("reportLock", ['ReportService', 'ErrorService', function(ReportService, ErrorService){ +.directive("reportLock", ['ReportService', 'ErrorService', 'GroupsService', 'ExperimentsService', function(ReportService, ErrorService, GroupsService, ExperimentsService){ return { scope: { + saveReport: '=saveReport', }, restrict: 'E', link: function(scope, el){ + scope.expNames = ExperimentsService.experimentNames; + scope.groups = GroupsService.groups; + const calcUnusedExperiments = () => { + const usedExps = Array.from(new Set([].concat.apply([], scope.groups.map(g => g.experiments)))); + const unusedExps = scope.expNames.filter(n => !usedExps.includes(n)); + scope.unusedExps = unusedExps.length !== 0; + }; + scope.$watchCollection('expNames', (names) => {calcUnusedExperiments();}); + scope.$watch('groups', () => {calcUnusedExperiments();}, true); + // sends the request to lock the report scope.lockReport = () => { - return ReportService.lockReport() - .then(() => { - window.location.reload(); - }) + return scope.saveReport() + .then(() => ReportService.lockReport()) + .then(() => {window.location.reload();}) .catch(e => { ErrorService.logError(e, `Could not lock the report.`); }); } }, template: ` -<bootstrap-modal dom-id='lockReportModal' button-submit-text='Lock' button-submit-func='lockReport'> +<div> +<bootstrap-modal dom-id='lockReportModal' button-submit-text='Lock' button-submit-func='lockReport' ng-if='!unusedExps'> <b-title> Lock Report </b-title> @@ -54,6 +65,16 @@ angular.module('reportApp') <p>In order to do lock your report, your experiments will be locked as well, if they are not already (they will not be able to be edited or deleted).</p> </b-content> </bootstrap-modal> +<bootstrap-modal dom-id='lockReportModal' ng-if='unusedExps'> + <b-title> + Lock Report + </b-title> + <b-content> + <p>There are experiments that haven't been added to a group.</p> + <p>Please make sure every experiment is in a group, or remove experiments from the report that aren't needed.</p> + </b-content> +</bootstrap-modal> +</div> ` }; }]); diff --git a/beat/web/reports/static/reports/app/directives/panelExperiments.js b/beat/web/reports/static/reports/app/directives/panelExperiments.js index c831da8272ad0bfa0cbf3e34752e01d3f2b690a7..882c2d236fad589b92edc8ce791a0f77d106e52e 100644 --- a/beat/web/reports/static/reports/app/directives/panelExperiments.js +++ b/beat/web/reports/static/reports/app/directives/panelExperiments.js @@ -27,7 +27,7 @@ * a table of experiments in the group, their databases/protocols, and aliases. * Also has a menu for adding (compatible) experiments to the group. */ -angular.module('reportApp').directive("groupPanelExperiments", ['GroupsService', 'ExperimentsService', 'UrlService', function(GroupsService, ExperimentsService, UrlService){ +angular.module('reportApp').directive("groupPanelExperiments", ['GroupsService', 'ExperimentsService', 'UrlService', 'ReportService', function(GroupsService, ExperimentsService, UrlService, ReportService){ return { scope: { group: '=' @@ -35,6 +35,7 @@ angular.module('reportApp').directive("groupPanelExperiments", ['GroupsService', link: function(scope){ scope.experiments = ExperimentsService.experiments; scope.dropdownId = `${scope.group.name}_exp_add_dropdown`; + scope.accessMap = ReportService.accessMap; scope.getExpName = (expName) => scope.experiments[expName] ? expName : expName.split('/').pop(); const getExp = (expName) => scope.experiments[expName] || scope.experiments[expName.split('/').pop()]; @@ -137,7 +138,10 @@ angular.module('reportApp').directive("groupPanelExperiments", ['GroupsService', </td> <td ng-if='!isViewmode()'><input ng-model='group.aliases[expName]' ng-model-options="{ debounce: 500 }"></input></td> <td ng-if='isViewmode()'><span>{{ group.aliases[expName] }}</span></td> - <td><a href='{{ getExpUrl(expName) }}'>{{ getExpName(expName) }}</a></td> + <td> + <a ng-if='accessMap[expName]' href='{{ getExpUrl(expName) }}'>{{ getExpName(expName) }}</a> + <i ng-if='!accessMap[expName]'><small>experiment not accessible for current user</small></i> + </td> <td> <span ng-repeat='db in getExpDatabases(expName)'> <a href='{{ getDatabaseUrl(db.split("@")[0]) }}'>{{ db }}</a> diff --git a/beat/web/reports/static/reports/app/directives/save.js b/beat/web/reports/static/reports/app/directives/save.js index d4231f155b9fbf4d260a4a4fad86d7bbed6a6502..cb0cfdda65a30d2073a877fa1b6da248e1f0d6ef 100644 --- a/beat/web/reports/static/reports/app/directives/save.js +++ b/beat/web/reports/static/reports/app/directives/save.js @@ -29,31 +29,11 @@ angular.module('reportApp') .directive("reportSave", ['GroupsService', 'ReportService', 'reportFactory', 'ErrorService', 'ExperimentsService', function(GroupsService, ReportService, reportFactory, ErrorService, ExperimentsService){ return { restrict: 'A', - link: function(scope, el){ - - const saveReport = () => { - // save the serialized group data... - // the rest of the state is reconstructed from it and the URL - let saveData = { - content: { - 'groups': GroupsService.serializeGroups() - } - }; - - return reportFactory.removeExperiments(ExperimentsService.cachedDeletedExperiments) - .then(() => reportFactory.updateReport(ReportService.author, ReportService.name, saveData, '')) - .then(() => { - const lastEditedEl = document.querySelector('.lastEdited'); - lastEditedEl.classList.remove('lastEditedAnimating'); - void lastEditedEl.offsetWidth; - lastEditedEl.classList.add('lastEditedAnimating'); - }) - .catch(e => { - ErrorService.logError(e, `Could not save the report.`); - }); - }; - - el.bind('click', saveReport); + scope: { + saveReport: '=saveReport', + }, + link: function(scope, el, attrs){ + el.bind('click', scope.saveReport); }, }; }]); diff --git a/beat/web/reports/static/reports/app/directives/tableItem.js b/beat/web/reports/static/reports/app/directives/tableItem.js index 99249203d1e3e7a3c7cad01f78dc2a371c5ecc3e..1a9cae99a0efb168e4d0f57728e9d2c9c30df0ce 100644 --- a/beat/web/reports/static/reports/app/directives/tableItem.js +++ b/beat/web/reports/static/reports/app/directives/tableItem.js @@ -27,7 +27,7 @@ * manage this table's selected cols and float precision */ angular.module('reportApp') -.directive("groupTableItem", ['GroupsService', 'ExperimentsService', 'UrlService', function(GroupsService, ExperimentsService, UrlService){ +.directive("groupTableItem", ['GroupsService', 'ExperimentsService', 'UrlService', 'ReportService', function(GroupsService, ExperimentsService, UrlService, ReportService){ return { scope: { group: '=', @@ -35,6 +35,8 @@ angular.module('reportApp') content: '=' }, link: function(scope){ + // access map for experiments + scope.accessMap = ReportService.accessMap; // aliases scope.fields = scope.content.fields; // ids @@ -282,10 +284,10 @@ angular.module('reportApp') <tbody> <tr ng-repeat="exp in group.experiments | orderBy:sortFunc:sortField.isReversed"> <td ng-repeat='field in fields'> - <a ng-if='$index == 0 && getExperimentUrl(exp)' href='{{ getExperimentUrl(exp) }}'> + <a ng-if='$index == 0 && getExperimentUrl(exp) && accessMap[exp]' href='{{ getExperimentUrl(exp) }}'> {{ getFieldVal(exp, field) }} </a> - <span ng-if='!$index == 0 || !getExperimentUrl(exp)'> + <span ng-if='!$index == 0 || !getExperimentUrl(exp) || !accessMap[exp]'> {{ getFieldVal(exp, field) }} </span> </td> diff --git a/beat/web/reports/static/reports/app/services/reportService.js b/beat/web/reports/static/reports/app/services/reportService.js index 90240945348a60332096885e2afe7fcc4197d8c2..cedaa6791717058199d133d166b2753b7ad5ae57 100644 --- a/beat/web/reports/static/reports/app/services/reportService.js +++ b/beat/web/reports/static/reports/app/services/reportService.js @@ -50,6 +50,8 @@ angular.module('reportApp').factory('ReportService', ['GroupsService', 'plotterF rs.number = report.number; rs.author = report.author; rs.name = report.name.split('/').length > 1 ? report.name.split('/')[1] : null; + rs.accessMap = report.experiments.reduce((o, expName, i) => + ({...o, [expName]: report.experiment_access_map[i]}), {}); // start up our GroupsService GroupsService.loadGroups(report.content.groups); diff --git a/beat/web/reports/templates/reports/panels/actions.html b/beat/web/reports/templates/reports/panels/actions.html index 823d722c81f164b3b71c2af04373e4465555447a..38e4be3a35b1c6c4f5311d488ee4de337742d446 100644 --- a/beat/web/reports/templates/reports/panels/actions.html +++ b/beat/web/reports/templates/reports/panels/actions.html @@ -22,7 +22,7 @@ {% load report_tags %} {% with object.get_status_display as status %} -<div class="btn-group btn-group-sm action-buttons pull-right"> +<div class="btn-group btn-group-sm action-buttons pull-right" ng-controller='GroupsController as vm'> {% if display_count %} <!-- Experiment count, works for all --> @@ -39,7 +39,7 @@ {% if display_count %} <a class="btn btn-default btn-edit" href="{{ object.get_author_absolute_url }}" data-toggle="tooltip" data-placement="bottom" title="Edit"><i class="fa fa-edit fa-lg"></i></a> {% else %} - <a id="save-button" class="btn btn-default btn-save" data-toggle="tooltip" data-placement="bottom" title="Save" report-save><i class="fa fa-floppy-o fa-lg"></i></a> + <a id="save-button" class="btn btn-default btn-save" data-toggle="tooltip" data-placement="bottom" title="Save" report-save save-report='vm.saveReport'><i class="fa fa-floppy-o fa-lg"></i></a> <span class="btn btn-default" data-toggle='modal' data-target='#lockReportModal'> <a class="btn-report" data-toggle="tooltip" data-placement="bottom" title="Lock"> <i class="fa fa-lock fa-lg"></i> @@ -65,7 +65,7 @@ <a class="btn btn-default btn-view" href="{{ object.get_absolute_url }}" data-toggle="tooltip" data-placement="bottom" title="Review"><i class="fa fa-arrow-circle-right fa-lg"></i></a> {% if not display_count and status == 'Editable' %} - <report-lock></report-lock> + <report-lock save-report='vm.saveReport'></report-lock> {% endif %} {% if not display_count and status == 'Locked' %} diff --git a/beat/web/reports/tests.py b/beat/web/reports/tests.py index d68353be41d4a80053f771f9c427e7140c79f51f..65781d8639fdd6311da6fc7a809910af4cd32437 100755 --- a/beat/web/reports/tests.py +++ b/beat/web/reports/tests.py @@ -891,6 +891,7 @@ class EditableReportRetrievalTestCase(ReportTestCase): "status": "editable", "creation_date": self.report.creation_date.isoformat(), "publication_date": None, + "experiment_access_map": [], "experiments": [], "content": {}, "analyzer": None, @@ -942,6 +943,7 @@ class LockedReportRetrievalTestCase(ReportTestCase): "status": "locked", "creation_date": self.report.creation_date.isoformat(), "publication_date": None, + "experiment_access_map": [True], "experiments": [self.experiment_analyzer1.fullname()], "content": {}, "analyzer": None, @@ -989,6 +991,7 @@ class PublishedReportRetrievalTestCase(ReportTestCase): "status": "published", "creation_date": self.report.creation_date.isoformat(), "publication_date": self.report.publication_date.isoformat(), + "experiment_access_map": [False], "experiments": [self.experiment_analyzer1.fullname()], "content": {}, "analyzer": None, @@ -1010,6 +1013,7 @@ class PublishedReportRetrievalTestCase(ReportTestCase): "status": "published", "creation_date": self.report.creation_date.isoformat(), "publication_date": self.report.publication_date.isoformat(), + "experiment_access_map": [True], "experiments": [self.experiment_analyzer1.fullname()], "content": {}, "analyzer": None, @@ -1031,6 +1035,7 @@ class PublishedReportRetrievalTestCase(ReportTestCase): "is_owner": False, "author": self.johndoe.username, "status": "published", + "experiment_access_map": [False], "creation_date": self.report.creation_date.isoformat(), "publication_date": self.report.publication_date.isoformat(), "experiments": [self.experiment_analyzer1.fullname()], diff --git a/beat/web/ui/templates/ui/doc_editor.html b/beat/web/ui/templates/ui/doc_editor.html index eeb862327d2e406c25134fac10b5a359522578cd..a8490fe3bf6d2cf3c12a6154461dcbd2347e2efc 100644 --- a/beat/web/ui/templates/ui/doc_editor.html +++ b/beat/web/ui/templates/ui/doc_editor.html @@ -1,21 +1,21 @@ {% comment %} * 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/. {% endcomment %} @@ -40,6 +40,13 @@ <div id="description-edition" style="display:none;"> <form id="description-form" method="post" action="" class="form"> {% csrf_token %} + {% if url_name == 'api_reports:object' %} + <div class="form-group"> + <div class="alert alert-warning"> + <i class="fa fa-warning"></i> Saving changes to the documentation will discard any unsaved changes on the rest of the report. Make sure to save the report before editing this documentation. + </div> + </div> + {% endif %} <div class="form-group"> <label for="short_description">Short description</label> <input maxlength="100" size="80" class="form-control" id="short-description" value="{{ object.short_description }}">