diff --git a/.gitignore b/.gitignore index 082f3dd1da41ecaee958ad880b3fd0c8a7248935..c4d8c6b0fe46cf16c23bcd2283a0b2a54eaa6b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ doc/api/api/ html/ *.tar.bz2 nohup.out +*.log diff --git a/beat/web/.eslintrc.json b/beat/web/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..ac0d92684101bd13eb9010a856e8755e6ef4c1f5 --- /dev/null +++ b/beat/web/.eslintrc.json @@ -0,0 +1,43 @@ +{ + "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 + } +} diff --git a/beat/web/.tern-project b/beat/web/.tern-project new file mode 100644 index 0000000000000000000000000000000000000000..d9ba4ab68143517bced493ef479e21273257b513 --- /dev/null +++ b/beat/web/.tern-project @@ -0,0 +1,9 @@ +{ + "libs": [ + "browser", + "jquery" + ], + "plugins": { + "angular": {} + } +} diff --git a/beat/web/experiments/static/experiments/js/utils.js b/beat/web/experiments/static/experiments/js/utils.js index 5f9bef84624325b9248078af735476487f187df0..7e3de93baa5cbcc736d8e89950fc65cccd0741ba 100644 --- a/beat/web/experiments/static/experiments/js/utils.js +++ b/beat/web/experiments/static/experiments/js/utils.js @@ -24,17 +24,12 @@ Experiment-related utility classes and functions */ - // Declaration of our namespaces -if (beat === undefined) - var beat = {} - -if (beat.experiments === undefined) - beat.experiments = {} +if (beat === undefined) var beat = {}; -if (beat.experiments.utils === undefined) - beat.experiments.utils = {} +if (beat.experiments === undefined) beat.experiments = {}; +if (beat.experiments.utils === undefined) beat.experiments.utils = {}; //------------------------------------------------------------------------------ // Display an experiment plot in a DOM element @@ -58,1612 +53,1587 @@ if (beat.experiments.utils === undefined) // 'height': 200, // } //------------------------------------------------------------------------------ -beat.experiments.utils.displayPlot = function(prefix, container, value, available_plotters, - available_plotter_parameter, replace_container_content, callback) -{ - var table_element = null; - var state_merged = value.merged; - if(state_merged == undefined) - state_merged = true; - - // Function to regenerate graph - function _regenerate_graph(plotter_selector) - { - var data = JSON.parse(JSON.stringify(value)); - - //sample data plot or real plot required? - var default_post_prefix = '/plotters/plot/'; - var sampledata_post_prefix = '/plotters/plot_sample/'; - var sampledatawithparams_post_prefix = '/plotters/plot_sample_with_params/'; - var post_prefix = default_post_prefix; - if('sample_data' in data) - { - post_prefix = sampledata_post_prefix; - - if('dynamic_params' in data) - post_prefix = sampledatawithparams_post_prefix; - } - - data.content_type = 'image/png'; - data.base64 = true; - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - $.ajax({ - type: "POST", - url: prefix + post_prefix, - data: data, - - success: function(data) { - if (replace_container_content) - { - while (container.firstChild) - container.removeChild(container.firstChild); - } - - if (table_element !== null) - $(table_element).remove(); - - // adds table that will have the two elements - table_element = document.createElement('table'); - table_element.className = 'plotter'; - container.appendChild(table_element); - - var tr = document.createElement('tr'); - table_element.appendChild(tr); - - var td = document.createElement('td'); - td.appendChild(plotter_selector); - tr.appendChild(td); - - // adds image place holder - var img = document.createElement('img'); - img.id = container.id + '_graph'; - //img.width = value['width']; - //img.height = value['height']; - img.className = 'img-responsive'; - img.src = "data:" + value['content_type'] + ";base64," + data; - - tr = document.createElement('tr'); - table_element.appendChild(tr); - - td = document.createElement('td'); - td.appendChild(img); - tr.appendChild(td); - - $(container).removeClass('progress'); - }, - - statusCode: { - 404: function(data, status, message) { - container.textContent = message + ' (' + data.status + ')'; - $(container).addClass('error'); - $(container).removeClass('progress'); - }, - 500: function(data, status, message) { - container.textContent = message + ' (' + data.status + ')'; - $(container).addClass('error'); - $(container).removeClass('progress'); - }, - }, - - }); - - } - - // Function to regenerate multiple graphs - function _regenerate_multiple_graph(plotter_selector) - { - var data = JSON.parse(JSON.stringify(value)); - data.content_type = 'image/png'; - data.base64 = true; - - // create the containers - if (replace_container_content) - { - while (container.firstChild) - container.removeChild(container.firstChild); - } - - if (table_element !== null) - $(table_element).remove(); - - // adds table that will have the two elements - table_element = document.createElement('table'); - table_element.className = 'plotter'; - container.appendChild(table_element); - - var tr = document.createElement('tr'); - table_element.appendChild(tr); - - var td = document.createElement('td'); - td.appendChild(plotter_selector); - tr.appendChild(td); - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - function getPlot(_data) - { - $.ajax({ - type: "POST", - url: prefix + '/plotters/plot/', - data: _data, - - success: function(data) { - - if(_data.experiment.length == 1) - { - var tr_title = document.createElement('tr'); - table_element.appendChild(tr_title); - - var td_title = document.createElement('td'); - td_title.className = "td_title" - td_title.textContent = _data.legend; - tr_title.appendChild(td_title); - } - // adds image place holder - var img = document.createElement('img'); - img.id = container.id + '_graph'; - //img.width = value['width']; - //img.height = value['height']; - img.className = 'img-responsive chart'; - img.src = "data:" + value['content_type'] + ";base64," + data; - - tr = document.createElement('tr'); - table_element.appendChild(tr); - - td = document.createElement('td'); - td.appendChild(img); - tr.appendChild(td); - - $(container).removeClass('progress'); - }, - - statusCode: { - 404: function(data, status, message) { - container.textContent = message + ' (' + data.status + ')'; - $(container).addClass('error'); - $(container).removeClass('progress'); - }, - 500: function(data, status, message) { - container.textContent = message + ' (' + data.status + ')'; - $(container).addClass('error'); - $(container).removeClass('progress'); - }, - }, - - }); - } - - if(!state_merged) - { - for(var i = 0; i < data.experiment.length; ++i) - { - // This step is required to copy elements from object in javascript! - // if we do a basic copy using "=" we have a reference to the object (pointer) - var data_per_experiment = JSON.parse(JSON.stringify(data)); - - data_per_experiment.experiment = [data.experiment[i]]; - if(data.legend != undefined) - { - data_per_experiment.legend = data.legend.split("&")[i]; - } - else - { - data_per_experiment.legend = data.experiment[i]; - } - - getPlot(data_per_experiment); - } - } - else - { - getPlot(data); - } - - } - - - // Creates a selector box for chart plotters and parameters - function _create_selector(what, selected, options, other_what, other_selected, other_options) - { - var span = document.createElement('span'); - - if (options.length > 1) - { - var title = document.createElement('span'); - //title.innerHTML = 'Plotter: '; - title.innerHTML = 'Plotter: '; - span.appendChild(title); - - var selector = document.createElement('select'); - selector.id = container.id + '_settings_select'; - selector.className = 'settings_select'; - span.appendChild(selector); - - for (var i = 0; i < options.length; ++i) - { - var opt = document.createElement('option'); - opt.value = options[i]; - opt.innerHTML = options[i]; - - if (options[i] === selected) - opt.selected = true; - - selector.appendChild(opt); - } - - $(selector).change(function() { - value[what] = $(selector).val(); - - var plotter_selector = _create_selector(what, value[what], available_plotters, other_what, - value[other_what], available_plotter_parameter); - _regenerate_graph(plotter_selector); - - if (callback) - callback($(selector).val(), value[other_what]); - }); - } - - if (other_options.length > 1) - { - var title = document.createElement('span'); - //title.innerHTML = 'Plotter: '; - title.innerHTML = ' Plotter parameter: '; - span.appendChild(title); - - var selector_plotterparameter = document.createElement('select'); - selector_plotterparameter.id = container.id + '_settings_select_plotterparameter'; - selector_plotterparameter.className = 'settings_select_plotterparameter'; - span.appendChild(selector_plotterparameter); - - for (var i = 0; i < other_options.length; ++i) - { - var opt = document.createElement('option'); - opt.value = other_options[i]; - opt.innerHTML = other_options[i]; - - if (other_options[i] === other_selected) - { - opt.selected = true; - } - - selector_plotterparameter.appendChild(opt); - } - - $(selector_plotterparameter).change(function() { - value[other_what] = $(selector_plotterparameter).val(); - - var plotter_selector = _create_selector(what, value[what], available_plotters, other_what, - value[other_what], available_plotter_parameter); - _regenerate_graph(plotter_selector); - - if (callback) - callback(value[what], $(selector_plotterparameter).val()); - }); - } - - return span; - } - - // Creates a selector box for chart plotters and parameters - function _create_selector_with_merge_button(what, selected, options, other_what, other_selected, other_options, multiple_experiments_flag, expand_plots) - { - var span = document.createElement('span'); - - if (options.length > 1) - { - var title = document.createElement('span'); - //title.innerHTML = 'Plotter: '; - title.innerHTML = 'Plotter: '; - span.appendChild(title); - - var selector = document.createElement('select'); - selector.id = container.id + '_settings_select'; - selector.className = 'settings_select'; - span.appendChild(selector); - - for (var i = 0; i < options.length; ++i) - { - var opt = document.createElement('option'); - opt.value = options[i]; - opt.innerHTML = options[i]; - - if (options[i] === selected) - opt.selected = true; - - selector.appendChild(opt); - } - - $(selector).change(function() { - value[what] = $(selector).val(); - - var plotter_selector = null; - if(!multiple_experiments_flag) - plotter_selector = _create_selector_with_merge_button(what, value[what], available_plotters, other_what, - value[other_what], available_plotter_parameter, false); - else - plotter_selector = _create_selector_with_merge_button(what, value[what], available_plotters, other_what, - value[other_what], available_plotter_parameter, true); - _regenerate_multiple_graph(plotter_selector); - - if (callback) - callback($(selector).val(), value[other_what]); - }); - } - - if (other_options.length > 1) - { - var title = document.createElement('span'); - //title.innerHTML = 'Plotter: '; - title.innerHTML = ' Plotter parameter: '; - span.appendChild(title); - - var selector_plotterparameter = document.createElement('select'); - selector_plotterparameter.id = container.id + '_settings_select_plotterparameter'; - selector_plotterparameter.className = 'settings_select_plotterparameter'; - span.appendChild(selector_plotterparameter); - - for (var i = 0; i < other_options.length; ++i) - { - var opt = document.createElement('option'); - opt.value = other_options[i]; - opt.innerHTML = other_options[i]; - - if (other_options[i] === other_selected) - { - opt.selected = true; - } - - selector_plotterparameter.appendChild(opt); - } - - $(selector_plotterparameter).change(function() { - value[other_what] = $(selector_plotterparameter).val(); - - var plotter_selector = null; - if(!multiple_experiments_flag) - plotter_selector = _create_selector_with_merge_button(what, value[what], available_plotters, other_what, - value[other_what], available_plotter_parameter, false); - else - plotter_selector = _create_selector_with_merge_button(what, value[what], available_plotters, other_what, - value[other_what], available_plotter_parameter, true, !value["merged"]); - _regenerate_multiple_graph(plotter_selector); - - if (callback) - callback(value[what], $(selector_plotterparameter).val(), value["merged"]); - }); - - var button_merge = document.createElement('a'); - button_merge.id = container.id + '_button_merge_a'; - - if(expand_plots) - { - button_merge.className = 'btn btn-xs btn-primary merge_unmerge expand'; - button_merge.innerHTML = "<i class='fa fa-compress fa-lg'></i> Merge"; - } - else - { - button_merge.className = 'btn btn-xs btn-primary merge_unmerge merge'; - button_merge.innerHTML = "<i class='fa fa-expand fa-lg'></i> Expand"; - } - span.appendChild(button_merge); - - $(button_merge).click(function() { - if(! $(button_merge).hasClass("expand")) - { - $(button_merge).addClass("expand"); - $(button_merge).removeClass("merge"); - $(button_merge).children().removeClass("fa-compress"); - $(button_merge).children().addClass("fa-expand"); - var plotter_selector = _create_selector_with_merge_button("plotter", value["plotter"], available_plotters, "parameter", value["parameter"], available_plotter_parameter, true, true); - state_merged = false; - } - else - { - $(button_merge).addClass("merge"); - $(button_merge).removeClass("expand"); - $(button_merge).children().removeClass("fa-expand"); - $(button_merge).children().addClass("fa-compress"); - var plotter_selector = _create_selector_with_merge_button("plotter", value["plotter"], available_plotters, "parameter", value["parameter"], available_plotter_parameter, true, false); - state_merged = true; - } - value.merged = state_merged; - _regenerate_multiple_graph(plotter_selector); - if (callback) - callback(value["plotter"], value["parameter"], value["merged"]); - //callback(value["plotter"], $(plotter_selector).val()); - - }); - - - } - - return span; - } - - if(Array.isArray(value.experiment) && value.experiment.length > 1) - { - //creates button merge/unmerge and plotter/plotter_parameter selector - var plotter_selector = _create_selector_with_merge_button("plotter", value["plotter"], available_plotters, "parameter", value["parameter"], available_plotter_parameter, true, !state_merged); - _regenerate_multiple_graph(plotter_selector); - } - else - { - // creates plotter/plotter_parameter selector - var plotter_selector = _create_selector("plotter", value["plotter"], available_plotters, "parameter", value["parameter"], available_plotter_parameter); - _regenerate_graph(plotter_selector); - - } - -} - +beat.experiments.utils.displayPlot = function( + prefix, + container, + value, + available_plotters, + available_plotter_parameter, + replace_container_content, + callback +) { + var table_element = null; + var state_merged = value.merged; + if (state_merged == undefined) state_merged = true; + + // Function to regenerate graph + function _regenerate_graph(plotter_selector) { + var data = JSON.parse(JSON.stringify(value)); + + //sample data plot or real plot required? + var default_post_prefix = '/plotters/plot/'; + var sampledata_post_prefix = '/plotters/plot_sample/'; + var sampledatawithparams_post_prefix = '/plotters/plot_sample_with_params/'; + var post_prefix = default_post_prefix; + if ('sample_data' in data) { + post_prefix = sampledata_post_prefix; + + if ('dynamic_params' in data) + post_prefix = sampledatawithparams_post_prefix; + } + + data.content_type = 'image/png'; + data.base64 = true; + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + $.ajax({ + type: 'POST', + url: prefix + post_prefix, + data: data, + + success: function(data) { + if (replace_container_content) { + while (container.firstChild) + container.removeChild(container.firstChild); + } + + if (table_element !== null) $(table_element).remove(); + + // adds table that will have the two elements + table_element = document.createElement('table'); + table_element.className = 'plotter'; + container.appendChild(table_element); + + var tr = document.createElement('tr'); + var td = document.createElement('td'); + + console.log(value); + if(!value.report_number){ + table_element.appendChild(tr); + + var td = document.createElement('td'); + td.appendChild(plotter_selector); + tr.appendChild(td); + } + + // adds image place holder + var img = document.createElement('img'); + img.id = container.id + '_graph'; + //img.width = value['width']; + //img.height = value['height']; + img.className = 'img-responsive'; + img.src = 'data:' + value['content_type'] + ';base64,' + data; + + tr = document.createElement('tr'); + table_element.appendChild(tr); + + td = document.createElement('td'); + td.appendChild(img); + tr.appendChild(td); + + $(container).removeClass('progress'); + }, + + statusCode: { + 404: function(data, status, message) { + container.textContent = message + ' (' + data.status + ')'; + $(container).addClass('error'); + $(container).removeClass('progress'); + }, + 500: function(data, status, message) { + container.textContent = message + ' (' + data.status + ')'; + $(container).addClass('error'); + $(container).removeClass('progress'); + }, + }, + }); + } + + // Function to regenerate multiple graphs + function _regenerate_multiple_graph(plotter_selector) { + var data = JSON.parse(JSON.stringify(value)); + data.content_type = 'image/png'; + data.base64 = true; + + // create the containers + if (replace_container_content) { + while (container.firstChild) container.removeChild(container.firstChild); + } + + if (table_element !== null) $(table_element).remove(); + + // adds table that will have the two elements + table_element = document.createElement('table'); + table_element.className = 'plotter'; + container.appendChild(table_element); + + var tr = document.createElement('tr'); + var td = document.createElement('td'); + + console.log(value); + if(!value.report_number){ + table_element.appendChild(tr); + + var td = document.createElement('td'); + td.appendChild(plotter_selector); + tr.appendChild(td); + } + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + function getPlot(_data) { + $.ajax({ + type: 'POST', + url: prefix + '/plotters/plot/', + data: _data, + + success: function(data) { + if (_data.experiment.length == 1) { + var tr_title = document.createElement('tr'); + table_element.appendChild(tr_title); + + var td_title = document.createElement('td'); + td_title.className = 'td_title'; + const title_h3 = document.createElement('h3'); + title_h3.textContent = _data.legend; + td_title.appendChild(title_h3); + tr_title.appendChild(td_title); + } + // adds image place holder + var img = document.createElement('img'); + img.id = container.id + '_graph'; + //img.width = value['width']; + //img.height = value['height']; + img.className = 'img-responsive chart'; + img.src = 'data:' + value['content_type'] + ';base64,' + data; + + tr = document.createElement('tr'); + table_element.appendChild(tr); + + td = document.createElement('td'); + td.appendChild(img); + tr.appendChild(td); + + $(container).removeClass('progress'); + }, + + statusCode: { + 404: function(data, status, message) { + container.textContent = message + ' (' + data.status + ')'; + $(container).addClass('error'); + $(container).removeClass('progress'); + }, + 500: function(data, status, message) { + container.textContent = message + ' (' + data.status + ')'; + $(container).addClass('error'); + $(container).removeClass('progress'); + }, + }, + }); + } + + if (!state_merged) { + for (var i = 0; i < data.experiment.length; ++i) { + // This step is required to copy elements from object in javascript! + // if we do a basic copy using "=" we have a reference to the object (pointer) + var data_per_experiment = JSON.parse(JSON.stringify(data)); + + data_per_experiment.experiment = [data.experiment[i]]; + if (data.legend != undefined) { + data_per_experiment.legend = data.legend.split('&')[i]; + } else { + data_per_experiment.legend = data.experiment[i]; + } + + getPlot(data_per_experiment); + } + } else { + getPlot(data); + } + } + + // Creates a selector box for chart plotters and parameters + function _create_selector( + what, + selected, + options, + other_what, + other_selected, + other_options + ) { + var span = document.createElement('span'); + + if (options.length > 1) { + var title = document.createElement('span'); + //title.innerHTML = 'Plotter: '; + title.innerHTML = 'Plotter: '; + span.appendChild(title); + + var selector = document.createElement('select'); + selector.id = container.id + '_settings_select'; + selector.className = 'settings_select'; + span.appendChild(selector); + + for (var i = 0; i < options.length; ++i) { + var opt = document.createElement('option'); + opt.value = options[i]; + opt.innerHTML = options[i]; + + if (options[i] === selected) opt.selected = true; + + selector.appendChild(opt); + } + + $(selector).change(function() { + value[what] = $(selector).val(); + + var plotter_selector = _create_selector( + what, + value[what], + available_plotters, + other_what, + value[other_what], + available_plotter_parameter + ); + _regenerate_graph(plotter_selector); + + if (callback) callback($(selector).val(), value[other_what]); + }); + } + + if (other_options.length > 1) { + var title = document.createElement('span'); + //title.innerHTML = 'Plotter: '; + title.innerHTML = ' Plotter parameter: '; + span.appendChild(title); + + var selector_plotterparameter = document.createElement('select'); + selector_plotterparameter.id = + container.id + '_settings_select_plotterparameter'; + selector_plotterparameter.className = 'settings_select_plotterparameter'; + span.appendChild(selector_plotterparameter); + + for (var i = 0; i < other_options.length; ++i) { + var opt = document.createElement('option'); + opt.value = other_options[i]; + opt.innerHTML = other_options[i]; + + if (other_options[i] === other_selected) { + opt.selected = true; + } + + selector_plotterparameter.appendChild(opt); + } + + $(selector_plotterparameter).change(function() { + value[other_what] = $(selector_plotterparameter).val(); + + var plotter_selector = _create_selector( + what, + value[what], + available_plotters, + other_what, + value[other_what], + available_plotter_parameter + ); + _regenerate_graph(plotter_selector); + + if (callback) callback(value[what], $(selector_plotterparameter).val()); + }); + } + + return span; + } + + // Creates a selector box for chart plotters and parameters + function _create_selector_with_merge_button( + what, + selected, + options, + other_what, + other_selected, + other_options, + multiple_experiments_flag, + expand_plots + ) { + var span = document.createElement('span'); + + if (options.length > 1) { + var title = document.createElement('span'); + //title.innerHTML = 'Plotter: '; + title.innerHTML = 'Plotter: '; + span.appendChild(title); + + var selector = document.createElement('select'); + selector.id = container.id + '_settings_select'; + selector.className = 'settings_select'; + span.appendChild(selector); + + for (var i = 0; i < options.length; ++i) { + var opt = document.createElement('option'); + opt.value = options[i]; + opt.innerHTML = options[i]; + + if (options[i] === selected) opt.selected = true; + + selector.appendChild(opt); + } + + $(selector).change(function() { + value[what] = $(selector).val(); + + var plotter_selector = null; + if (!multiple_experiments_flag) + plotter_selector = _create_selector_with_merge_button( + what, + value[what], + available_plotters, + other_what, + value[other_what], + available_plotter_parameter, + false + ); + else + plotter_selector = _create_selector_with_merge_button( + what, + value[what], + available_plotters, + other_what, + value[other_what], + available_plotter_parameter, + true + ); + _regenerate_multiple_graph(plotter_selector); + + if (callback) callback($(selector).val(), value[other_what]); + }); + } + + if (other_options.length > 1) { + var title = document.createElement('span'); + //title.innerHTML = 'Plotter: '; + title.innerHTML = ' Plotter parameter: '; + span.appendChild(title); + + var selector_plotterparameter = document.createElement('select'); + selector_plotterparameter.id = + container.id + '_settings_select_plotterparameter'; + selector_plotterparameter.className = 'settings_select_plotterparameter'; + span.appendChild(selector_plotterparameter); + + for (var i = 0; i < other_options.length; ++i) { + var opt = document.createElement('option'); + opt.value = other_options[i]; + opt.innerHTML = other_options[i]; + + if (other_options[i] === other_selected) { + opt.selected = true; + } + + selector_plotterparameter.appendChild(opt); + } + + $(selector_plotterparameter).change(function() { + value[other_what] = $(selector_plotterparameter).val(); + + var plotter_selector = null; + if (!multiple_experiments_flag) + plotter_selector = _create_selector_with_merge_button( + what, + value[what], + available_plotters, + other_what, + value[other_what], + available_plotter_parameter, + false + ); + else + plotter_selector = _create_selector_with_merge_button( + what, + value[what], + available_plotters, + other_what, + value[other_what], + available_plotter_parameter, + true, + !value['merged'] + ); + _regenerate_multiple_graph(plotter_selector); + + if (callback) + callback( + value[what], + $(selector_plotterparameter).val(), + value['merged'] + ); + }); + } + + var button_merge = document.createElement('a'); + button_merge.id = container.id + '_button_merge_a'; + + if (expand_plots) { + button_merge.className = 'btn btn-xs btn-primary merge_unmerge expand'; + button_merge.innerHTML = "<i class='fa fa-compress fa-lg'></i> Merge"; + } else { + button_merge.className = 'btn btn-xs btn-primary merge_unmerge merge'; + button_merge.innerHTML = "<i class='fa fa-expand fa-lg'></i> Expand"; + } + span.appendChild(button_merge); + + $(button_merge).click(function() { + if (!$(button_merge).hasClass('expand')) { + $(button_merge).addClass('expand'); + $(button_merge).removeClass('merge'); + $(button_merge).children().removeClass('fa-compress'); + $(button_merge).children().addClass('fa-expand'); + var plotter_selector = _create_selector_with_merge_button( + 'plotter', + value['plotter'], + available_plotters, + 'parameter', + value['parameter'], + available_plotter_parameter, + true, + true + ); + state_merged = false; + } else { + $(button_merge).addClass('merge'); + $(button_merge).removeClass('expand'); + $(button_merge).children().removeClass('fa-expand'); + $(button_merge).children().addClass('fa-compress'); + var plotter_selector = _create_selector_with_merge_button( + 'plotter', + value['plotter'], + available_plotters, + 'parameter', + value['parameter'], + available_plotter_parameter, + true, + false + ); + state_merged = true; + } + value.merged = state_merged; + _regenerate_multiple_graph(plotter_selector); + if (callback) + callback(value['plotter'], value['parameter'], value['merged']); + //callback(value["plotter"], $(plotter_selector).val()); + }); + + return span; + } + + if (Array.isArray(value.experiment) && value.experiment.length > 1) { + //creates button merge/unmerge and plotter/plotter_parameter selector + var plotter_selector = _create_selector_with_merge_button( + 'plotter', + value['plotter'], + available_plotters, + 'parameter', + value['parameter'], + available_plotter_parameter, + true, + !state_merged + ); + _regenerate_multiple_graph(plotter_selector); + } else { + // creates plotter/plotter_parameter selector + var plotter_selector = _create_selector( + 'plotter', + value['plotter'], + available_plotters, + 'parameter', + value['parameter'], + available_plotter_parameter + ); + _regenerate_graph(plotter_selector); + } +}; //----------------------------------------------- - -beat.experiments.utils.getPlotData = function(prefix, value, available_plotters, - available_plotter_parameter, content_type, callback) -{ - var selected_content_type = ""; - if(content_type == 'PNG') - { - selected_content_type = 'image/png'; - } - else if(content_type == 'JPEG') - { - selected_content_type = 'image/jpeg'; - } - else if(content_type == 'PDF') - { - selected_content_type = 'application/pdf'; - } - - var state_merged = value.merged; - if(state_merged == undefined) - state_merged = true; - - - var data = JSON.parse(JSON.stringify(value)); - data.content_type = selected_content_type; - data.base64 = true; - - // Function to regenerate graph - function _fetch_graph(_data) - { - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - var content_type = selected_content_type; - $.ajax({ - type: "GET", - url: prefix + '/plotters/plot/', - data: _data, - - success: function(data) { - - var returned_data = "data:" + content_type + ";base64," + data; - if (callback) - callback(returned_data, content_type); - - }, - - statusCode: { - 404: function(data, status, message) { - container.textContent = message + ' (' + data.status + ')'; - $(container).addClass('error'); - $(container).removeClass('progress'); - }, - 500: function(data, status, message) { - container.textContent = message + ' (' + data.status + ')'; - $(container).addClass('error'); - $(container).removeClass('progress'); - }, - }, - - }); - } - - if(!state_merged) - { - for(var i = 0; i < data.experiment.length; ++i) - { - // This step is required to copy elements from object in javascript! - // if we do a basic copy using "=" we have a reference to the object (pointer) - var data_per_experiment = JSON.parse(JSON.stringify(data)); - - data_per_experiment.experiment = [data.experiment[i]]; - data_per_experiment.legend = data.legend.split("&")[i]; - - _fetch_graph(data_per_experiment); - } - } - else - { - _fetch_graph(data); - } -} - +beat.experiments.utils.getPlotData = function( + prefix, + value, + available_plotters, + available_plotter_parameter, + content_type, + callback +) { + var selected_content_type = ''; + if (content_type == 'PNG') { + selected_content_type = 'image/png'; + } else if (content_type == 'JPEG') { + selected_content_type = 'image/jpeg'; + } else if (content_type == 'PDF') { + selected_content_type = 'application/pdf'; + } + + var state_merged = value.merged; + if (state_merged == undefined) state_merged = true; + + var data = JSON.parse(JSON.stringify(value)); + data.content_type = selected_content_type; + data.base64 = true; + + // Function to regenerate graph + function _fetch_graph(_data) { + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + var content_type = selected_content_type; + $.ajax({ + type: 'GET', + url: prefix + '/plotters/plot/', + data: _data, + + success: function(data) { + var returned_data = 'data:' + content_type + ';base64,' + data; + if (callback) callback(returned_data, content_type); + }, + + statusCode: { + 404: function(data, status, message) { + if (typeof container !== 'undefined') { + container.textContent = message + ' (' + data.status + ')'; + $(container).addClass('error'); + $(container).removeClass('progress'); + } else { + throw new Error(message); + } + }, + 500: function(data, status, message) { + if (typeof container !== 'undefined') { + container.textContent = message + ' (' + data.status + ')'; + $(container).addClass('error'); + $(container).removeClass('progress'); + } else { + throw new Error(data.responseText); + } + }, + }, + }); + } + + if (!state_merged) { + for (var i = 0; i < data.experiment.length; ++i) { + // This step is required to copy elements from object in javascript! + // if we do a basic copy using "=" we have a reference to the object (pointer) + var data_per_experiment = JSON.parse(JSON.stringify(data)); + + data_per_experiment.experiment = [data.experiment[i]]; + data_per_experiment.legend = data.legend.split('&')[i]; + + _fetch_graph(data_per_experiment); + } + } else { + _fetch_graph(data); + } +}; //----------------------------------------------- - -beat.experiments.utils.displayResult = function(element, value, type) -{ - if (value === null) return; - - if (type == 'float32') - { - if ((value == '+inf') || (value == '-inf') || (value == 'NaN')) - { - $(element).text('' + value); - } - else - { - var n = parseFloat(value); - if (n <= 1.0) - $(element).text('' + parseFloat((parseFloat(value).toPrecision(3)))); - else - $(element).text('' + parseFloat((parseFloat(value).toFixed(3)))); - } - } - else if (type == 'int32') - { - $(element).text('' + value); - } - else if (type == 'bool') - { - $(element).text(value ? 'True' : 'False'); - } - else if (type == 'string') - { - $(element).html(value.replace(/\n/g, '<br />')); - } - else - { - $(element).text('ERROR (invalid type)'); - element.style.color = '#FF0000'; - } -} - +beat.experiments.utils.displayResult = function(element, value, type) { + if (value === null) return; + + if (type == 'float32') { + if (value == '+inf' || value == '-inf' || value == 'NaN') { + $(element).text('' + value); + } else { + var n = parseFloat(value); + if (n <= 1.0) + $(element).text('' + parseFloat(parseFloat(value).toPrecision(3))); + else $(element).text('' + parseFloat(parseFloat(value).toFixed(3))); + } + } else if (type == 'int32') { + $(element).text('' + value); + } else if (type == 'bool') { + $(element).text(value ? 'True' : 'False'); + } else if (type == 'string') { + $(element).html(value.replace(/\n/g, '<br />')); + } else { + $(element).text('ERROR (invalid type)'); + element.style.color = '#FF0000'; + } +}; //----------------------------------------------- +beat.experiments.utils.getBlockInputSignature = function(block) { + var block_inputs_list = []; -beat.experiments.utils.getBlockInputSignature = function(block) -{ - var block_inputs_list = []; - - for (var i = 0; i < block.nbInputs(); ++i) - { - var input = block.inputs[i]; - - block_inputs_list.push({ - name: input.name, - dataformat: input.connections[0].output.dataformat, - channel: input.channel, - }); - } + for (var i = 0; i < block.nbInputs(); ++i) { + var input = block.inputs[i]; - return beat.experiments.utils.getEntryPointsSignature(block_inputs_list); -} + block_inputs_list.push({ + name: input.name, + dataformat: input.connections[0].output.dataformat, + channel: input.channel, + }); + } + return beat.experiments.utils.getEntryPointsSignature(block_inputs_list); +}; //----------------------------------------------- +beat.experiments.utils.getBlockOutputSignature = function(block) { + var block_outputs_list = []; -beat.experiments.utils.getBlockOutputSignature = function(block) -{ - var block_outputs_list = []; + for (var i = 0; i < block.nbOutputs(); ++i) { + var output = block.outputs[i]; - for (var i = 0; i < block.nbOutputs(); ++i) - { - var output = block.outputs[i]; - - block_outputs_list.push({ - name: output.name, - dataformat: (output.connections.length > 0 ? output.connections[0].input.dataformat : null), - channel: output.channel, - }) - } - - return beat.experiments.utils.getEntryPointsSignature(block_outputs_list); -} + block_outputs_list.push({ + name: output.name, + dataformat: output.connections.length > 0 + ? output.connections[0].input.dataformat + : null, + channel: output.channel, + }); + } + return beat.experiments.utils.getEntryPointsSignature(block_outputs_list); +}; //----------------------------------------------- +beat.experiments.utils.getEntryPointsSignature = function(entry_points) { + var signature = {}; -beat.experiments.utils.getEntryPointsSignature = function(entry_points) -{ - var signature = {}; - - for (var i = 0; i < entry_points.length; ++i) - { - var entry = entry_points[i]; + for (var i = 0; i < entry_points.length; ++i) { + var entry = entry_points[i]; - if (signature[entry.channel] === undefined) - signature[entry.channel] = {}; + if (signature[entry.channel] === undefined) signature[entry.channel] = {}; - if (signature[entry.channel][entry.dataformat] !== undefined) - signature[entry.channel][entry.dataformat] += 1; - else - signature[entry.channel][entry.dataformat] = 1; - } - - return signature; -} + if (signature[entry.channel][entry.dataformat] !== undefined) + signature[entry.channel][entry.dataformat] += 1; + else signature[entry.channel][entry.dataformat] = 1; + } + return signature; +}; //----------------------------------------------- - -beat.experiments.utils.analyzeCompatibility = function(block_inputs_signature, - block_outputs_signature, - algorithm_inputs_signature, - algorithm_outputs_signature, - dataformats_list) -{ - function _getInfos(names, input_signatures, output_signatures) - { - var result = {}; - for (var i = 0; i < names.length; ++i) - { - var name = names[i]; - - var entry = { - nb_inputs: 0, - nb_outputs: 0, - }; - - if (input_signatures[name] !== undefined) - { - var dataformats = Object.keys(input_signatures[name]); - for (var j = 0; j < dataformats.length; ++j) - entry.nb_inputs += input_signatures[name][dataformats[j]]; - } - - if (output_signatures[name] !== undefined) - { - var dataformats = Object.keys(output_signatures[name]); - for (var j = 0; j < dataformats.length; ++j) - entry.nb_outputs += output_signatures[name][dataformats[j]]; - } - - result[name] = entry; - } - - return result; - } - - - var input_channels = Object.keys(block_inputs_signature); - var output_channels = Object.keys(block_outputs_signature); - - var input_groups = Object.keys(algorithm_inputs_signature); - var output_groups = Object.keys(algorithm_outputs_signature); - - // Check that the number of channels/groups match - if ((input_channels.length != input_groups.length) || - (output_channels.length != output_groups.length)) - return null; - - // Find the compatible channel/group associations - var channel_names = input_channels.concat(output_channels).unique(); - var group_names = input_groups.concat(output_groups).unique(); - - var channels = _getInfos(channel_names, block_inputs_signature, block_outputs_signature); - var groups = _getInfos(group_names, algorithm_inputs_signature, algorithm_outputs_signature); - - var mapping = {}; - - for (var i = 0; i < channel_names.length; ++i) - { - var channel = channel_names[i]; - - var matches = []; - for (var j = 0; j < group_names.length; ++j) - { - var group = group_names[j]; - - if ((channels[channel].nb_inputs != groups[group].nb_inputs) || - (channels[channel].nb_outputs != groups[group].nb_outputs)) - continue; - - if (channels[channel].nb_inputs > 0) - { - if (!beat.experiments.utils.compareSignatures(algorithm_inputs_signature[group], - block_inputs_signature[channel], - dataformats_list)) - { - continue; - } - } - - if (channels[channel].nb_outputs > 0) - { - if (!beat.experiments.utils.compareSignatures(block_outputs_signature[channel], - algorithm_outputs_signature[group], - dataformats_list)) - { - continue; - } - } - - matches.push(group); - } - - if (matches.length == 0) - return null; - - mapping[channel] = matches; - } - - var smart_mapping = new beat.experiments.utils.SmartMapping(mapping, - block_inputs_signature, - block_outputs_signature, - algorithm_inputs_signature, - algorithm_outputs_signature); - - if (smart_mapping.isValid()) - return smart_mapping; - - return null; -} - +beat.experiments.utils.analyzeCompatibility = function( + block_inputs_signature, + block_outputs_signature, + algorithm_inputs_signature, + algorithm_outputs_signature, + dataformats_list +) { + function _getInfos(names, input_signatures, output_signatures) { + var result = {}; + for (var i = 0; i < names.length; ++i) { + var name = names[i]; + + var entry = { + nb_inputs: 0, + nb_outputs: 0, + }; + + if (input_signatures[name] !== undefined) { + var dataformats = Object.keys(input_signatures[name]); + for (var j = 0; j < dataformats.length; ++j) + entry.nb_inputs += input_signatures[name][dataformats[j]]; + } + + if (output_signatures[name] !== undefined) { + var dataformats = Object.keys(output_signatures[name]); + for (var j = 0; j < dataformats.length; ++j) + entry.nb_outputs += output_signatures[name][dataformats[j]]; + } + + result[name] = entry; + } + + return result; + } + + var input_channels = Object.keys(block_inputs_signature); + var output_channels = Object.keys(block_outputs_signature); + + var input_groups = Object.keys(algorithm_inputs_signature); + var output_groups = Object.keys(algorithm_outputs_signature); + + // Check that the number of channels/groups match + if ( + input_channels.length != input_groups.length || + output_channels.length != output_groups.length + ) + return null; + + // Find the compatible channel/group associations + var channel_names = input_channels.concat(output_channels).unique(); + var group_names = input_groups.concat(output_groups).unique(); + + var channels = _getInfos( + channel_names, + block_inputs_signature, + block_outputs_signature + ); + var groups = _getInfos( + group_names, + algorithm_inputs_signature, + algorithm_outputs_signature + ); + + var mapping = {}; + + for (var i = 0; i < channel_names.length; ++i) { + var channel = channel_names[i]; + + var matches = []; + for (var j = 0; j < group_names.length; ++j) { + var group = group_names[j]; + + if ( + channels[channel].nb_inputs != groups[group].nb_inputs || + channels[channel].nb_outputs != groups[group].nb_outputs + ) + continue; + + if (channels[channel].nb_inputs > 0) { + if ( + !beat.experiments.utils.compareSignatures( + algorithm_inputs_signature[group], + block_inputs_signature[channel], + dataformats_list + ) + ) { + continue; + } + } + + if (channels[channel].nb_outputs > 0) { + if ( + !beat.experiments.utils.compareSignatures( + block_outputs_signature[channel], + algorithm_outputs_signature[group], + dataformats_list + ) + ) { + continue; + } + } + + matches.push(group); + } + + if (matches.length == 0) return null; + + mapping[channel] = matches; + } + + var smart_mapping = new beat.experiments.utils.SmartMapping( + mapping, + block_inputs_signature, + block_outputs_signature, + algorithm_inputs_signature, + algorithm_outputs_signature + ); + + if (smart_mapping.isValid()) return smart_mapping; + + return null; +}; //----------------------------------------------- +beat.experiments.utils.compareSignatures = function( + fixed, + extendable, + dataformats_list +) { + // Clone the signatures to have a modifiable version + fixed = JSON.parse(JSON.stringify(fixed)); + extendable = JSON.parse(JSON.stringify(extendable)); -beat.experiments.utils.compareSignatures = function(fixed, extendable, dataformats_list) -{ - // Clone the signatures to have a modifiable version - fixed = JSON.parse(JSON.stringify(fixed)); - extendable = JSON.parse(JSON.stringify(extendable)); - - // Compare the two signatures - while ((Object.keys(fixed).length != 0) && (Object.keys(extendable).length != 0)) - { - var fixed_names = Object.keys(fixed); - var extendable_names = Object.keys(extendable); - - if ((fixed_names.length == 1) && (fixed_names[0] == 'null')) - break; - - if ((extendable_names.length == 1) && (extendable_names[0] == 'null')) - break; + // Compare the two signatures + while ( + Object.keys(fixed).length != 0 && + Object.keys(extendable).length != 0 + ) { + var fixed_names = Object.keys(fixed); + var extendable_names = Object.keys(extendable); - for (var k = 0; k < extendable_names.length; ++k) - { - var dataformat_name = extendable_names[k]; + if (fixed_names.length == 1 && fixed_names[0] == 'null') break; - if (dataformat_name == 'null') - continue; + if (extendable_names.length == 1 && extendable_names[0] == 'null') break; - if (fixed_names.indexOf(dataformat_name) >= 0) - { - var nb = Math.min(fixed[dataformat_name], extendable[dataformat_name]); + for (var k = 0; k < extendable_names.length; ++k) { + var dataformat_name = extendable_names[k]; - fixed[dataformat_name] -= nb; - extendable[dataformat_name] -= nb; + if (dataformat_name == 'null') continue; - if (fixed[dataformat_name] == 0) - fixed[dataformat_name] = undefined; + if (fixed_names.indexOf(dataformat_name) >= 0) { + var nb = Math.min(fixed[dataformat_name], extendable[dataformat_name]); - if (extendable[dataformat_name] == 0) - extendable[dataformat_name] = undefined; - } + fixed[dataformat_name] -= nb; + extendable[dataformat_name] -= nb; - if (extendable[dataformat_name] !== undefined) - { - var details = dataformats_list.get(dataformat_name); - if ((details !== null) && (details.extend !== null)) - { - if (extendable[details.extend] !== undefined) - extendable[details.extend] += extendable[dataformat_name]; - else - extendable[details.extend] = extendable[dataformat_name]; + if (fixed[dataformat_name] == 0) fixed[dataformat_name] = undefined; - extendable[dataformat_name] = undefined; - } - else - { - return false; - } - } + if (extendable[dataformat_name] == 0) + extendable[dataformat_name] = undefined; + } - fixed = JSON.parse(JSON.stringify(fixed)); - extendable = JSON.parse(JSON.stringify(extendable)); - } - } + if (extendable[dataformat_name] !== undefined) { + var details = dataformats_list.get(dataformat_name); + if (details !== null && details.extend !== null) { + if (extendable[details.extend] !== undefined) + extendable[details.extend] += extendable[dataformat_name]; + else extendable[details.extend] = extendable[dataformat_name]; - var fixed_names = Object.keys(fixed); - var extendable_names = Object.keys(extendable); + extendable[dataformat_name] = undefined; + } else { + return false; + } + } - var fixed_nulls = 0; - var fixed_not_nulls = 0; + fixed = JSON.parse(JSON.stringify(fixed)); + extendable = JSON.parse(JSON.stringify(extendable)); + } + } - for (var i = 0; i < fixed_names.length; ++i) - { - if (fixed_names[i] == 'null') - fixed_nulls += fixed[fixed_names[i]]; - else - fixed_not_nulls += fixed[fixed_names[i]]; - } + var fixed_names = Object.keys(fixed); + var extendable_names = Object.keys(extendable); - var extended_nulls = 0; - var extended_not_nulls = 0; + var fixed_nulls = 0; + var fixed_not_nulls = 0; - for (var i = 0; i < extendable_names.length; ++i) - { - if (extendable_names[i] == 'null') - extended_nulls += extendable[extendable_names[i]]; - else - extended_not_nulls += extendable[extendable_names[i]]; - } + for (var i = 0; i < fixed_names.length; ++i) { + if (fixed_names[i] == 'null') fixed_nulls += fixed[fixed_names[i]]; + else fixed_not_nulls += fixed[fixed_names[i]]; + } - return (fixed_nulls == extended_not_nulls) && (fixed_not_nulls == extended_nulls); -} + var extended_nulls = 0; + var extended_not_nulls = 0; + for (var i = 0; i < extendable_names.length; ++i) { + if (extendable_names[i] == 'null') + extended_nulls += extendable[extendable_names[i]]; + else extended_not_nulls += extendable[extendable_names[i]]; + } + return fixed_nulls == extended_not_nulls && fixed_not_nulls == extended_nulls; +}; /******************************** CLASS: SmartDatasetList *******************************/ //---------------------------------------------------------------------------------------- // Constructor //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList = function() -{ - // Attributes - this.databases = {}; -} - +beat.experiments.utils.SmartDatasetList = function() { + // Attributes + this.databases = {}; +}; //---------------------------------------------------------------------------------------- // Add an entry into the list //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.add = function(database, protocol, block, - dataset, compatibility_infos) -{ - if (this.databases[database] === undefined) - { - this.databases[database] = { - usable: null, - protocols: {}, - }; - } - - if (this.databases[database].protocols[protocol] === undefined) - { - this.databases[database].protocols[protocol] = { - usable: null, - blocks: {}, - }; - } - - if (this.databases[database].protocols[protocol].blocks[block] === undefined) - this.databases[database].protocols[protocol].blocks[block] = []; - - compatibility_infos.set = dataset; - this.databases[database].protocols[protocol].blocks[block].push(compatibility_infos); -} - +beat.experiments.utils.SmartDatasetList.prototype.add = function( + database, + protocol, + block, + dataset, + compatibility_infos +) { + if (this.databases[database] === undefined) { + this.databases[database] = { + usable: null, + protocols: {}, + }; + } + + if (this.databases[database].protocols[protocol] === undefined) { + this.databases[database].protocols[protocol] = { + usable: null, + blocks: {}, + }; + } + + if (this.databases[database].protocols[protocol].blocks[block] === undefined) + this.databases[database].protocols[protocol].blocks[block] = []; + + compatibility_infos.set = dataset; + this.databases[database].protocols[protocol].blocks[block].push( + compatibility_infos + ); +}; //---------------------------------------------------------------------------------------- // Attempt to find the best configuration for each dataset block for each // database/protocol //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.finish = function(nb_dataset_blocks) -{ - var database_names = Object.keys(this.databases); - for (var i = 0; i < database_names.length; ++i) - { - var database = this.databases[database_names[i]]; - - var protocol_names = Object.keys(database.protocols); - for (var j = 0; j < protocol_names.length; ++j) - { - var protocol = database.protocols[protocol_names[j]]; - - // Remove protocols not covering all the dataset blocks - var block_names = Object.keys(protocol.blocks); - if (block_names.length != nb_dataset_blocks) - { - delete database.protocols[protocol_names[j]]; - continue; - } - - // Sort the compatible datasets for each block - for (var k = 0; k < block_names.length; ++k) - { - protocol.blocks[block_names[k]].sort(function(a, b) { - if (a.perfect) - { - if (!b.perfect) - return -1.0; - else - return (a.distance - b.distance); - } - else - { - if (b.perfect) - return +1.0; - else - return (a.distance - b.distance); - } - }); - } - - // First process all the dataset blocks for which only one dataset is - // compatible - block_names = this._processSingleMatches(block_names, protocol); - if (block_names === null) - { - delete database.protocols[protocol_names[j]]; - continue; - } - - // Next process all the dataset blocks for which one perfect dataset was - // found - if (block_names.length > 0) - { - block_names = this._processPerfectMatches(block_names, protocol); - if (block_names === null) - { - delete database.protocols[protocol_names[j]]; - continue; - } - } - - // Next process the best remaining match for each dataset block - if (block_names.length > 0) - { - block_names = this._processBestMatches(block_names, protocol); - if (block_names === null) - { - delete database.protocols[protocol_names[j]]; - continue; - } - } - } - - if (Object.keys(database.protocols) == 0) - delete this.databases[database_names[i]]; - } - - return (Object.keys(this.databases).length > 0); -} - +beat.experiments.utils.SmartDatasetList.prototype.finish = function( + nb_dataset_blocks +) { + var database_names = Object.keys(this.databases); + for (var i = 0; i < database_names.length; ++i) { + var database = this.databases[database_names[i]]; + + var protocol_names = Object.keys(database.protocols); + for (var j = 0; j < protocol_names.length; ++j) { + var protocol = database.protocols[protocol_names[j]]; + + // Remove protocols not covering all the dataset blocks + var block_names = Object.keys(protocol.blocks); + if (block_names.length != nb_dataset_blocks) { + delete database.protocols[protocol_names[j]]; + continue; + } + + // Sort the compatible datasets for each block + for (var k = 0; k < block_names.length; ++k) { + protocol.blocks[block_names[k]].sort(function(a, b) { + if (a.perfect) { + if (!b.perfect) return -1.0; + else return a.distance - b.distance; + } else { + if (b.perfect) return +1.0; + else return a.distance - b.distance; + } + }); + } + + // First process all the dataset blocks for which only one dataset is + // compatible + block_names = this._processSingleMatches(block_names, protocol); + if (block_names === null) { + delete database.protocols[protocol_names[j]]; + continue; + } + + // Next process all the dataset blocks for which one perfect dataset was + // found + if (block_names.length > 0) { + block_names = this._processPerfectMatches(block_names, protocol); + if (block_names === null) { + delete database.protocols[protocol_names[j]]; + continue; + } + } + + // Next process the best remaining match for each dataset block + if (block_names.length > 0) { + block_names = this._processBestMatches(block_names, protocol); + if (block_names === null) { + delete database.protocols[protocol_names[j]]; + continue; + } + } + } + + if (Object.keys(database.protocols) == 0) + delete this.databases[database_names[i]]; + } + + return Object.keys(this.databases).length > 0; +}; //---------------------------------------------------------------------------------------- // Returns the name of all the databases in the list //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.databaseNames = function() -{ - return Object.keys(this.databases); -} - +beat.experiments.utils.SmartDatasetList.prototype.databaseNames = function() { + return Object.keys(this.databases); +}; //---------------------------------------------------------------------------------------- // Returns the name of all the protocols of a specific database //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.protocolNames = function(database) -{ - if (this.databases[database] === undefined) - return []; - - return Object.keys(this.databases[database].protocols); -} +beat.experiments.utils.SmartDatasetList.prototype.protocolNames = function( + database +) { + if (this.databases[database] === undefined) return []; + return Object.keys(this.databases[database].protocols); +}; //---------------------------------------------------------------------------------------- // Returns the name of all the protocols of a specific database //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.protocol = function(database, name) -{ - if (this.databases[database] === undefined) - return null; - - if (this.databases[database].protocols[name] === undefined) - return null; +beat.experiments.utils.SmartDatasetList.prototype.protocol = function( + database, + name +) { + if (this.databases[database] === undefined) return null; - return this.databases[database].protocols[name]; -} + if (this.databases[database].protocols[name] === undefined) return null; + return this.databases[database].protocols[name]; +}; //---------------------------------------------------------------------------------------- // Update the usability status of one block/dataset combination //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.update = function(database_name, protocol_name, - block_name, dataset, usable) -{ - var database = this.databases[database_name]; - if (database === undefined) - return; - - var protocol = database.protocols[protocol_name]; - if (protocol === undefined) - return; - - var block = protocol.blocks[block_name]; - if (block === undefined) - return; - - if (block.set != dataset) - return; - - if (usable != block.usable) - { - block.usable = usable; - - if (usable) - { - protocol.usable = null; - database.usable = null; - } - else - { - protocol.usable = false; - database.usable = null; - } - } -} - +beat.experiments.utils.SmartDatasetList.prototype.update = function( + database_name, + protocol_name, + block_name, + dataset, + usable +) { + var database = this.databases[database_name]; + if (database === undefined) return; + + var protocol = database.protocols[protocol_name]; + if (protocol === undefined) return; + + var block = protocol.blocks[block_name]; + if (block === undefined) return; + + if (block.set != dataset) return; + + if (usable != block.usable) { + block.usable = usable; + + if (usable) { + protocol.usable = null; + database.usable = null; + } else { + protocol.usable = false; + database.usable = null; + } + } +}; //---------------------------------------------------------------------------------------- // Indicates if a protocol is usable (given the current configuration) //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.isProtocolUsable = function(database_name, - protocol_name) -{ - var database = this.databases[database_name]; - if (database === undefined) - return false; - - var protocol = database.protocols[protocol_name]; - if (protocol === undefined) - return false; - - if (protocol.usable === null) - { - protocol.usable = true; - - var block_names = Object.keys(protocol.blocks); - for (var i = 0; i < block_names.length; ++i) - { - if (!protocol.blocks[ block_names[i] ].usable) - { - protocol.usable = false; - break; - } - } - } - - return protocol.usable; -} - +beat.experiments.utils.SmartDatasetList.prototype.isProtocolUsable = function( + database_name, + protocol_name +) { + var database = this.databases[database_name]; + if (database === undefined) return false; + + var protocol = database.protocols[protocol_name]; + if (protocol === undefined) return false; + + if (protocol.usable === null) { + protocol.usable = true; + + var block_names = Object.keys(protocol.blocks); + for (var i = 0; i < block_names.length; ++i) { + if (!protocol.blocks[block_names[i]].usable) { + protocol.usable = false; + break; + } + } + } + + return protocol.usable; +}; //---------------------------------------------------------------------------------------- // Indicates if a database is usable (given the current configuration) //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartDatasetList.prototype.isDatabaseUsable = function(database_name) -{ - var database = this.databases[database_name]; - if (database === undefined) - return false; - - if (database.usable === null) - { - database.usable = false; - - var protocol_names = Object.keys(database.protocols); - for (var i = 0; i < protocol_names.length; ++i) - { - if (this.isProtocolUsable(database_name, protocol_names[i])) - { - database.usable = true; - break; - } - } - } - - return database.usable; -} - +beat.experiments.utils.SmartDatasetList.prototype.isDatabaseUsable = function( + database_name +) { + var database = this.databases[database_name]; + if (database === undefined) return false; + + if (database.usable === null) { + database.usable = false; + + var protocol_names = Object.keys(database.protocols); + for (var i = 0; i < protocol_names.length; ++i) { + if (this.isProtocolUsable(database_name, protocol_names[i])) { + database.usable = true; + break; + } + } + } + + return database.usable; +}; //--------------------------------------------------------- - beat.experiments.utils.SmartDatasetList.prototype._removeDataset = function( - block_names, protocol, set_to_remove) -{ - for (var i = 0; i < block_names.length; ++i) - { - var block_name = block_names[i]; - - for (var j = 0; j < protocol.blocks[block_name].length; ++j) - { - if (protocol.blocks[block_name][j].set == set_to_remove) - { - protocol.blocks[block_name].splice(j, 1); + block_names, + protocol, + set_to_remove +) { + for (var i = 0; i < block_names.length; ++i) { + var block_name = block_names[i]; - if (protocol.blocks[block_name].length == 0) - return false; + for (var j = 0; j < protocol.blocks[block_name].length; ++j) { + if (protocol.blocks[block_name][j].set == set_to_remove) { + protocol.blocks[block_name].splice(j, 1); - break; - } - } - } + if (protocol.blocks[block_name].length == 0) return false; - return true; -} + break; + } + } + } + return true; +}; //---------------------------------------------------------------------------------------- // Process all the dataset blocks for which only one dataset is compatible //---------------------------------------------------------------------------------------- beat.experiments.utils.SmartDatasetList.prototype._processSingleMatches = function( - block_names, protocol) -{ - var changed = true; - while (changed && (block_names.length > 0)) - { - changed = false; - for (var i = 0; i < block_names.length; ++i) - { - var block_name = block_names[i]; - - if (protocol.blocks[block_name].length == 1) - { - protocol.blocks[block_name] = protocol.blocks[block_name][0]; - block_names.splice(i, 1); - - if (!this._removeDataset(block_names, protocol, protocol.blocks[block_name].set)) - return null; - - changed = true; - break; - } - } - } - - return block_names; -} - + block_names, + protocol +) { + var changed = true; + while (changed && block_names.length > 0) { + changed = false; + for (var i = 0; i < block_names.length; ++i) { + var block_name = block_names[i]; + + if (protocol.blocks[block_name].length == 1) { + protocol.blocks[block_name] = protocol.blocks[block_name][0]; + block_names.splice(i, 1); + + if ( + !this._removeDataset( + block_names, + protocol, + protocol.blocks[block_name].set + ) + ) + return null; + + changed = true; + break; + } + } + } + + return block_names; +}; //---------------------------------------------------------------------------------------- // Process all the dataset blocks for which one perfect dataset was found //---------------------------------------------------------------------------------------- beat.experiments.utils.SmartDatasetList.prototype._processPerfectMatches = function( - block_names, protocol) -{ - var changed = true; - while (changed && (block_names.length > 0)) - { - changed = false; - for (var i = 0; i < block_names.length; ++i) - { - var block_name = block_names[i]; - - for (var j = 0; j < protocol.blocks[block_name].length; ++j) - { - if (protocol.blocks[block_name][j].perfect) - { - protocol.blocks[block_name] = protocol.blocks[block_name][j]; - block_names.splice(i, 1); - - if (!this._removeDataset(block_names, protocol, protocol.blocks[block_name].set)) - return null; - - changed = true; - break; - } - } - } - } - - if (block_names.length > 0) - block_names = this._processSingleMatches(block_names, protocol); - - return block_names; -} - + block_names, + protocol +) { + var changed = true; + while (changed && block_names.length > 0) { + changed = false; + for (var i = 0; i < block_names.length; ++i) { + var block_name = block_names[i]; + + for (var j = 0; j < protocol.blocks[block_name].length; ++j) { + if (protocol.blocks[block_name][j].perfect) { + protocol.blocks[block_name] = protocol.blocks[block_name][j]; + block_names.splice(i, 1); + + if ( + !this._removeDataset( + block_names, + protocol, + protocol.blocks[block_name].set + ) + ) + return null; + + changed = true; + break; + } + } + } + } + + if (block_names.length > 0) + block_names = this._processSingleMatches(block_names, protocol); + + return block_names; +}; //---------------------------------------------------------------------------------------- // Process the best match for each dataset block, when no ambiguity is possible //---------------------------------------------------------------------------------------- beat.experiments.utils.SmartDatasetList.prototype._processSingleBestMatches = function( - block_names, protocol) -{ - var changed = true; - while (changed && (block_names.length > 0)) - { - changed = false; - for (var i = 0; i < block_names.length; ++i) - { - var block_name = block_names[i]; - - var best_match = 0; - var nb_matches = 1; - - for (var j = 1; j < protocol.blocks[block_name].length; ++j) - { - if (protocol.blocks[block_name][j].distance == protocol.blocks[block_name][best_match].distance) - { - nb_matches += 1; - } - else if (protocol.blocks[block_name][j].distance < protocol.blocks[block_name][best_match].distance) - { - best_match = j; - nb_matches = 1; - } - } - - if (nb_matches == 1) - { - protocol.blocks[block_name] = protocol.blocks[block_name][best_match]; - block_names.splice(i, 1); - - if (!this._removeDataset(block_names, protocol, protocol.blocks[block_name].set)) - return null; - - changed = true; - break; - } - } - } - - if (block_names.length > 0) - block_names = this._processSingleMatches(block_names, protocol); - - return block_names; -} - + block_names, + protocol +) { + var changed = true; + while (changed && block_names.length > 0) { + changed = false; + for (var i = 0; i < block_names.length; ++i) { + var block_name = block_names[i]; + + var best_match = 0; + var nb_matches = 1; + + for (var j = 1; j < protocol.blocks[block_name].length; ++j) { + if ( + protocol.blocks[block_name][j].distance == + protocol.blocks[block_name][best_match].distance + ) { + nb_matches += 1; + } else if ( + protocol.blocks[block_name][j].distance < + protocol.blocks[block_name][best_match].distance + ) { + best_match = j; + nb_matches = 1; + } + } + + if (nb_matches == 1) { + protocol.blocks[block_name] = protocol.blocks[block_name][best_match]; + block_names.splice(i, 1); + + if ( + !this._removeDataset( + block_names, + protocol, + protocol.blocks[block_name].set + ) + ) + return null; + + changed = true; + break; + } + } + } + + if (block_names.length > 0) + block_names = this._processSingleMatches(block_names, protocol); + + return block_names; +}; //---------------------------------------------------------------------------------------- // Process the best matches for each dataset block //---------------------------------------------------------------------------------------- beat.experiments.utils.SmartDatasetList.prototype._processBestMatches = function( - block_names, protocol) -{ - block_names = this._processSingleBestMatches(block_names, protocol); - - while ((block_names !== null) && (block_names.length > 0)) - { - var best_block = null; - var best_distance = null; - var matches = null; - - for (var i = 0; i < block_names.length; ++i) - { - var block_name = block_names[i]; - - var block_best_distance = protocol.blocks[block_name][0].distance; - var block_matches = [ { - set: protocol.blocks[block_name][0].set, - index: 0, - }]; - - for (var j = 1; j < protocol.blocks[block_name].length; ++j) - { - if (protocol.blocks[block_name][j].distance == block_best_distance) - { - block_matches.push({ - set: protocol.blocks[block_name][j].set, - index: j, - }); - } - else if (protocol.blocks[block_name][j].distance < block_best_distance) - { - block_best_distance = protocol.blocks[block_name][j].distance; - block_matches.push({ - set: protocol.blocks[block_name][j].set, - index: j, - }); - } - } - - if ((best_block === null) || (block_best_distance < best_distance) || - ((block_best_distance == best_distance) && (block_matches.length < matches.length))) - { - best_block = block_name; - best_distance = block_best_distance; - matches = block_matches; - } - } - - // Select the dataset by fuzzy matching - var choice = null; - var score = -1.0; - for (var i = 0; i < matches.length; ++i) - { - var match_score = best_block.score(matches[i].set, 0.5); - if (match_score > score) - { - choice = matches[i]; - score = match_score; - } - } - - protocol.blocks[best_block] = protocol.blocks[best_block][choice.index]; - block_names.splice(block_names.indexOf(best_block), 1); - - if (!this._removeDataset(block_names, protocol, protocol.blocks[best_block].set)) - return null; - - if (block_names.length > 0) - block_names = this._processSingleMatches(block_names, protocol); - } - - return block_names; -} - - + block_names, + protocol +) { + block_names = this._processSingleBestMatches(block_names, protocol); + + while (block_names !== null && block_names.length > 0) { + var best_block = null; + var best_distance = null; + var matches = null; + + for (var i = 0; i < block_names.length; ++i) { + var block_name = block_names[i]; + + var block_best_distance = protocol.blocks[block_name][0].distance; + var block_matches = [ + { + set: protocol.blocks[block_name][0].set, + index: 0, + }, + ]; + + for (var j = 1; j < protocol.blocks[block_name].length; ++j) { + if (protocol.blocks[block_name][j].distance == block_best_distance) { + block_matches.push({ + set: protocol.blocks[block_name][j].set, + index: j, + }); + } else if ( + protocol.blocks[block_name][j].distance < block_best_distance + ) { + block_best_distance = protocol.blocks[block_name][j].distance; + block_matches.push({ + set: protocol.blocks[block_name][j].set, + index: j, + }); + } + } + + if ( + best_block === null || + block_best_distance < best_distance || + (block_best_distance == best_distance && + block_matches.length < matches.length) + ) { + best_block = block_name; + best_distance = block_best_distance; + matches = block_matches; + } + } + + // Select the dataset by fuzzy matching + var choice = null; + var score = -1.0; + for (var i = 0; i < matches.length; ++i) { + var match_score = best_block.score(matches[i].set, 0.5); + if (match_score > score) { + choice = matches[i]; + score = match_score; + } + } + + protocol.blocks[best_block] = protocol.blocks[best_block][choice.index]; + block_names.splice(block_names.indexOf(best_block), 1); + + if ( + !this._removeDataset( + block_names, + protocol, + protocol.blocks[best_block].set + ) + ) + return null; + + if (block_names.length > 0) + block_names = this._processSingleMatches(block_names, protocol); + } + + return block_names; +}; /********************************** CLASS: SmartMapping *********************************/ //---------------------------------------------------------------------------------------- // Constructor //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartMapping = function(possibilities, block_inputs_signature, - block_outputs_signature, - algorithm_inputs_signature, - algorithm_outputs_signature) -{ - // Attributes - this.possibilities = JSON.parse(JSON.stringify(possibilities)); - this.block_inputs_signature = block_inputs_signature; - this.block_outputs_signature = block_outputs_signature; - this.algorithm_inputs_signature = algorithm_inputs_signature; - this.algorithm_outputs_signature = algorithm_outputs_signature; - this.iteration_entries = []; - - // First process all the possibilities for which only one choice is possible - if (!this._automaticMapping()) - this.possibilities = null; -} - +beat.experiments.utils.SmartMapping = function( + possibilities, + block_inputs_signature, + block_outputs_signature, + algorithm_inputs_signature, + algorithm_outputs_signature +) { + // Attributes + this.possibilities = JSON.parse(JSON.stringify(possibilities)); + this.block_inputs_signature = block_inputs_signature; + this.block_outputs_signature = block_outputs_signature; + this.algorithm_inputs_signature = algorithm_inputs_signature; + this.algorithm_outputs_signature = algorithm_outputs_signature; + this.iteration_entries = []; + + // First process all the possibilities for which only one choice is possible + if (!this._automaticMapping()) this.possibilities = null; +}; //---------------------------------------------------------------------------------------- // Indicates if a valid mapping was found //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartMapping.prototype.isValid = function() -{ - return (this.possibilities !== null); -} - +beat.experiments.utils.SmartMapping.prototype.isValid = function() { + return this.possibilities !== null; +}; //---------------------------------------------------------------------------------------- // Starts an iteration over the best channel/group combinations //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartMapping.prototype.startIteration = function() -{ - this.iteration_entries = []; - - var channels_to_process = Object.keys(this.possibilities); - var processed_indexes = []; - var biggest = 0; - - // First: add the single possibility combinations - for (var i = 0; i < channels_to_process.length; ++i) - { - var channel = channels_to_process[i]; - if (this.possibilities[channel].length == 1) - { - this.iteration_entries.push({ - channel: channel, - group: this.possibilities[channel][0].group, - }); - processed_indexes.push(i); - } - else if (this.possibilities[channel].length > biggest) - { - biggest = this.possibilities[channel].length; - } - } - - processed_indexes.reverse(); - for (var i = 0; i < processed_indexes.length; ++i) - channels_to_process.splice(processed_indexes[i], 1); - - // Next: add the best match of all the other combinations, beginning with the ones - // with the most possibilities - var unavailable_groups = []; - while (channels_to_process.length > 0) - { - processed_indexes = []; - var biggest2 = 0; - - for (var i = 0; i < channels_to_process.length; ++i) - { - var channel = channels_to_process[i]; - - if (this.possibilities[channel].length == biggest) - { - var entries = this.possibilities[channel].filter(function(entry) { - return (unavailable_groups.indexOf(entry.group) == -1); - }); - - this.iteration_entries.push({ - channel: channel, - group: entries[0].group, - }); - - processed_indexes.push(i); - unavailable_groups.push(entries[0].group); - } - else if (this.possibilities[channel].length > biggest2) - { - biggest2 = this.possibilities[channel].length; - } - } - - processed_indexes.reverse(); - for (var i = 0; i < processed_indexes.length; ++i) - channels_to_process.splice(processed_indexes[i], 1); - - biggest = biggest2; - } - - this.iteration_entries.reverse(); -} - +beat.experiments.utils.SmartMapping.prototype.startIteration = function() { + this.iteration_entries = []; + + var channels_to_process = Object.keys(this.possibilities); + var processed_indexes = []; + var biggest = 0; + + // First: add the single possibility combinations + for (var i = 0; i < channels_to_process.length; ++i) { + var channel = channels_to_process[i]; + if (this.possibilities[channel].length == 1) { + this.iteration_entries.push({ + channel: channel, + group: this.possibilities[channel][0].group, + }); + processed_indexes.push(i); + } else if (this.possibilities[channel].length > biggest) { + biggest = this.possibilities[channel].length; + } + } + + processed_indexes.reverse(); + for (var i = 0; i < processed_indexes.length; ++i) + channels_to_process.splice(processed_indexes[i], 1); + + // Next: add the best match of all the other combinations, beginning with the ones + // with the most possibilities + var unavailable_groups = []; + while (channels_to_process.length > 0) { + processed_indexes = []; + var biggest2 = 0; + + for (var i = 0; i < channels_to_process.length; ++i) { + var channel = channels_to_process[i]; + + if (this.possibilities[channel].length == biggest) { + var entries = this.possibilities[channel].filter(function(entry) { + return unavailable_groups.indexOf(entry.group) == -1; + }); + + this.iteration_entries.push({ + channel: channel, + group: entries[0].group, + }); + + processed_indexes.push(i); + unavailable_groups.push(entries[0].group); + } else if (this.possibilities[channel].length > biggest2) { + biggest2 = this.possibilities[channel].length; + } + } + + processed_indexes.reverse(); + for (var i = 0; i < processed_indexes.length; ++i) + channels_to_process.splice(processed_indexes[i], 1); + + biggest = biggest2; + } + + this.iteration_entries.reverse(); +}; //---------------------------------------------------------------------------------------- // Indicates if the iteration over the best channel/group combinations is done //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartMapping.prototype.isIterationDone = function() -{ - return (this.iteration_entries.length == 0); -} - +beat.experiments.utils.SmartMapping.prototype.isIterationDone = function() { + return this.iteration_entries.length == 0; +}; //---------------------------------------------------------------------------------------- // Returns the next channel/group combination in the iteration //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartMapping.prototype.next = function() -{ - return this.iteration_entries.pop(); -} - +beat.experiments.utils.SmartMapping.prototype.next = function() { + return this.iteration_entries.pop(); +}; //--------------------------------------------------------- - beat.experiments.utils.SmartMapping.prototype._removePossibility = function( - channel_names, group_to_remove) -{ - for (var i = 0; i < channel_names.length; ++i) - { - var channel = channel_names[i]; - - for (var j = 0; j < this.possibilities[channel].length; ++j) - { - if (this.possibilities[channel][j] == group_to_remove) - { - this.possibilities[channel].splice(j, 1); + channel_names, + group_to_remove +) { + for (var i = 0; i < channel_names.length; ++i) { + var channel = channel_names[i]; - if (this.possibilities[channel].length == 0) - return false; + for (var j = 0; j < this.possibilities[channel].length; ++j) { + if (this.possibilities[channel][j] == group_to_remove) { + this.possibilities[channel].splice(j, 1); - break; - } - } - } + if (this.possibilities[channel].length == 0) return false; - return true; -} + break; + } + } + } + return true; +}; //---------------------------------------------------------------------------------------- // Automatically assign the channels to the groups //---------------------------------------------------------------------------------------- -beat.experiments.utils.SmartMapping.prototype._automaticMapping = function() -{ - var channel_names = Object.keys(this.possibilities); - - // Process all the channels for which there is only one unique group - var changed = true; - while (changed && (channel_names.length > 0)) - { - changed = false; - for (var i = 0; i < channel_names.length; ++i) - { - var channel = channel_names[i]; - - if (this.possibilities[channel].length == 1) - { - this.possibilities[channel] = [{ - group: this.possibilities[channel][0], - score: 1.0, - }]; - - channel_names.splice(i, 1); - - if (!this._removePossibility(channel_names, this.possibilities[channel][0].group)) - return false; - - changed = true; - break; - } - } - } - - - // Process all the groups for which there is only one unique channel - var reverse_possibilities = {}; - for (var i = 0; i < channel_names.length; ++i) - { - var channel = channel_names[i]; - - for (var j = 0; j < this.possibilities[channel].length; ++j) - { - var group = this.possibilities[channel][j]; - - if (reverse_possibilities[group] === undefined) - reverse_possibilities[group] = []; - - reverse_possibilities[group].push(channel); - } - } - - var group_names = Object.keys(reverse_possibilities); - for (var i = 0; i < group_names.length; ++i) - { - var group = group_names[i]; - - if (reverse_possibilities[group].length == 1) - { - this.possibilities[reverse_possibilities[group][0]] = [{ - group: group, - score: 1.0, - }]; - - channel_names.splice(channel_names.indexOf(reverse_possibilities[group][0]), 1); - - if (!this._removePossibility(channel_names, group)) - return false; - } - } - - - // Process all the remaining channels - for (var i = 0; i < channel_names.length; ++i) - { - var channel = channel_names[i]; - - var result = []; - for (var j = 0; j < this.possibilities[channel].length; ++j) - { - var entry = { - group: this.possibilities[channel][j], - score: channel.score(this.possibilities[channel][j], 0.5), - }; - - result.push(entry); - } - - result.sort(function(a, b) { return a.score < b.score; }); - this.possibilities[channel] = result; - } - - return true; -} - +beat.experiments.utils.SmartMapping.prototype._automaticMapping = function() { + var channel_names = Object.keys(this.possibilities); + + // Process all the channels for which there is only one unique group + var changed = true; + while (changed && channel_names.length > 0) { + changed = false; + for (var i = 0; i < channel_names.length; ++i) { + var channel = channel_names[i]; + + if (this.possibilities[channel].length == 1) { + this.possibilities[channel] = [ + { + group: this.possibilities[channel][0], + score: 1.0, + }, + ]; + + channel_names.splice(i, 1); + + if ( + !this._removePossibility( + channel_names, + this.possibilities[channel][0].group + ) + ) + return false; + + changed = true; + break; + } + } + } + + // Process all the groups for which there is only one unique channel + var reverse_possibilities = {}; + for (var i = 0; i < channel_names.length; ++i) { + var channel = channel_names[i]; + + for (var j = 0; j < this.possibilities[channel].length; ++j) { + var group = this.possibilities[channel][j]; + + if (reverse_possibilities[group] === undefined) + reverse_possibilities[group] = []; + + reverse_possibilities[group].push(channel); + } + } + + var group_names = Object.keys(reverse_possibilities); + for (var i = 0; i < group_names.length; ++i) { + var group = group_names[i]; + + if (reverse_possibilities[group].length == 1) { + this.possibilities[reverse_possibilities[group][0]] = [ + { + group: group, + score: 1.0, + }, + ]; + + channel_names.splice( + channel_names.indexOf(reverse_possibilities[group][0]), + 1 + ); + + if (!this._removePossibility(channel_names, group)) return false; + } + } + + // Process all the remaining channels + for (var i = 0; i < channel_names.length; ++i) { + var channel = channel_names[i]; + + var result = []; + for (var j = 0; j < this.possibilities[channel].length; ++j) { + var entry = { + group: this.possibilities[channel][j], + score: channel.score(this.possibilities[channel][j], 0.5), + }; + + result.push(entry); + } + + result.sort(function(a, b) { + return a.score < b.score; + }); + this.possibilities[channel] = result; + } + + return true; +}; /** * Uses bootstrap3 to display a modal window to attest an experiment @@ -1680,60 +1650,56 @@ beat.experiments.utils.SmartMapping.prototype._automaticMapping = function() * redirect (str): The URL where to redirect the client after the operation * finished (e.g. "/experiments/user/user/toolchain/1/name/") **/ -beat.experiments.utils.modal_attest = function (name, url, redirect) { - - var dialog = new BootstrapDialog({ - title: '<i class="fa fa-certificate"></i> Attestation for "' + name + '"', - message: '<p>When you attest an experiment, the platform guarantees it is reproducible, therefore all components related to this experiment (including the toolchain, algorithms, libraries and dataformats) will be frozen. This procedure is <strong>not</strong> irreversible. You can always delete locked (i.e. unpublished) attestations. This procedure also does not stop you from forking or creating new revisions of objects used in this experiment.</p>', - type: BootstrapDialog.TYPE_PRIMARY, - size: BootstrapDialog.SIZE_WIDE, - buttons: [ - { - label: 'Cancel', - cssClass: 'btn-default', - action: function(the_dialog) { - the_dialog.close(); - } - }, - { - label: 'Attest', - cssClass: 'btn-primary', - action: function(the_dialog) { - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - var d = $.ajax({ - type: "POST", - url: url, - data: JSON.stringify({experiment: name}), - contentType: "application/json; charset=utf-8", - dataType: "json", - }); - - d.done(function(data) { - the_dialog.close(); - window.location.href = redirect; - }); - - d.fail(function(data, text_status) { - the_dialog.close(); - process_error(data, text_status); - }); - - } - }, - ], - }); - dialog.realize(); - dialog.open(); - -} - +beat.experiments.utils.modal_attest = function(name, url, redirect) { + var dialog = new BootstrapDialog({ + title: '<i class="fa fa-certificate"></i> Attestation for "' + name + '"', + message: + '<p>When you attest an experiment, the platform guarantees it is reproducible, therefore all components related to this experiment (including the toolchain, algorithms, libraries and dataformats) will be frozen. This procedure is <strong>not</strong> irreversible. You can always delete locked (i.e. unpublished) attestations. This procedure also does not stop you from forking or creating new revisions of objects used in this experiment.</p>', + type: BootstrapDialog.TYPE_PRIMARY, + size: BootstrapDialog.SIZE_WIDE, + buttons: [ + { + label: 'Cancel', + cssClass: 'btn-default', + action: function(the_dialog) { + the_dialog.close(); + }, + }, + { + label: 'Attest', + cssClass: 'btn-primary', + action: function(the_dialog) { + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + var d = $.ajax({ + type: 'POST', + url: url, + data: JSON.stringify({ experiment: name }), + contentType: 'application/json; charset=utf-8', + dataType: 'json', + }); + + d.done(function(data) { + the_dialog.close(); + window.location.href = redirect; + }); + + d.fail(function(data, text_status) { + the_dialog.close(); + process_error(data, text_status); + }); + }, + }, + ], + }); + dialog.realize(); + dialog.open(); +}; /** * Uses bootstrap3 to display a modal window to cancel an experiment @@ -1749,56 +1715,52 @@ beat.experiments.utils.modal_attest = function (name, url, redirect) { * redirect (str): The URL where to redirect the client after the operation * finished (e.g. "/experiments/user/user/toolchain/1/name/") **/ -beat.experiments.utils.modal_cancel = function (name, url, redirect) { - - var dialog = new BootstrapDialog({ - title: '<i class="fa fa-power-off"></i> Stopping experiment "' + name + '"', - message: '<p>Choose "Cancel" to halt the stop operation. Choose "Stop" to continue and cancel the experiment. Stopping the experiment execution does not erase cached information, so you can continue later from the point where you have stopped.</p>', - type: BootstrapDialog.TYPE_PRIMARY, - buttons: [ - { - label: 'Cancel', - cssClass: 'btn-default', - action: function(the_dialog) { - the_dialog.close(); - } - }, - { - label: 'Stop', - cssClass: 'btn-primary', - action: function(the_dialog) { - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - var d = $.ajax({ - type: "POST", - url: url, - }); - - d.done(function(data) { - the_dialog.close(); - window.location.href = redirect; - }); - - d.fail(function(data, text_status) { - the_dialog.close(); - process_error(data, text_status); - }); - - } - }, - ], - }); - dialog.realize(); - dialog.open(); - -} - +beat.experiments.utils.modal_cancel = function(name, url, redirect) { + var dialog = new BootstrapDialog({ + title: '<i class="fa fa-power-off"></i> Stopping experiment "' + name + '"', + message: + '<p>Choose "Cancel" to halt the stop operation. Choose "Stop" to continue and cancel the experiment. Stopping the experiment execution does not erase cached information, so you can continue later from the point where you have stopped.</p>', + type: BootstrapDialog.TYPE_PRIMARY, + buttons: [ + { + label: 'Cancel', + cssClass: 'btn-default', + action: function(the_dialog) { + the_dialog.close(); + }, + }, + { + label: 'Stop', + cssClass: 'btn-primary', + action: function(the_dialog) { + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + var d = $.ajax({ + type: 'POST', + url: url, + }); + + d.done(function(data) { + the_dialog.close(); + window.location.href = redirect; + }); + + d.fail(function(data, text_status) { + the_dialog.close(); + process_error(data, text_status); + }); + }, + }, + ], + }); + dialog.realize(); + dialog.open(); +}; /** * Uses bootstrap3 to display a modal window to add an experiment to an @@ -1811,206 +1773,214 @@ beat.experiments.utils.modal_cancel = function (name, url, redirect) { * report_list_url (String): The URL where to retrieve reports from * **/ -beat.experiments.utils.modal_add_to_report = function (names, report_list_url) { - - if (!Array.isArray(names)) { - BootstrapDialog.alert('The input "names" must be an array'); - return false; - } - - if (names.length == 0) { - BootstrapDialog.alert('Select at least 1 experiment to add to a report'); - return false; - } - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - //retrieve list of existing reports - if that succeeds, construct modal form - function addToReport(names) { - return $.ajax({ - - type: 'GET', - url: report_list_url + '?fields=name,short_description,add_url', - - }).pipe(function(data) { - - var message = $(document.createElement('div')); - message.append($(document.createElement('p')).text('By clicking Add, the experiment(s) will be added to the selected report (if possible). You can cancel the operation by clicking Cancel.')); - var form_group = $(document.createElement('div')).addClass('form-group'); - message.append(form_group); - var select = $(document.createElement('select')).addClass('form-control'); - form_group.append(select); - var first_option = $(document.createElement('option')); - select.append(first_option); - first_option.val(''); - first_option.attr('disabled', true); - first_option.attr('selected', true); - first_option.text('Select a report...'); - - select.chosen({ - disable_search_threshold: 5, - search_contains: true, - allow_single_deselect: true, - }); - - select.on('chosen:showing_dropdown', function(e, params) { - select.find('option:gt(0)').remove(); - select.find('option:eq(0)').attr('selected', true); - data.forEach(function(i) { - var opt = $(document.createElement('option')); - select.append(opt); - opt.val(i.add_url); - var div = $(document.createElement('div')); - opt.append(div); - opt.data('name', i.name); - div.text(i.name); - if (i.short_description) { - var help = $(document.createElement('span')).addClass('help'); - help.text(' (' + i.short_description + ')'); - div.append(help); - } - }); - select.trigger('chosen:updated'); - select.trigger('chosen:open'); - }); - - //fix options when selected - select.on('change', function(e, params) { - var selected = $(this).find('option:selected'); - selected.text(selected.data('name')); - select.trigger('chosen:updated'); - }); - - BootstrapDialog.show({ - title: '<i class="fa fa-file-text-o fa-lg"></i> Select a report', - message: message, - type: BootstrapDialog.TYPE_PRIMARY, - buttons: [ - { - label: 'Cancel', - cssClass: 'btn-default', - action: function(the_dialog) { - the_dialog.close(); - return false; - }, - }, - { - label: 'Add', - cssClass: 'btn-primary', - action: function(the_dialog) { - if (!select.val()) { - BootstrapDialog.alert({ - title: '<i class="fa fa-warning"></i> Error', - message: 'You must select a report to add experiments to', - type: BootstrapDialog.TYPE_WARNING, - }); - return false; - } - the_dialog.close(); - - var post_info = {experiments:names}; - - var d = $.ajax({ - type: "POST", - data: JSON.stringify(post_info), - url: select.val(), - contentType: "application/json; charset=utf-8", - dataType: 'json', - }); - - d.done(function(data, status) { - - var message = $(document.createElement('div')); - message.addClass('report-results'); - - var sent = post_info.experiments.length; - var successful = sent; - - var description = $(document.createElement('h5')); - message.append(description); - - if (data !== undefined) { - // some experiments have failed - - // adds information about failed/incompatible experiments - function _add_list(message, objects, title) { - if (objects === undefined) return; - var length = objects.length; - if (length > 0) { - var _title = $(document.createElement('h5')); - _title.text(title); - message.append(_title); - var ul = $(document.createElement('ul')); - for (var i = 0; i < length; ++i) { - var li = $(document.createElement('li')); - li.text(objects[i]); - ul.append(li); - } - message.append(ul); - } - return length; - } - if (data.inaccessible_experiments !== undefined) - successful -= _add_list(message, data.inaccessible_experiments, - 'These experiments have failed (and cannot be added):'); - if (data.incompatible_experiments !== undefined) - successful -= _add_list(message, data.incompatible_experiments, - 'These experiments have different analyzers (and cannot be added):'); - } - - var size = BootstrapDialog.SIZE_NORMAL; - var type = BootstrapDialog.TYPE_PRIMARY; - var title = '<i class="fa fa-check"></i> Report changes'; - var btn_type = 'btn-primary'; - if (successful == sent) { - description.text('Successfully added ' + sent + ' experiment(s) to report'); - } - else { - description.text('Added ' + successful + ' (out of ' + sent + ' in total) experiment(s) to report'); - size = BootstrapDialog.SIZE_WIDE; - type = BootstrapDialog.TYPE_WARNING; - btn_type = 'btn-warning'; - title = '<i class="fa fa-warning"></i> Report changes'; - } - - BootstrapDialog.show({ - title: title, - message: message, - size: size, - type: type, - buttons: [{ - label: 'OK', - cssClass: btn_type, - action: function(dialog){dialog.close();}, - }] - }); - - return true; - }); - - d.fail(function(data, status) { - process_error(data, status); - }); - - }, - }, - ], - }); - }); - } - - addToReport(names).fail(function(data, text_status) { - process_error(data, text_status); - return false; - }); - -} - +beat.experiments.utils.modal_add_to_report = function(names, report_list_url) { + if (!Array.isArray(names)) { + BootstrapDialog.alert('The input "names" must be an array'); + return false; + } + + if (names.length == 0) { + BootstrapDialog.alert('Select at least 1 experiment to add to a report'); + return false; + } + + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + //retrieve list of existing reports - if that succeeds, construct modal form + function addToReport(names) { + return $.ajax({ + type: 'GET', + url: report_list_url + '?fields=name,short_description,add_url', + }).pipe(function(data) { + var message = $(document.createElement('div')); + message.append( + $(document.createElement('p')).text( + 'By clicking Add, the experiment(s) will be added to the selected report (if possible). You can cancel the operation by clicking Cancel.' + ) + ); + var form_group = $(document.createElement('div')).addClass('form-group'); + message.append(form_group); + var select = $(document.createElement('select')).addClass('form-control'); + form_group.append(select); + var first_option = $(document.createElement('option')); + select.append(first_option); + first_option.val(''); + first_option.attr('disabled', true); + first_option.attr('selected', true); + first_option.text('Select a report...'); + + data.forEach(function(i) { + const opt = $(document.createElement('option')); + select.append(opt); + opt.val(i.add_url); + const div = $(document.createElement('div')); + opt.append(div); + opt.data('name', i.name); + div.text(i.name); + if (i.short_description) { + const help = $(document.createElement('span')).addClass('help'); + help.text(' (' + i.short_description + ')'); + div.append(help); + } + }); + + BootstrapDialog.show({ + title: '<i class="fa fa-file-text-o fa-lg"></i> Select a report', + message: message, + type: BootstrapDialog.TYPE_PRIMARY, + buttons: [ + { + label: 'Cancel', + cssClass: 'btn-default', + action: function(the_dialog) { + the_dialog.close(); + return false; + }, + }, + { + label: 'Add', + cssClass: 'btn-primary', + action: function(the_dialog) { + if (!select.val()) { + BootstrapDialog.alert({ + title: '<i class="fa fa-warning"></i> Error', + message: 'You must select a report to add experiments to', + type: BootstrapDialog.TYPE_WARNING, + }); + return false; + } + the_dialog.close(); + + var post_info = { experiments: names }; + + var d = $.ajax({ + type: 'POST', + data: JSON.stringify(post_info), + url: select.val(), + contentType: 'application/json; charset=utf-8', + dataType: 'json', + }); + + d.done(function(data, status) { + var message = $(document.createElement('div')); + message.addClass('report-results'); + + var sent = post_info.experiments.length; + var successful = sent; + + var description = $(document.createElement('h5')); + message.append(description); + + if (data !== undefined) { + // some experiments have failed + + // adds information about failed/incompatible experiments + function _add_list(message, objects, title) { + if (objects === undefined) return; + var length = objects.length; + if (length > 0) { + var _title = $(document.createElement('h5')); + _title.text(title); + message.append(_title); + var ul = $(document.createElement('ul')); + for (var i = 0; i < length; ++i) { + var li = $(document.createElement('li')); + li.text(objects[i]); + ul.append(li); + } + message.append(ul); + } + return length; + } + if (data.inaccessible_experiments !== undefined) + successful -= _add_list( + message, + data.inaccessible_experiments, + 'These experiments have failed (and cannot be added):' + ); + if (data.incompatible_experiments !== undefined) + successful -= _add_list( + message, + data.incompatible_experiments, + 'These experiments have different analyzers (and cannot be added):' + ); + } + + var size = BootstrapDialog.SIZE_NORMAL; + var type = BootstrapDialog.TYPE_PRIMARY; + var title = '<i class="fa fa-check"></i> Report changes'; + var btn_type = 'btn-primary'; + let viewReportUrl = `${$.ajaxSettings.url.split( + '/experiments' + )[0]}${select + .val() + .replace('add/', '') + .replace(/api\/v.\//, '')}`; + if (successful == sent) { + description.text( + `Successfully added ${sent} experiment(s) to report` + ); + } else { + description.text( + 'Added ' + + successful + + ' (out of ' + + sent + + ' in total) experiment(s) to report' + ); + size = BootstrapDialog.SIZE_WIDE; + type = BootstrapDialog.TYPE_WARNING; + btn_type = 'btn-warning'; + title = '<i class="fa fa-warning"></i> Report changes'; + } + + BootstrapDialog.show({ + title: title, + message: message, + size: size, + type: type, + buttons: [ + { + label: 'View Report', + cssClass: btn_type, + action: function(dialog) { + dialog.close(); + window.open(viewReportUrl, '_blank'); + }, + }, + { + label: 'OK', + cssClass: btn_type, + action: function(dialog) { + dialog.close(); + }, + }, + ], + }); + + return true; + }); + + d.fail(function(data, status) { + process_error(data, status); + }); + }, + }, + ], + }); + }); + } + + addToReport(names).fail(function(data, text_status) { + process_error(data, text_status); + return false; + }); +}; /** * Uses bootstrap3 to display a modal window to create a new experiment (used @@ -2022,112 +1992,114 @@ beat.experiments.utils.modal_add_to_report = function (names, report_list_url) { * from * **/ -beat.experiments.utils.modal_new_experiment = function (toolchain_list_url) { - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - //retrieve list of existing toolchains - if that succeeds, builds modal form - var d = $.ajax({ - type: 'GET', - url: toolchain_list_url + '?fields=name,short_description,new_experiment_url', - }); - - d.fail(function(data, text_status) { - process_error(data, text_status); - return false; - }); - - d.done(function(data) { - - var message = $(document.createElement('div')); - message.append($(document.createElement('p')).text('Choose a toolchain to create a new experiment.')); - var form_group = $(document.createElement('div')).addClass('form-group'); - message.append(form_group); - var select = $(document.createElement('select')).addClass('form-control'); - form_group.append(select); - var first_option = $(document.createElement('option')); - select.append(first_option); - first_option.val(''); - first_option.attr('disabled', true); - first_option.attr('selected', true); - first_option.text('Select a toolchain...'); - - select.chosen({ - disable_search_threshold: 5, - search_contains: true, - allow_single_deselect: true, - }); - - select.on('chosen:showing_dropdown', function(e, params) { - select.find('option:gt(0)').remove(); - select.find('option:eq(0)').attr('selected', true); - data.forEach(function(i) { - var opt = $(document.createElement('option')); - select.append(opt); - opt.val(i.new_experiment_url); - var div = $(document.createElement('div')); - opt.append(div); - opt.data('name', i.name); - div.text(i.name); - if (i.short_description) { - var help = $(document.createElement('span')).addClass('help'); - help.text(' (' + i.short_description + ')'); - div.append(help); - } - }); - select.trigger('chosen:updated'); - select.trigger('chosen:open'); - }); - - //fix options when selected - select.on('change', function(e, params) { - var selected = $(this).find('option:selected'); - selected.text(selected.data('name')); - select.trigger('chosen:updated'); - }); - - BootstrapDialog.show({ - title: '<i class="fa fa-cog fa-lg"> Select a toolchain', - message: message, - type: BootstrapDialog.TYPE_PRIMARY, - onshown: function(the_dialog) { - select.trigger('chosen:activate'); - }, - buttons: [ - { - label: 'Cancel', - cssClass: 'btn-default', - action: function(the_dialog) { - the_dialog.close(); - return false; - }, - }, - { - label: 'Create', - cssClass: 'btn-primary', - action: function(the_dialog) { - if (!select.val()) { - BootstrapDialog.alert({ - title: '<i class="fa fa-warning"></i> Error', - message: 'You must select a toolchain to continue', - type: BootstrapDialog.TYPE_WARNING, - }); - return false; - } - the_dialog.close(); - window.location.href = select.val(); - }, - }, - ], - }); - }); -} - +beat.experiments.utils.modal_new_experiment = function(toolchain_list_url) { + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + //retrieve list of existing toolchains - if that succeeds, builds modal form + var d = $.ajax({ + type: 'GET', + url: + toolchain_list_url + '?fields=name,short_description,new_experiment_url', + }); + + d.fail(function(data, text_status) { + process_error(data, text_status); + return false; + }); + + d.done(function(data) { + var message = $(document.createElement('div')); + message.append( + $(document.createElement('p')).text( + 'Choose a toolchain to create a new experiment.' + ) + ); + var form_group = $(document.createElement('div')).addClass('form-group'); + message.append(form_group); + var select = $(document.createElement('select')).addClass('form-control'); + form_group.append(select); + var first_option = $(document.createElement('option')); + select.append(first_option); + first_option.val(''); + first_option.attr('disabled', true); + first_option.attr('selected', true); + first_option.text('Select a toolchain...'); + + select.chosen({ + disable_search_threshold: 5, + search_contains: true, + allow_single_deselect: true, + }); + + select.on('chosen:showing_dropdown', function(e, params) { + select.find('option:gt(0)').remove(); + select.find('option:eq(0)').attr('selected', true); + data.forEach(function(i) { + var opt = $(document.createElement('option')); + select.append(opt); + opt.val(i.new_experiment_url); + var div = $(document.createElement('div')); + opt.append(div); + opt.data('name', i.name); + div.text(i.name); + if (i.short_description) { + var help = $(document.createElement('span')).addClass('help'); + help.text(' (' + i.short_description + ')'); + div.append(help); + } + }); + select.trigger('chosen:updated'); + select.trigger('chosen:open'); + }); + + //fix options when selected + select.on('change', function(e, params) { + var selected = $(this).find('option:selected'); + selected.text(selected.data('name')); + select.trigger('chosen:updated'); + }); + + BootstrapDialog.show({ + title: '<i class="fa fa-cog fa-lg"> Select a toolchain', + message: message, + type: BootstrapDialog.TYPE_PRIMARY, + onshown: function(the_dialog) { + select.trigger('chosen:activate'); + }, + buttons: [ + { + label: 'Cancel', + cssClass: 'btn-default', + action: function(the_dialog) { + the_dialog.close(); + return false; + }, + }, + { + label: 'Create', + cssClass: 'btn-primary', + action: function(the_dialog) { + if (!select.val()) { + BootstrapDialog.alert({ + title: '<i class="fa fa-warning"></i> Error', + message: 'You must select a toolchain to continue', + type: BootstrapDialog.TYPE_WARNING, + }); + return false; + } + the_dialog.close(); + window.location.href = select.val(); + }, + }, + ], + }); + }); +}; /** * Uses bootstrap3 to display a modal window to change an experiment name @@ -2141,85 +2113,101 @@ beat.experiments.utils.modal_new_experiment = function (toolchain_list_url) { * update_url (String): The URL endpoint for updating the experiment * **/ -beat.experiments.utils.modal_rename = function (userid, current_name, list_url, update_url) { - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - //retrieve list of existing experiments - if that succeeds, builds modal form - var d = $.ajax({ - type: 'GET', - url: list_url + '?fields=short_name,author', - }); - - d.fail(function(data, text_status) { - process_error(data, text_status); - return false; - }); - - d.done(function(data) { - - //filter returned names to only keep author experiments' names - data = data.filter(function(e) { - return (e.author == userid && e.short_name != current_name); - }).map(function(e){ return e.short_name; }); - - var message = $(document.createElement('div')); - //message copied from templates/experiments/setup.html - message.append($('<span class="help">Enter a meaningful name to help you recognize this experiment. Auto-completion will help you in keeping your naming conventions tide. If a chosen name is <span class="text-danger">highlighted in red</span>, it is because it is already being used. In this case, choose another name.</span>')); - message.append($('<div class="form-group has-feedback"><label class="control-label" for="settings_name">Name:</label><input class="form-control input-sm" id="modal-rename" type="text" class="label" data-placeholder="Start typing a name..." autocomplete="off" autocorrect="off" autocapitalize="off" value="' + current_name + '"></input><span class="glyphicon glyphicon-remove form-control-feedback" aria-hidden="true"></span>')); - - beat.experiments.dialogs.name_typeahead(message.find('input'), data); - - BootstrapDialog.show({ - title: '<i class="fa fa-tag fa-lg"></i> Rename experiment', - message: message, - type: BootstrapDialog.TYPE_PRIMARY, - buttons: [ - { - label: 'Cancel', - cssClass: 'btn-default', - action: function(the_dialog) { - the_dialog.close(); - return false; - }, - }, - { - label: 'Rename', - cssClass: 'btn-primary', - action: function(the_dialog) { - the_dialog.close(); - var new_name = message.find('input').val().trim(); - if (new_name === current_name) { - the_dialog.close(); - return false; - } - var d2 = $.ajax({ - type: "PUT", - url: update_url, - data: JSON.stringify({name: new_name}), - contentType: "application/json; charset=utf-8", - dataType: "json" - }); - d2.fail(function(data, text_status) { - process_error(data, text_status); - return false; - }); - d2.done(function(data) { - window.location.href = data.view_url; - return true; - }); - }, - }, - ], - }); - }); -} - +beat.experiments.utils.modal_rename = function( + userid, + current_name, + list_url, + update_url +) { + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + //retrieve list of existing experiments - if that succeeds, builds modal form + var d = $.ajax({ + type: 'GET', + url: list_url + '?fields=short_name,author', + }); + + d.fail(function(data, text_status) { + process_error(data, text_status); + return false; + }); + + d.done(function(data) { + //filter returned names to only keep author experiments' names + data = data + .filter(function(e) { + return e.author == userid && e.short_name != current_name; + }) + .map(function(e) { + return e.short_name; + }); + + var message = $(document.createElement('div')); + //message copied from templates/experiments/setup.html + message.append( + $( + '<span class="help">Enter a meaningful name to help you recognize this experiment. Auto-completion will help you in keeping your naming conventions tide. If a chosen name is <span class="text-danger">highlighted in red</span>, it is because it is already being used. In this case, choose another name.</span>' + ) + ); + message.append( + $( + '<div class="form-group has-feedback"><label class="control-label" for="settings_name">Name:</label><input class="form-control input-sm" id="modal-rename" type="text" class="label" data-placeholder="Start typing a name..." autocomplete="off" autocorrect="off" autocapitalize="off" value="' + + current_name + + '"></input><span class="glyphicon glyphicon-remove form-control-feedback" aria-hidden="true"></span>' + ) + ); + + beat.experiments.dialogs.name_typeahead(message.find('input'), data); + + BootstrapDialog.show({ + title: '<i class="fa fa-tag fa-lg"></i> Rename experiment', + message: message, + type: BootstrapDialog.TYPE_PRIMARY, + buttons: [ + { + label: 'Cancel', + cssClass: 'btn-default', + action: function(the_dialog) { + the_dialog.close(); + return false; + }, + }, + { + label: 'Rename', + cssClass: 'btn-primary', + action: function(the_dialog) { + the_dialog.close(); + var new_name = message.find('input').val().trim(); + if (new_name === current_name) { + the_dialog.close(); + return false; + } + var d2 = $.ajax({ + type: 'PUT', + url: update_url, + data: JSON.stringify({ name: new_name }), + contentType: 'application/json; charset=utf-8', + dataType: 'json', + }); + d2.fail(function(data, text_status) { + process_error(data, text_status); + return false; + }); + d2.done(function(data) { + window.location.href = data.view_url; + return true; + }); + }, + }, + ], + }); + }); +}; /** * Updates the toolchain viewer with status information from the blocks @@ -2235,19 +2223,23 @@ beat.experiments.utils.modal_rename = function (userid, current_name, list_url, * dt_block_status (String): The name of the data object inside each block * where the current block state is stored. */ -beat.experiments.utils.update_viewer = function (viewer, objects, dt_block_name, dt_block_status) { - //gather block information - block_status = {}; - objects.each(function(idx) { - var bk_name = $(this).data(dt_block_name); - if (!bk_name) return; - var bk_status = $(this).data(dt_block_status); - if (bk_status === 'cached') bk_status = 'generated'; //< viewer quirk - block_status[bk_name] = bk_status; - }); - viewer.updateBlocksStatus(block_status); -} - +beat.experiments.utils.update_viewer = function( + viewer, + objects, + dt_block_name, + dt_block_status +) { + //gather block information + block_status = {}; + objects.each(function(idx) { + var bk_name = $(this).data(dt_block_name); + if (!bk_name) return; + var bk_status = $(this).data(dt_block_status); + if (bk_status === 'cached') bk_status = 'generated'; //< viewer quirk + block_status[bk_name] = bk_status; + }); + viewer.updateBlocksStatus(block_status); +}; /** * Updates blocks of a running experiment on the experiment "view" page @@ -2274,59 +2266,75 @@ beat.experiments.utils.update_viewer = function (viewer, objects, dt_block_name, * interval (Int): A timeout in milliseconds to use for updating the block * representations and the toolchain viewer. */ -beat.experiments.utils.update_blocks = function (url, st, dt, unveil, objects, dt_block_name, viewer, interval) { - - var _status = $(st).data(dt); - - if (_status === 'Failed') { - beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name, dt); - return; - } - - //only updates if in one of the "interesting" states - var interesting_states = ['Scheduled', 'Running', 'Canceling']; - if (interesting_states.indexOf(_status) <= -1) return; - - function _do_update() { - var _status = $(st).data(dt); - - if (interesting_states.indexOf(_status) <= -1) { - //experiment changed status - should reload - $(unveil).show(); - if (viewer.running) viewer.onExperimentDone(); - if (_status === 'Failed') { - beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name, - dt); - } - return; - } - - var d = $.get(url); - - d.done(function(data) { - var parsed = $($.parseHTML(data)); - $(objects).each(function(idx) { - var _self = $(this); - var r = parsed.find('#' + _self.attr('id')); - //only replaces if it changed - var old_status = _self.data(dt); - var new_status = r.data(dt); - if (r && old_status !== new_status) _self.replaceWith(r); - }); - - if (!viewer.running) viewer.onExperimentStarted(); - beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name, dt); - }); - } - - //if we get to this point, we install the interval function - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - var csrftoken = $.cookie('csrftoken'); - xhr.setRequestHeader('X-CSRFToken', csrftoken); - } - }); - - var timeout_id = window.setInterval(_do_update, interval); - -} +beat.experiments.utils.update_blocks = function( + url, + st, + dt, + unveil, + objects, + dt_block_name, + viewer, + interval +) { + var _status = $(st).data(dt); + + if (_status === 'Failed') { + beat.experiments.utils.update_viewer(viewer, $(objects), dt_block_name, dt); + return; + } + + //only updates if in one of the "interesting" states + var interesting_states = ['Scheduled', 'Running', 'Canceling']; + if (interesting_states.indexOf(_status) <= -1) return; + + function _do_update() { + var _status = $(st).data(dt); + + if (interesting_states.indexOf(_status) <= -1) { + //experiment changed status - should reload + $(unveil).show(); + if (viewer.running) viewer.onExperimentDone(); + if (_status === 'Failed') { + beat.experiments.utils.update_viewer( + viewer, + $(objects), + dt_block_name, + dt + ); + } + return; + } + + var d = $.get(url); + + d.done(function(data) { + var parsed = $($.parseHTML(data)); + $(objects).each(function(idx) { + var _self = $(this); + var r = parsed.find('#' + _self.attr('id')); + //only replaces if it changed + var old_status = _self.data(dt); + var new_status = r.data(dt); + if (r && old_status !== new_status) _self.replaceWith(r); + }); + + if (!viewer.running) viewer.onExperimentStarted(); + beat.experiments.utils.update_viewer( + viewer, + $(objects), + dt_block_name, + dt + ); + }); + } + + //if we get to this point, we install the interval function + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + var csrftoken = $.cookie('csrftoken'); + xhr.setRequestHeader('X-CSRFToken', csrftoken); + }, + }); + + var timeout_id = window.setInterval(_do_update, interval); +}; diff --git a/beat/web/plotters/api.py b/beat/web/plotters/api.py index 6f15acb94b1a420974bb9fb6c9b25ad4600e5a03..782cb1326a35920a6512ee7004e995305d1690d0 100644 --- a/beat/web/plotters/api.py +++ b/beat/web/plotters/api.py @@ -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): diff --git a/beat/web/reports/api.py b/beat/web/reports/api.py index 2a38acc8f751e431faf1c0a469f162a7f2dac295..608ab89374cbdb47bf69ae40653ad038c8e0b686 100644 --- a/beat/web/reports/api.py +++ b/beat/web/reports/api.py @@ -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) diff --git a/beat/web/reports/api_urls.py b/beat/web/reports/api_urls.py index f8d4017819df11bc44cd09b3717f8fe273008e06..1d0440b7a6e7d04424699196c42d5bca94ff4ae6 100644 --- a/beat/web/reports/api_urls.py +++ b/beat/web/reports/api_urls.py @@ -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(), diff --git a/beat/web/reports/migrations/0003_report_last_edited_date.py b/beat/web/reports/migrations/0003_report_last_edited_date.py new file mode 100644 index 0000000000000000000000000000000000000000..35e29a56ff414ca3ff236558c676362ef1c772bd --- /dev/null +++ b/beat/web/reports/migrations/0003_report_last_edited_date.py @@ -0,0 +1,20 @@ +# -*- 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), + ), + ] diff --git a/beat/web/reports/migrations/0004_auto_20170410_1121.py b/beat/web/reports/migrations/0004_auto_20170410_1121.py new file mode 100644 index 0000000000000000000000000000000000000000..c2206e36f4f0be771d30b526adb72308afb01786 --- /dev/null +++ b/beat/web/reports/migrations/0004_auto_20170410_1121.py @@ -0,0 +1,147 @@ +# -*- 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) + ] diff --git a/beat/web/reports/models.py b/beat/web/reports/models.py index 4a2c87c78aaa2e8c9234519510f5a8e9769556ec..442effbd01fcd53cd1672e682961f705576168ba 100644 --- a/beat/web/reports/models.py +++ b/beat/web/reports/models.py @@ -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"]) diff --git a/beat/web/reports/permissions.py b/beat/web/reports/permissions.py index 9bfd0a1da13060cb1fae5729adbd522fcea65dc5..9ed36974dfa271a080f62cc0e7fac0fe55cf0273 100644 --- a/beat/web/reports/permissions.py +++ b/beat/web/reports/permissions.py @@ -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 diff --git a/beat/web/reports/serializers.py b/beat/web/reports/serializers.py index bc87f2f1c78ab27c5bb2d4f416d03820a8123662..7b1b14f08161b4a9ef3e1de8b8da0d81f36c2bcd 100644 --- a/beat/web/reports/serializers.py +++ b/beat/web/reports/serializers.py @@ -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'] #---------------------------------------------------------- diff --git a/beat/web/reports/static/reports/app/app.config.js b/beat/web/reports/static/reports/app/app.config.js index 0b9a5c26116275df6975e0e87cea94d33309d139..0896cf9b7006508d69f8198af583c1126751c86f 100644 --- a/beat/web/reports/static/reports/app/app.config.js +++ b/beat/web/reports/static/reports/app/app.config.js @@ -1,34 +1,32 @@ /* * 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; +} ); diff --git a/beat/web/reports/static/reports/app/app.js b/beat/web/reports/static/reports/app/app.js index 569fbec8bef5d28ad34bd09930343aa8ec04da6d..663256161a1b9cfeaeda43da27c3ffb0befa8836 100644 --- a/beat/web/reports/static/reports/app/app.js +++ b/beat/web/reports/static/reports/app/app.js @@ -1,51 +1,35 @@ /* * 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); - // //}, - //} - } - }) + $stateProvider + .state('report', { + url: '/', + views: { + } + }); }); diff --git a/beat/web/reports/templates/reports/dialogs/report_lock.html b/beat/web/reports/static/reports/app/controllers/groupsController.js similarity index 50% rename from beat/web/reports/templates/reports/dialogs/report_lock.html rename to beat/web/reports/static/reports/app/controllers/groupsController.js index 57dd4b9c64515f46a76c268a776053f41117f388..fd2ab9e8c19838c78071ec938ad3ede23b2de801 100644 --- a/beat/web/reports/templates/reports/dialogs/report_lock.html +++ b/beat/web/reports/static/reports/app/controllers/groupsController.js @@ -1,41 +1,60 @@ -{% 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 %} -<div id="{{ dialog_id }}" class="report_lock" title="Report" style="display:none"> - <p class="explanation">Locking a report</p> + */ +/* + * GroupsController + * 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){ + let vm = this; - <div class="explanation_text"> - <p class="experiments">1) Locking your report is the first step for publication.</p> - <p>2) Your report will not be editable anymore.</p> - <p>3) In order to do lock your report, we will need to make the experiments you have added non deletable. This means that your experiments will be locked too.</p> - <p>Do you wish to proceed?</p> - </div> - <div class="warnings" style="display:none"> - <p class="experiments">Sucessfully locked the report</p> - <ul class="experimentslist"></ul> - </div> + vm.expNamesToRemove = []; + vm.toggleExpName = (expName) => { + //console.log(vm.expNamesToRemove); + let idx = vm.expNamesToRemove.indexOf(expName); + if(idx > -1){ + vm.expNamesToRemove.splice(idx, 1); + } else { + vm.expNamesToRemove.push(expName); + } + //console.log(vm.expNamesToRemove); + }; - <div class="errors" style="display:none"> - <p class="errors">Some errors occured while attempting to lock the report:</p> - <ul class="errorslist"></ul> - </div> -</div> + vm.removeExperiments = () => { + if(vm.expNamesToRemove.length === 0){ + return; + } + + let url = UrlService.getRemoveExperimentUrl(); + return $http({ + headers: {'Content-Type': 'application/json'}, + url, + method: "POST", + data: { + experiments: [...vm.expNamesToRemove] + } + }) + .then(res => location.reload()) + ; + }; +}]); diff --git a/beat/web/reports/static/reports/app/controllers/reportController.js b/beat/web/reports/static/reports/app/controllers/reportController.js index e5acb779935a334c71a0acf89fe6f68b1abfc893..64b783d7bd56d685468883c979fbeafe2dc4f29d 100644 --- a/beat/web/reports/static/reports/app/controllers/reportController.js +++ b/beat/web/reports/static/reports/app/controllers/reportController.js @@ -1,518 +1,27 @@ /* * 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/. -*/ -//This controller retrieves data from the reportFactory/experimentFactory through the REST API and associates it with the $scope -//The $scope is ultimately bound to the report view -app.controller('reportController',['$scope', 'reportFactory', 'experimentFactory', 'plotterFactory', 'dataFactory', '$q', function ($scope, reportFactory, experimentFactory, plotterFactory, dataFactory, $q) -{ - $scope.q = $q; - $scope.user; - $scope.report_id; - $scope.url_prefix; - $scope.reportFactory = reportFactory; - $scope.report; - $scope.report_experiments = {}; - $scope.num_report_items = 0; - $scope.max_num = 0; - $scope.item_content = []; - $scope.table_item_content = []; - $scope.selectedObject = {}; + */ - $scope.tables_details = {}; - $scope.plots_details = {}; - $scope.report_experiments_blocks = {}; - $scope.report_experiments_blocks_merged_blocks = []; - $scope.report_experiments_blocks_computation_times = {}; - - $scope.report_experiments_alias = {}; - $scope.report_experiments_alias_from_content = {}; - $scope.floating_point_precision = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - $scope.floating_point_precision.selected = 10; - $scope.report_algorithm_parameters = {}; - $scope.report_algorithm_parameters_experiment = {}; - $scope.sorted_tables = []; //tables that were sorted - $scope.sorted_experiments_keys_tables = {}; - $scope.sorted_experiments_alias_keys_tables = {}; - $scope.sorted_experiments_keys_reverse = {}; - $scope.sorted_experiments_keys_tables_sortkey = {}; - - $scope.init = function(user, report_id, url_prefix, data_itemcontent_file, data_table_itemcontent_file) - { - $scope.user = user; - $scope.report_id = report_id; - $scope.url_prefix = url_prefix; - getReportData($scope.user, $scope.report_id); - $scope.item_content = dataFactory.getData(data_itemcontent_file); - $scope.table_item_content = dataFactory.getData(data_table_itemcontent_file); - } - - $scope.initWithReportNumber = function(report_number, url_prefix, data_itemcontent_file, data_table_itemcontent_file) - { - $scope.report_number = report_number; - $scope.url_prefix = url_prefix; - getReportDataFromNumber($scope.report_number); - $scope.item_content = dataFactory.getData(data_itemcontent_file); - $scope.table_item_content = dataFactory.getData(data_table_itemcontent_file); - } - - - function getReportData(user, report_id) - { - reportFactory.getReportInformation(user, report_id, $scope.url_prefix) - .success(function (reportData) - { - $scope.report = reportData; - $scope.report.url_prefix = $scope.url_prefix; - - var experiments = $scope.report.experiments; - if($scope.report.content.alias_experiments != undefined) - { - $scope.report_experiments_alias = $scope.report.content.alias_experiments; - } - - var promises = []; - - function lastTask(){ - getPlotters(); - } - - if(experiments != undefined) - { - promises.push(getExperimentDataForAuthor(experiments)); - $scope.q.all(promises).then(lastTask); - } - - - //if(experiments != undefined) - //{ - // for (var i = 0; i < experiments.length ; i++) - // { - // getExperimentData(experiments[i]); - // //$state.go("beat.reportInfo") - // } - - // getPlotters(); - //} - - }) - .error(function (error) - { - $scope.status = 'Unable to load report data: ' + error.message; - }); - - $('#tabs_progress').hide(); - } - - - function getReportDataFromNumber(report_number, $q) - { - reportFactory.getReportInformationFromNumber(report_number, $scope.url_prefix) - .success(function (reportData) - { - $scope.report = reportData; - $scope.report.url_prefix = $scope.url_prefix; - - var experiments = $scope.report.experiments; - if($scope.report.content.alias_experiments != undefined) - { - $scope.report_experiments_alias = $scope.report.content.alias_experiments; - } - - var promises = []; - - function lastTask(){ - getPlotters(); - } - - //if(experiments != undefined) - //{ - // for (var i = 0; i < experiments.length ; i++) - // { - // promises.push(getExperimentData(experiments[i])); - // } - - // $scope.q.all(promises).then(lastTask); - //} - if(experiments != undefined) - { - promises.push(getExperimentDataReportNumber(report_number)); - $scope.q.all(promises).then(lastTask); - } - }) - .error(function (error) - { - $scope.status = 'Unable to load report data: ' + error.message; - }); - - $('#tabs_progress').hide(); - } - - function getExperimentDataReportNumber(report_number) - { - experimentFactory.getAllExperimentResults($scope.url_prefix, report_number) - .then(function (experiments) - { - $scope.report_experiments = experiments; - $scope.report.all_experiments = experiments; - var experiment_aliases = Object.keys(experiments); - for(var i = 0; i < experiment_aliases.length; i++) - { - var experiment_alias = experiment_aliases[i]; - - if($scope.report.content.alias_experiments != undefined) - { - if($scope.report.content.alias_experiments[experiment_alias] != undefined) - { - setExperimentAliasFromContent(experiment_alias, $scope.report.content.alias_experiments[experiment_alias]); - } - else - { - setExperimentAlias(experiment_alias); - } - } - else - { - setExperimentAlias(experiment_alias); - } - intersectBlocks(experiment_alias); - algorithmParameters(experiment_alias); - } - //sort experiments in case it's sorted - if(!(jQuery.isEmptyObject($scope.report.content.sorted_tables_experiments)) && - !(jQuery.isEmptyObject($scope.report.content.sorted_tables_alias_experiments)) && - !(jQuery.isEmptyObject($scope.report.content.sorted_tables_keys_reverse)) && - !(jQuery.isEmptyObject($scope.report.content.sorted_tables_sortkey))) - { - $scope.sorted_experiments_keys_tables = $scope.report.content.sorted_tables_experiments; - if($scope.report.status == "locked") - { - //special case for locked reports - $scope.sorted_experiments_keys_tables = $scope.report.content.sorted_tables_alias_experiments; - } - $scope.sorted_experiments_alias_keys_tables = $scope.report.content.sorted_tables_alias_experiments; - $scope.sorted_experiments_keys_reverse = $scope.report.content.sorted_tables_keys_reverse; - $scope.sorted_experiments_keys_tables_sortkey = $scope.report.content.sorted_tables_sortkey; - } - for(var i = 0; i < Object.keys($scope.sorted_experiments_keys_tables_sortkey).length; i++) - { - $scope.sorted_tables.push(Object.keys($scope.sorted_experiments_keys_tables_sortkey)[i]); - } - }) - } - - function getExperimentDataForAuthor(all_experiments) - { - experimentFactory.getAllExperimentResultsForAuthor($scope.user, $scope.report_id, $scope.url_prefix) - .then(function (experiments) - { - $scope.report_experiments = experiments; - $scope.report.all_experiments = experiments; - var experiment_aliases = Object.keys(experiments); - for(var i = 0; i < experiment_aliases.length; i++) - { - var experiment_alias = experiment_aliases[i]; - - if($scope.report.content.alias_experiments != undefined) - { - if($scope.report.content.alias_experiments[experiment_alias] != undefined) - { - setExperimentAliasFromContent(experiment_alias, $scope.report.content.alias_experiments[experiment_alias]); - } - else - { - setExperimentAlias(experiment_alias); - } - } - else - { - setExperimentAlias(experiment_alias); - } - intersectBlocks(experiment_alias); - algorithmParameters(experiment_alias); - } - - //sort experiments in case it's sorted - if(!(jQuery.isEmptyObject($scope.report.content.sorted_tables_experiments)) && - !(jQuery.isEmptyObject($scope.report.content.sorted_tables_alias_experiments)) && - !(jQuery.isEmptyObject($scope.report.content.sorted_tables_keys_reverse)) && - !(jQuery.isEmptyObject($scope.report.content.sorted_tables_sortkey))) - { - $scope.sorted_experiments_keys_tables = $scope.report.content.sorted_tables_experiments; - if($scope.report.status == "locked") - { - //special case for locked reports - $scope.sorted_experiments_keys_tables = $scope.report.content.sorted_tables_alias_experiments; - } - $scope.sorted_experiments_alias_keys_tables = $scope.report.content.sorted_tables_alias_experiments; - $scope.sorted_experiments_keys_reverse = $scope.report.content.sorted_tables_keys_reverse; - $scope.sorted_experiments_keys_tables_sortkey = $scope.report.content.sorted_tables_sortkey; - } - for(var i = 0; i < Object.keys($scope.sorted_experiments_keys_tables_sortkey).length; i++) - { - $scope.sorted_tables.push(Object.keys($scope.sorted_experiments_keys_tables_sortkey)[i]); - } - - }) - } - - - function getExperimentData(experiment_id) - { - experimentFactory.getExperimentInformation($scope.url_prefix, experiment_id) - .then(function (experimentData) - { - var experiments = $scope.report_experiments; - experiments[experiment_id] = experimentData; - $scope.report_experiments = experiments; - $scope.report.all_experiments = experiments; - - if($scope.report.content.alias_experiments != undefined) - { - if($scope.report.content.alias_experiments[experiment_id] != undefined) - { - setExperimentAliasFromContent(experiment_id, $scope.report.content.alias_experiments[experiment_id]); - } - else - { - setExperimentAlias(experiment_id); - } - } - else - { - setExperimentAlias(experiment_id); - } - - intersectBlocks(experiment_id); - algorithmParameters(experiment_id); - }) - //.error(function (error) - //{ - // $scope.status = 'Unable to load experiment data - ' + experiment_id + ' : ' + error.message; - //}); - - //.success(function (experimentData) - //{ - // var experiments = $scope.report_experiments; - // experiments[experiment_id] = experimentData; - // $scope.report_experiments = experiments; - // $scope.report.all_experiments = experiments; - - // if($scope.report.content.alias_experiments != undefined) - // { - // if($scope.report.content.alias_experiments[experiment_id] != undefined) - // { - // setExperimentAliasFromContent(experiment_id, $scope.report.content.alias_experiments[experiment_id]); - // } - // else - // { - // setExperimentAlias(experiment_id); - // } - // } - // else - // { - // setExperimentAlias(experiment_id); - // } - // intersectBlocks(experiment_id); - //}) - //.error(function (error) - //{ - // $scope.status = 'Unable to load experiment data - ' + experiment_id + ' : ' + error.message; - //}); - } - - - function getPlotters() - { - plotterFactory.getPlotters($scope.url_prefix) - .success(function (plottersData) - { - $scope.report.plotters = plottersData; - getPlotterParameterData(); - }) - .error(function (error) - { - $scope.status = 'Unable to load plotters data - ' + ' : ' + error.message; - }); - } - - - function getDefaultPlotters() - { - plotterFactory.getDefaultPlotters($scope.url_prefix) - .success(function (defaultPlottersData) - { - $scope.report.defaultplotters = defaultPlottersData; - checkContent(); - }) - .error(function (error) - { - $scope.status = 'Unable to load default plotters parameter data - ' + ' : ' + error.message; - }); - } - - - function getPlotterParameterData() - { - plotterFactory.getPlotterParameter($scope.url_prefix) - .success(function (plotterParameterData) - { - $scope.report.plotterparameter = plotterParameterData; - //$scope.report.plotterparameter.push({"name":"Default"}); - getDefaultPlotters(); - }) - .error(function (error) - { - $scope.status = 'Unable to load plotter parameter data - ' + ' : ' + error.message; - }); - } - - - function checkContent() - { - if($scope.report.content != undefined) - { - - var orderkeys = []; - angular.forEach($scope.report.content, function(value, key) - { - orderkeys.push(key); - }); - - orderkeys.sort(function(a,b) - { - a = a.split("_"); - b = b.split("_"); - a_num = parseInt(a[a.length-1]) || -1; - b_num = parseInt(b[b.length-1]) || -1; - - return a_num - b_num; - }); - $scope.report.orderedcontent = orderkeys; - - if($scope.report.content.floating_point_precision != undefined) - { - $scope.floating_point_precision.selected = $scope.report.content.floating_point_precision; - } - - for(var i = 0; i < $scope.report.orderedcontent.length; i++) - { - var id_content = $scope.report.orderedcontent[i]; - addElementToReport(id_content); - } - } - } - - function addElementToReport(id_content) - { - var type = id_content.split("_")[0]; - $scope.$broadcast("addSavedElement", id_content, type); - } - - function intersectBlocks(experiment_id) - { - $scope.report_experiments_blocks[experiment_id] = Object.keys($scope.report_experiments[experiment_id].blocks_status); - var values = $.map($scope.report_experiments_blocks[experiment_id], function(value, key) { return value }); - $scope.report_experiments_blocks_merged_blocks.push(values); - } - - function algorithmParameters(experiment_id) - { - //go through globals - angular.forEach($scope.report_experiments[experiment_id].declaration.globals, function(value_algorithm, key_algorithm) - { - //only get algorithms - if(key_algorithm.indexOf("/") != -1) - { - $scope.report_algorithm_parameters[key_algorithm] = value_algorithm; - var experiment_algorithm_parameter = {}; - - if($scope.report_algorithm_parameters_experiment[experiment_id] == undefined) - { - $scope.report_algorithm_parameters_experiment[experiment_id] = {}; - } - - if($scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm] == undefined) - { - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm] = {}; - } - - //go through each blocks of the experiment and save parameters for given algorithm - angular.forEach($scope.report_experiments[experiment_id].declaration.blocks, function(value_block, key_block) - { - //check blocks that matches globals - if($scope.report_experiments[experiment_id].declaration.blocks[key_block]['algorithm'] == key_algorithm) - { - if($scope.report_experiments[experiment_id].declaration.blocks[key_block]['parameters'] != undefined) - { - //override globals parameters value if block contains 'parameters' key - angular.forEach($scope.report_experiments[experiment_id].declaration.blocks[key_block]['parameters'], function(value_block_parameter, key_block_parameter) - { - if($scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter] == undefined) - { - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter] = []; - } - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter].push(value_block_parameter); - - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter] = unique($scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter]); - }); - } - else - { - //get globals parameters value if block doesn't contain 'parameters' key - angular.forEach($scope.report_experiments[experiment_id].declaration.globals[key_algorithm], function(value_block_parameter, key_block_parameter) - { - if($scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter] == undefined) - { - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter] = []; - } - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter].push(value_block_parameter); - $scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter] = unique($scope.report_algorithm_parameters_experiment[experiment_id][key_algorithm][key_block_parameter]); - }); - } - } - }); - } - }); - } - - function setExperimentAlias(experiment_id) - { - $scope.report_experiments_alias[experiment_id] = experiment_id.split("/").pop(); - $scope.report_experiments_alias_from_content[experiment_id] = experiment_id.split("/").pop(); - } - - function setExperimentAliasFromContent(experiment_id, experiment_alias) - { - $scope.report_experiments_alias[experiment_id] = experiment_alias; - $scope.report_experiments_alias_from_content[experiment_id] = experiment_alias; - } - - function unique(list) - { - var result = []; - $.each(list, function(i, e) { - if ($.inArray(e, result) == -1) result.push(e); - }); - return result; - } - - -}]); +/* reportController + * bootstraps the reports angular code by requiring the ReportService. + * NOTE: DONT add any code to this controller. + */ +angular.module('reportApp').controller('reportController',['ReportService', function (ReportService){}]); diff --git a/beat/web/reports/static/reports/app/data/itemcontent.json b/beat/web/reports/static/reports/app/data/itemcontent.json deleted file mode 100644 index 297831665fdde6e7bf49f9756e1f9e6c697617a8..0000000000000000000000000000000000000000 --- a/beat/web/reports/static/reports/app/data/itemcontent.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "identifier": "table", - "name": "Table", - "description": "Compare elements in a table" - }, - { - "identifier": "plot", - "name": "Figure", - "description": "Compare graphical elements (ROC, bar plots, etc.)" - } -] diff --git a/beat/web/reports/static/reports/app/data/table_itemcontent.json b/beat/web/reports/static/reports/app/data/table_itemcontent.json deleted file mode 100644 index 13e37560096b4947b4c0a316a0bb9f71a7698260..0000000000000000000000000000000000000000 --- a/beat/web/reports/static/reports/app/data/table_itemcontent.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "identifier": "tableresults", - "name": "Results", - "description": "Only compare experiments results" - } -] diff --git a/beat/web/reports/static/reports/app/directives/bootstrapModal.js b/beat/web/reports/static/reports/app/directives/bootstrapModal.js new file mode 100644 index 0000000000000000000000000000000000000000..3a60c59e9f31fc0a32279295071eaea48e93b39d --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/bootstrapModal.js @@ -0,0 +1,69 @@ +/* + * 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/. + */ + +/* + * bootstrapModal + * Desc: + * represents a modal from Bootstrap 3 + */ +angular.module('reportApp') +.directive("bootstrapModal", [function(){ + return { + scope: { + domId: '@', + buttonCancelFunc: '&', + buttonSubmitFunc: '&', + buttonCancelText: '@', + buttonSubmitText: '@' + }, + link: function(scope){ + }, + transclude: { + 'title': '?bTitle', + 'content': '?bContent', + 'footer': '?bFooter' + }, + template: ` +<div class="modal fade" id="{{ domId }}" tabindex="-1" role="dialog" aria-labelledby="reportModalLabel"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <h4 class="modal-title" id="reportModalLabel" ng-transclude='title'>Modal Title</h4> + </div> + <div class="modal-body" ng-transclude='content'> + Content + </div> + <div class="modal-footer" ng-transclude='footer'> + <button ng-click='buttonCancelFunc && buttonCancelFunc()()' type="button" class="btn btn-default" data-dismiss="modal"> + Cancel + </button> + <button ng-if='buttonSubmitText' ng-click='buttonSubmitFunc && buttonSubmitFunc()()' type="button" class="btn btn-primary" data-dismiss="modal"> + {{ buttonSubmitText }} + </button> + </div> + </div> + </div> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/downloadLink.js b/beat/web/reports/static/reports/app/directives/downloadLink.js new file mode 100644 index 0000000000000000000000000000000000000000..74733f12af922559ff6bd66723151aa0d67f8da3 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/downloadLink.js @@ -0,0 +1,75 @@ +/* + * 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/. + */ + +/* + * downloadLink + * Desc: + * A button to download the content of the report item in the + * chosen file format (PNG, JPEG, PDF) + */ +angular.module('reportApp') +.directive("downloadLink", ['PlotService', function(PlotService){ + return { + scope: { + domId: '@', + group: '=', + itemId: '=' + }, + link: function(scope){ + scope.filetypes = [ + 'PNG', + 'JPEG', + 'PDF' + ]; + + // download the img via the invisible link element + scope.downloadImgs = (e, ftype) => { + // get plot data URL + PlotService.downloadPlot(scope.group, scope.itemId, ftype) + .then(data => { + e.preventDefault(); + // invisible el, see end of template + const a = document.querySelector(`#${scope.domId}-download`); + + a.href = data; + a.download = `${scope.domId}-${scope.itemId}.${ftype.toLowerCase()}`; + a.click(); + a.href = ''; + a.download = ''; + }) + ; + }; + }, + template: ` +<div class='btn-group'> + <button type='button' class='btn btn-primary dropdown-toggle' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'> + Download + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li><a ng-repeat='t in filetypes' ng-click='downloadImgs($event, t)'>{{ t }}</a></li> + </ul> +</div> +<a id='{{ domId }}-download'></a> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/factories/dataFactory.js b/beat/web/reports/static/reports/app/directives/dragHandle.js similarity index 64% rename from beat/web/reports/static/reports/app/factories/dataFactory.js rename to beat/web/reports/static/reports/app/directives/dragHandle.js index f10f9bdca3de6b89f328a213dcc7d77acff1ab6e..4eb0fce3646b9fc1caaccfb8a4814dfb6f2e8a74 100644 --- a/beat/web/reports/static/reports/app/factories/dataFactory.js +++ b/beat/web/reports/static/reports/app/directives/dragHandle.js @@ -1,40 +1,46 @@ /* * 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/. -*/ -//This factory retrieves data from the static files and associates it with the $scope -app.factory('dataFactory', function($http) -{ - var getData = function (URL) { - //return $http.get(URL + 'itemcontent.json'); - var obj = {content:null}; - //return $http.get(URL); - $http.get(URL).success(function(data) - { - obj.content = data; - }); + */ - return obj; - }; - - return { - getData: getData - }; -}); +/* + * dragHandle + * Desc: + * displays the drag handle button, and adds the specified element class + * to the parent el + */ +angular.module('reportApp') +.directive("dragHandle", [function(){ + return { + scope: { + handleHelperClass: '@' + }, + link: function(scope, el){ + el.addClass(`${scope.handleHelperClass} btn-group`); + }, + template: ` +<span + class='btn btn-default drag-handle' + data-toggle='tooltip' data-placement='top' title='Drag to re-order'> + <i class='fa fa-arrows fa-lg'></i> +</span> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/edit/addGroupMenu.js b/beat/web/reports/static/reports/app/directives/edit/addGroupMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..dcefe7f83a0fedd4b9110c930596efdfe8f8e5bf --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/edit/addGroupMenu.js @@ -0,0 +1,70 @@ +/* + * 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/. + */ + +/* + * groupAddGroupMenu + * Desc: + * the menu & validating for creating a new group + */ +angular.module('reportApp') +.directive("groupAddGroupMenu", ['GroupsService', function(GroupsService){ + return { + scope: { + }, + link: function(scope){ + scope.newGroupName = { val: '' }; + // validates the user input + scope.hasError = (val) => { + // if val is undefined, empty, or a dup, its an err + const isErr = !val || val.length === 0 || GroupsService.groups.find(g => g.name === val); + // cast to boolean + return !!isErr; + }; + + // creates a new group if the new group name is valid + // wipes the input on successful creation + scope.createGroup = () => { + if(scope.hasError(scope.newGroupName.val)){ + return; + } + + GroupsService.createGroup(scope.newGroupName.val); + scope.newGroupName.val = ''; + }; + }, + template: ` +<form ng-submit='createGroup()'> + <div class='form-group' ng-class="{'has-error': hasError(newGroupName.val)}"> + <div class="input-group"> + <span class="input-group-btn"> + <button ng-click='createGroup()' class="btn btn-default" type="button"> + <i class="fa fa-plus" aria-hidden="true"></i> + </button> + </span> + <input required id='createNewGroupInput' ng-model='newGroupName.val' type="text" class="form-control" placeholder="New group name..."> + </div> + </div> +</form> +` + }; +}]); + diff --git a/beat/web/reports/static/reports/app/directives/edit/addItemsMenu.js b/beat/web/reports/static/reports/app/directives/edit/addItemsMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..de4455da844f15e2cd6e963d3aab883b9050098d --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/edit/addItemsMenu.js @@ -0,0 +1,153 @@ +/* + * 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/. + */ + +/* + * groupAddItemsMenu + * Desc: + * the button group for adding report items (plot, table, text) to a group + */ +angular.module('reportApp') +.directive("groupAddItemsMenu", ['ExperimentsService', 'GroupsService', 'PlotService', function(ExperimentsService, GroupsService, PlotService){ + return { + scope: { + group: '=' + }, + link: function(scope){ + // finds the id for the next report item of + // the given type + // by looking at the existing items + const getNextItemId = (type) => { + const formatId = (type, count) => `${type}_${count}`; + let currCount = 0; + let nextId = formatId(type, currCount); + while(scope.group.reportItems.find(i => i.id === nextId)){ + currCount++; + nextId = formatId(type, currCount); + } + + return nextId; + }; + + scope.plottables = ExperimentsService.plottables; + + // helper func for adding a table + scope.addNewTable = () => { + const id = getNextItemId('table'); + + // default fields are fields that are meant for tables + // that have the 'primary' property on the field value set to true + const defaultFieldsSet = Object.entries(ExperimentsService.tableables) + // only exps in group + .filter(([expName, o]) => scope.group.experiments.includes(expName)) + .map(([expName, o]) => Object.entries(o)) + .reduce((a, fEntries) => [...a, ...fEntries], []) + // only fields with primary == true + .filter(([fName, o]) => o.primary) + .map(([fName, o]) => fName) + .reduce((s, fName) => s.add(fName), new Set()) + ; + + const defaultFields = Array.from(defaultFieldsSet); + //console.log(defaultFields); + + // tables have an arr of selected fields + // and a float precision + let content = { + itemName: `Table`, + fields: defaultFields, + precision: 10 + }; + + scope.group.addReportItem(id, content); + }; + + // helper func for adding a plot + scope.addNewPlot = (plot) => { + let id = getNextItemId('plot'); + + // plots have a given name (by analyzer) + // and a plot type + let content = { + itemName: `${plot.label}`, + name: plot.label, + type: plot.type, + merged: true, + savedPlotter: '', + savedConfig: '' + }; + + content.savedPlotter = PlotService.getPlotter(content).name; + content.savedConfig = PlotService.getPlotterConfig(content).name; + //console.log(content); + + scope.group.addReportItem(id, content); + }; + + // helper func for adding a text block + scope.addNewText = () => { + let id = getNextItemId('text'); + // text blocks just have raw RST + let content = { + itemName: 'Text', + text: '' + }; + + scope.group.addReportItem(id, content); + }; + }, + template: ` +<div class="btn-group" role="group"> + <button + ng-disabled='group.experiments.length == 0' + type="button" + class="btn btn-default dropdown-toggle" + data-toggle="dropdown" + aria-haspopup="true" + aria-expanded="false"> + Add Plot + <span class="caret"></span> + </button> + <ul class='dropdown-menu' ng-repeat='(expName, plots) in plottables' ng-if='expName === group.experiments[0]'> + <li ng-repeat='plot in plots'> + <a ng-click='addNewPlot(plot)'>{{ plot.label }} <i>({{ plot.type }})</i></a> + </li> + </ul> +</div> +<button + ng-disabled='group.experiments.length == 0' + class='btn btn-default' + ng-click='addNewTable()'> + Add Table +</button> +<div class="btn-group" role="group"> + <button + ng-disabled='group.experiments.length == 0' + ng-click='addNewText()' + type="button" + class="btn btn-default" + > + Add Text Block + </button> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/edit/tableFieldSelector.js b/beat/web/reports/static/reports/app/directives/edit/tableFieldSelector.js new file mode 100644 index 0000000000000000000000000000000000000000..b16c7394463f24b72569ea8ce5bcb256fe8e8e57 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/edit/tableFieldSelector.js @@ -0,0 +1,202 @@ +/* + * 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/. + */ + +/* + * GroupTableFieldSelector + * Desc: + * Handles the choosing of table columns + */ +angular.module('reportApp') +.directive("groupTableFieldSelector", ['GroupsService', 'ExperimentsService', function(GroupsService, ExperimentsService){ + return { + scope: { + id: '=', + group: '=', + // currently selected columns for the table + colsSelected: '=', + // function to execute when the user clicks the submit button + buttonAction: '&', + // title for the menu + title: '@', + // text for the submit button + buttonText: '@' + }, + link: function(scope){ + // bootstrap's auto dropdown toggling is disabled for the table creation dropdown + // add a click handler for the table creation dropdown submit button to toggle + // manually + scope.clickButton = (e) => { + $(`#${scope.id}`).dropdown('toggle'); + scope.buttonAction()(); + }; + + scope.tableables = () => { + // start with the tableables generated in experimentsservice + const tableables = ExperimentsService.tableables; + const fieldArr = Object.entries(tableables) + // only look at fields that are from an experiment in the group + .filter(([e, fields]) => scope.group.experiments.includes(e)) + // get the names of the fields + .map(([e, fields]) => Object.keys(fields)) + // make one big array of all field names + .reduce((arr, fArr) => [...arr, ...fArr], []) + ; + + // converting to and from a Set is a simple way of + // removing dups + const arr = Array.from(new Set(fieldArr)); + + return arr; + }; + + // has this fieldName already been processed? + // need to look at the already-processed field names + scope.isUniqueTableable = (expName, fieldName) => { + const tableables = ExperimentsService.tableables; + const concatNames = (eName, fName) => `${eName}.${fName}`; + + // see if this field is a repeat + const isRepeat = Object.entries(tableables) + .filter(([e, fields]) => { + // only look at tableables of exps that are in group + let isInGroup = scope.group.experiments.includes(e); + // and have already been looked at + let alreadyChecked = Object.keys(tableables).indexOf(e) < Object.keys(tableables).indexOf(expName); + return isInGroup && alreadyChecked; + }) + // get field names + .map(([e, fields]) => Object.keys(fields)) + // flatten + .reduce((arr, fArr) => [...arr, ...fArr], []) + // does this flattened array have this field name in it? + .includes(fieldName) + ; + + // if it isnt a repeat, its unique! + return !isRepeat; + }; + + // many tableable fields are fields from an analyzer, a block, or something else + // these fields have a field group name, a '.', and an actual field name + // find these group names and use them to subdivide the list of fields in the menu + scope.tableablesGroups = () => { + let groupNames = scope.tableables() + .filter(f => f.includes('.')) + .map(f => f.split('.')[0]); + + const sorted = Array.from(new Set(groupNames)) + .sort((a, b) => groupNames.indexOf(a) - groupNames.indexOf(b)) + + return sorted; + }; + + // finds the actual field name whether its in a field group or not + scope.groupName = (field) => field.includes('.') ? field.split('.')[0] : field; + + // finds the actual field name whether its in a field group or not + scope.subfieldName = (field) => field.includes('.') ? field.split('.').slice(1).join('.') : field; + + scope.shouldShowField = (fName, gName) => { + const isUnique = scope.isUniqueTableable(fName); + const isInGroup = scope.groupName(fName) == gName; + + // console.log(`for field "${fName}" and group "${gName}":`); + // console.log(`isUnique: ${isUnique}; isInGroup: ${isInGroup}`); + + return isUnique && isInGroup; + } + + // toggle the selection of a field + scope.toggleField = (fName) => { + let idx = scope.colsSelected.indexOf(fName); + if(idx > -1){ + scope.colsSelected.splice(idx, 1); + } else { + scope.colsSelected.push(fName); + scope.colsSelected.sort((a, b) => scope.tableables().indexOf(a) - scope.tableables().indexOf(b)); + } + }; + }, + template: ` +<button id='{{ id }}' type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {{ title }} + <span class="caret"></span> +</button> +<div class='dropdown-menu' ng-click="$event.stopPropagation();" style='width:300px;'> + <h4>Select columns to show in Table</h4> + <form style='max-height: 200px; overflow-y: scroll;'> + <fieldset> + <h4>General</h4> + <div + ng-repeat='fName in tableables()' + ng-if="isUniqueTableable(fName) && !fName.includes('.')"> + <label> + <input + type='checkbox' + value='{{ fName }}' + ng-checked='colsSelected.includes(fName)' + ng-click='toggleField(fName)'> + {{ fName }} + </label> + </div> + </fieldset> + <fieldset ng-repeat='gName in tableablesGroups()'> + <h4>{{ gName }}</h4> + <div + ng-repeat='fName in tableables()' + ng-if='shouldShowField(fName, gName)'> + <label> + <input + type='checkbox' + value='{{ fName }}' + ng-checked='colsSelected.includes(fName)' + ng-click='toggleField(fName)'> + {{ subfieldName(fName) }} + </label> + </div> + </fieldset> + </form> + <!-- + <select multiple ng-model='colsSelected'> + <optgroup label='General'> + <option + ng-repeat='fName in tableables()' + ng-if="isUniqueTableable(fName) && !fName.includes('.')" + value='{{ fName }}'> + {{ fName }} + </option> + </optgroup> + <optgroup ng-repeat='gName in tableablesGroups()' label='{{ gName }}'> + <option + ng-repeat='fName in tableables()' + ng-if='isUniqueTableable(fName) && fName.startsWith(gName)' + value='{{ fName }}'> + {{ subfieldName(fName) }} + </option> + </optgroup> + </select> + !--> + <button class='btn btn-default' ng-click='clickButton($event)'>{{ buttonText }}</button> +</div> +` + } +}]); diff --git a/beat/web/reports/templates/reports/dialogs/report_saved.html b/beat/web/reports/static/reports/app/directives/editableLabel.js similarity index 60% rename from beat/web/reports/templates/reports/dialogs/report_saved.html rename to beat/web/reports/static/reports/app/directives/editableLabel.js index ec97e677216732586740f73eb9822f3b9024185e..01f9eecd3ef2664dba66a86995ad93f1a916b858 100644 --- a/beat/web/reports/templates/reports/dialogs/report_saved.html +++ b/beat/web/reports/static/reports/app/directives/editableLabel.js @@ -1,29 +1,52 @@ -{% 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 %} -<div id="{{ dialog_id }}" class="report_remove_experiment" title="Report" style="display:none"> - <p class="explanation">Report status</p> + */ +/* + * editableLabel + * Desc: + * represents an editable label (group name, report item name, etc.) + */ +angular.module('reportApp').directive("editableLabel", ['UrlService', function(UrlService){ + return { + scope: { + obj: '=', + field: '@' + }, + link: function(scope){ + scope.isViewmode = UrlService.isViewmode; + }, + template: ` +<span style='display: inline-block;'> + <span ng-if='isViewmode()'>{{ obj[field] }}</span> + <input + ng-if='!isViewmode()' + style='display: inline;' + required + type='text' + class='form-control' + placeholder='A label...' + ng-model='obj[field]'/> +</span> +` + }; +}]); - <div class="explanation_text"> - <p>Your report has been successfully saved</p> - </div> -</div> diff --git a/beat/web/reports/static/reports/app/directives/error.js b/beat/web/reports/static/reports/app/directives/error.js new file mode 100644 index 0000000000000000000000000000000000000000..a8759663eef6136d0f7bc6580a96766d7be2d0dc --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/error.js @@ -0,0 +1,88 @@ +/* + * 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/. + */ + +/* + * reportError + * Desc: + * Watches the ErrorService's error list and processes an error if + * its not empty. + * 'Processing': showing a modal to the user about the error. + * + */ +angular.module('reportApp') +.directive("reportError", ['ErrorService', '$timeout', function(ErrorService, $timeout){ + return { + scope: { + }, + restrict: 'E', + link: function(scope){ + scope.currError = { + message: '', + error: '' + }; + + const errors = []; + + const processError = () => { + const modalOpen = ($("#errorReportModal").data('bs.modal') || {}).isShown; + if(modalOpen || errors.length === 0){ + return; + } + + // save our error + const e = errors.shift(); + scope.currError.message = e.message; + scope.currError.error = e.error; + scope.$apply(); + + // pop up the modal + $('#errorReportModal').modal(); + }; + + $timeout(() => { + $('#errorReportModal').on('hidden.bs.modal', () => { + // finished processing the last error + // process the next one + processError(); + }); + }, 0); + + scope.$on('user:error', (event, error) => { + errors.push(error); + processError(); + }); + }, + template: ` +<bootstrap-modal dom-id='errorReportModal' button-cancel-text='Continue'> + <b-title> + Error + </b-title> + <b-content> + <p>There was an error:</p> + <p>{{ currError.message }}</p> + <small>Details:</small> + <pre>{{ currError.error }}</pre> + </b-content> +</bootstrap-modal> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/experimentsTable.js b/beat/web/reports/static/reports/app/directives/experimentsTable.js new file mode 100644 index 0000000000000000000000000000000000000000..7ff2edbfbb3706554bf365bbcef3bcb17fd7e67d --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/experimentsTable.js @@ -0,0 +1,138 @@ +/* + * 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/. + */ + +/* + * experimentsTable + * Desc: + * displays the report's experiments table + * - Experiment statuses + * - Name + * - Databases/Protocols + * - Analyzer + * + * also lets the user remove experiments from the report. + */ +angular.module('reportApp') +.directive("experimentsTable", ['GroupsService', 'ExperimentsService', 'UrlService', 'ReportService', function(GroupsService, ExperimentsService, UrlService, ReportService){ + return { + scope: { + }, + link: function(scope){ + scope.ReportService = ReportService; + scope.domId = `experiments-table`; + scope.getAnalyzerFromExpName = ExperimentsService.getAnalyzerFromExpName; + scope.getExpUrl = UrlService.getExperimentUrl; + scope.getBlockUrl = UrlService.getBlockUrl; + scope.getDatabaseUrl = UrlService.getDatabaseUrl; + scope.getExperimentListPath = UrlService.getExperimentListPath; + scope.isViewmode = UrlService.isViewmode; + + scope.expNames = ExperimentsService.experimentNames; + scope.exps = ExperimentsService.experiments; + + scope.groups = GroupsService.groups; + + // collects an array of formatted databases & protocols of an experiment + // format is "<database name>@<protocol name>" + scope.getExpDatabases = (expName) => { + let dbs = scope.exps[expName].declaration.datasets; + return Array.from(new Set(Object.values(dbs).map(db => `${db.database}@${db.protocol}`))); + }; + + scope.deleteExpFromReport = (expName) => { + ExperimentsService.deleteExperiment(expName); + }; + }, + template: ` +<div id='{{ domId }}' class='panel panel-default'> + <div id='{{ domId }}-heading' class="panel-heading" role="tab"> + <h4 class="panel-title"> + <a + class='' + role="button" + data-toggle="collapse" + data-parent="#{{ domId }}-heading" + href="#collapse-{{ domId }}" + aria-expanded="true" + aria-controls="collapse-{{ domId }}"> + Experiments List + </a> + </h4> + </div> + <div id="collapse-{{ domId }}" + class="panel-collapse collapse in" + role="tabpanel" + aria-labelledby="{{ domId }}-heading"> + <table ng-if='expNames.length > 0' class="table table-striped table-hover"> + <thead> + <tr> + <th ng-if='!isViewmode()'></th> + <th ng-if='isViewmode() && groups.length == 1'>Alias</th> + <th>Experiment</th> + <th>Databases/Protocols</th> + <th>Analyzer</th> + </tr> + </thead> + <tbody> + <tr ng-repeat='expName in expNames'> + <td ng-if='!isViewmode()'> + <div class='btn-group action-buttons'> + <span + ng-click='deleteExpFromReport(expName)' + style='cursor: pointer;' + class="btn-delete" + data-toggle="tooltip" + data-placement="top" + title="Remove Experiment from Report"> + <i class="fa fa-times fa-lg"></i> + </span> + </div> + </td> + <td ng-if='isViewmode() && groups.length == 1'> + <span ng-if='groups[0].experiments.includes(expName)'> + {{ groups[0].aliases[expName] }} + </span> + </td> + <td><a href='{{ getExpUrl(expName) }}'>{{ expName }}</a></td> + <td> + <span ng-repeat='db in getExpDatabases(expName)'> + <a href='{{ getDatabaseUrl(db.split("@")[0]) }}'>{{ db }}</a> + + </span> + </td> + <td>{{ getAnalyzerFromExpName(expName) }}</td> + </tr> + </tbody> + </table> + + <div class='panel-body' ng-if='expNames.length == 0'> + <i class="fa fa-warning fa-lg"></i> + You have <strong>not added any experiments</strong> to this report yet. + You may add experiments from throughout the platform + (e.g. <a href="{{ getExperimentListPath() }}{{ ReportService.author }}/">your experiment list page</a>) + to unlock editing features for this report. + </div> + </div> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/layout.js b/beat/web/reports/static/reports/app/directives/layout.js new file mode 100644 index 0000000000000000000000000000000000000000..f32d9a5271abcba1d23fe006a55a0ed0e1b934b9 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/layout.js @@ -0,0 +1,66 @@ +/* + * 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/. + */ + +/* + * groupsLayout + * Desc: + * controls the layout of the reports content, + * generating group panels using the GroupsService data, + * and holding the menu for adding a group + */ +angular.module('reportApp').directive("groupsLayout", ['GroupsService', 'UrlService', function(GroupsService, UrlService){ + return { + scope: { + }, + link: function(scope, el, attr){ + scope.GroupsService = GroupsService; + scope.isViewmode = UrlService.isViewmode; + + // drag handle CSS selector + scope.sortableOptions = { + handle: '.dragGroup .drag-handle' + }; + }, + template: ` +<experiments-table></experiments-table> +<div ng-if='!isViewmode()' group-add-group-menu class='panel'></div> + +<div id='groupsLayout' class='panel-group'> + <div ng-if='!isViewmode() || GroupsService.groups.length > 1' + ui-sortable='sortableOptions' + ng-model='GroupsService.groups' + id='groupsLayout' + class='panel-group'> + <div + group-panel-content + style='margin-bottom: 5px;' + ng-repeat='group in GroupsService.groups' + group='group'> + </div> + </div> + <div ng-if='isViewmode() && GroupsService.groups.length == 1'> + <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 new file mode 100644 index 0000000000000000000000000000000000000000..ead9538a1da8b32f5c2e248866a38bb3c0104fc2 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/lock.js @@ -0,0 +1,59 @@ +/* + * 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/. + */ + +/* + * reportLock + * Desc: + * Displays a modal for locking the current report. + */ +angular.module('reportApp') +.directive("reportLock", ['ReportService', 'ErrorService', function(ReportService, ErrorService){ + return { + scope: { + }, + restrict: 'E', + link: function(scope, el){ + // sends the request to lock the report + scope.lockReport = () => { + return 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'> + <b-title> + Lock Report + </b-title> + <b-content> + <p>Locking your report is the first step for publication.</p> + <p>Your report will not be editable anymore.</p> + <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> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/panelContainer.js b/beat/web/reports/static/reports/app/directives/panelContainer.js new file mode 100644 index 0000000000000000000000000000000000000000..e971fe661eafad4686000287745cbda4d403c313 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/panelContainer.js @@ -0,0 +1,68 @@ +/* + * 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/. + */ + +/* + * panelContainer + * Desc: + * displays a Bootstrap panel with the specified header & body content + */ +angular.module('reportApp') +.directive("panelContainer", [function(){ + return { + scope: { + noPanelBody: '=', + domId: '=' + }, + restrict: 'A', + transclude: { + 'headerSlot': 'header', + 'contentSlot': 'content' + }, + link: function(scope, el){ + el.addClass('panel panel-default'); + }, + template: ` +<div id="{{domId}}-heading" class="panel-heading" role="tab"> + <h4 class="panel-title"> + <a + class='' + role="button" + data-toggle="collapse" + data-parent="#{{domId}}-heading" + href="#collapse-{{domId}}" + aria-expanded="true" + aria-controls="collapse-{{domId}}"> + </a> + <span ng-transclude='headerSlot'>Header Content</span> + </h4> +</div> +<div id="collapse-{{domId}}" + class="panel-collapse collapse in" + role="tabpanel" + aria-labelledby="{{domId}}-heading"> + <div ng-class='{ "panel-body": !noPanelBody }' ng-transclude='contentSlot'> + Body Content with hasPanelBody: {{ hasPanelBody }} + </div> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/panelContent.js b/beat/web/reports/static/reports/app/directives/panelContent.js new file mode 100644 index 0000000000000000000000000000000000000000..9f6319b036b2e8e4c739cc3502f38410a8d73bed --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/panelContent.js @@ -0,0 +1,83 @@ +/* + * 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/. + */ + +/* + * groupPanelContent + * Desc: + * presents the group to the user in logical blocks: + * - a panel holds content + * - panel header contains an (editable) group name label + * - panel header also contains the buttons to add report items + * - two sub-panels are added: the experiments list, and the items list + */ +angular.module('reportApp').directive("groupPanelContent", ['GroupsService', 'UrlService', function(GroupsService, UrlService){ + return { + scope: { + group: '=' + }, + link: function(scope){ + scope.deleteGroup = GroupsService.deleteGroup; + scope.isViewmode = UrlService.isViewmode; + }, + template: ` +<div panel-container class='dragGroup' domId='group.name'> + <header> + <a + class='' + role="button" + data-toggle="collapse" + data-parent="#{{group.name}}-heading" + href="#collapse-{{group.name}}" + aria-expanded="true" + aria-controls="collapse-{{group.name}}"> + </a> + <editable-label obj='group' field='_name'></editable-label> + <div ng-if='!isViewmode()' class='btn-group action-buttons'> + <span + ng-click='deleteGroup(group.name)' + class="btn btn-default btn-delete" + data-toggle="tooltip" data-placement="top" title="Delete Group"> + <i class="fa fa-times fa-lg"></i> + </span> + <span drag-handle handle-helper-class='dragGroup'></span> + </div> + <div ng-if='!isViewmode()' + group-add-items-menu + class="btn-group" role="group" role='tab' + group='group'> + </div> + </header> + <content> + <div group-panel-experiments group='group' class='panel panel-default'></div> + <div + style='margin-top: 5px;' + ng-if='group.experiments.length > 0' + group-panel-items + group='group' + > + </div> + </content> +</div> +` + }; +}]); + diff --git a/beat/web/reports/static/reports/app/directives/panelExperiments.js b/beat/web/reports/static/reports/app/directives/panelExperiments.js new file mode 100644 index 0000000000000000000000000000000000000000..32326886e59ba01b32c1195c75158dc1dca8290c --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/panelExperiments.js @@ -0,0 +1,154 @@ +/* + * 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/. + */ + +/* + * groupPanelExperiments + * Desc: + * displays the experiments panel of the group - + * 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){ + return { + scope: { + group: '=' + }, + link: function(scope){ + scope.experiments = ExperimentsService.experiments; + scope.dropdownId = `${scope.group.name}_exp_add_dropdown`; + + scope.getExpName = (expName) => scope.experiments[expName] ? expName : expName.split('/').pop(); + const getExp = (expName) => scope.experiments[expName] || scope.experiments[expName.split('/').pop()]; + + // find experiments that are not in the group but are + // compatible with the existing experiments (if any) + scope.expsNotInGroup = () => { + return ExperimentsService.experimentNames + // exps not in group + .filter(e => !scope.group.experiments.includes(e)) + // exp has compatible analyzer + .filter(e => scope.group.analyzer === '' || ExperimentsService.getAnalyzerFromExpName(e) === scope.group.analyzer) + ; + }; + + // collects an array of formatted databases & protocols of an experiment + // format is "<database name>@<protocol name>" + scope.getExpDatabases = (expName) => { + const expObj = getExp(expName); + if(!expObj){ + return; + } + + let dbs = expObj.declaration.datasets; + return Array.from(new Set(Object.values(dbs).map(db => `${db.database}@${db.protocol}`))); + }; + + scope.getAnalyzerFromExpName = ExperimentsService.getAnalyzerFromExpName; + scope.getExpUrl = UrlService.getExperimentUrl; + scope.getBlockUrl = UrlService.getBlockUrl; + scope.getDatabaseUrl = UrlService.getDatabaseUrl; + scope.isViewmode = UrlService.isViewmode; + }, + template: ` +<div id="{{group.name}}-explist-heading" class="panel-heading" role="tab"> + <h4 class="panel-title"> + <a + class='' + role="button" + data-toggle="collapse" + data-parent="#{{group.name}}-explist-heading" + href="#collapse-{{group.name}}-explist" + aria-expanded="true" + aria-controls="collapse-{{group.name}}-explist"> + Experiments + </a> + <div ng-if='!isViewmode()' class='btn-group'> + <div class="dropdown"> + <button + class="btn btn-default dropdown-toggle" + ng-class='{disabled: expsNotInGroup().length === 0}' + type="button" + id="{{ dropdownId }}" + data-toggle="dropdown" + aria-haspopup="true" + aria-expanded="true"> + Add Experiment + <span class="caret"></span> + </button> + <ul class="dropdown-menu" aria-labelledby="{{ dropdownId }}"> + <li + ng-repeat='exp in expsNotInGroup()' + ng-click='group.addExperiment(exp, getAnalyzerFromExpName(exp))'> + <a>{{ exp }}</a> + </li> + </ul> + </div> + </div> + <i style='margin-left: 5px;' ng-if='group.analyzer.length > 0'> + Analyzer: <a href='{{ getBlockUrl(group.analyzer) }}'>{{ group.analyzer }}</a> + </i> + </h4> +</div> +<div id="collapse-{{group.name}}-explist" + class="panel-collapse collapse in" + role="tabpanel" + aria-labelledby="{{group.name}}-explist-heading"> + <div class="panel-body"> + <table ng-if='group.experiments.length > 0' class="table table-striped table-hover"> + <thead> + <tr> + <th ng-if='!isViewmode()'></th> + <th>Alias</th> + <th>Experiment</th> + <th>Databases/Protocols</th> + </tr> + </thead> + <tbody> + <tr ng-repeat='expName in group.experiments'> + <td ng-if='!isViewmode()'> + <div class='btn-group action-buttons'> + <span + style='cursor: pointer;' + ng-click='group.removeExperiment(expName)' + class="btn-delete" + data-toggle="tooltip" data-placement="top" title="Remove Experiment from Group"> + <i class="fa fa-times fa-lg"></i> + </span> + </div> + </td> + <td ng-if='!isViewmode()'><input ng-model='group.aliases[expName]'></input></td> + <td ng-if='isViewmode()'><span>{{ group.aliases[expName] }}</span></td> + <td><a href='{{ getExpUrl(expName) }}'>{{ getExpName(expName) }}</a></td> + <td> + <span ng-repeat='db in getExpDatabases(expName)'> + <a href='{{ getDatabaseUrl(db.split("@")[0]) }}'>{{ db }}</a> + + </span> + </td> + </tr> + </tbody> + </table> + </div> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/panelItems.js b/beat/web/reports/static/reports/app/directives/panelItems.js new file mode 100644 index 0000000000000000000000000000000000000000..358862dd6011b54b7979a23511a80f8491d41a08 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/panelItems.js @@ -0,0 +1,70 @@ +/* + * 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/. + */ + +/* + * groupPanelItems + * Desc: + * displays the panel of report items of the group, + * using the item container adaptor + */ +angular.module('reportApp').directive("groupPanelItems", [function(){ + return { + scope: { + group: '=' + }, + link: function(scope){ + // CSS selector for drag handles for the ui-sortable functionality + // TODO: this needs to be changed each time the HTML hierarchy changes. + // Make it hierarchy-independent + scope.sortableOptions = { + handle: '.dragItem .drag-handle' + }; + }, + template: ` +<div ui-sortable='sortableOptions' ng-model='group._reportItems' class='panel-group'> + <div ng-repeat='item in group.reportItems'> + <div group-table-item + style='margin-bottom: 5px;' + ng-if="item.id.includes('table')" + group='group' + item-id='item.id' + content='item.content'> + </div> + <div group-plot-item + style='margin-bottom: 5px;' + ng-if="item.id.includes('plot')" + group='group' + item-id='item.id' + content='item.content'> + </div> + <div group-text-item + style='margin-bottom: 5px;' + ng-if="item.id.includes('text')" + group='group' + report-item='item' + item-id='item.id'> + </div> + </div> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/plotItem.js b/beat/web/reports/static/reports/app/directives/plotItem.js new file mode 100644 index 0000000000000000000000000000000000000000..4c7655b1ba4429d745673cf24f0112a195eca054 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/plotItem.js @@ -0,0 +1,154 @@ +/* + * 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/. + */ + +/* + * groupPlotItem + * Desc: + * displays a plot report item (basically a container for the plots code to insert into) + */ +angular.module('reportApp') +.directive("groupPlotItem", ['ExperimentsService', 'PlotService', '$timeout', 'UrlService', function(ExperimentsService, PlotService, $timeout, UrlService){ + return { + scope: { + group: '=', + itemId: '=', + content: '=' + }, + link: function(scope){ + const group = scope.group; + scope.domId = `${scope.group.name}_${scope.itemId}`; + + // container for the plots applet to insert into + scope.renderDivId = `${scope.domId}-render`; + + // the callback for when the plot renders + // (called every time the plot re-renders, e.g. user changes config/merged + const updatePlotConfig = (selectedPlotter, selectedConfig, isMerged) => { + scope.content.merged = isMerged; + scope.content.savedPlotter = selectedPlotter; + scope.content.savedConfig = selectedConfig; + }; + + // wait until the container html element is rendered. + // angular will run these functions called with $timeout + // after everything has been rendered + $timeout(function() { + PlotService.addPlot(scope.group, scope.itemId, scope.renderDivId, updatePlotConfig); + }); + + let plotTimer; + + const updatePlot = () => { + clearTimeout(plotTimer); + + const queueUpdate = () => { + let el = document.querySelector(`#${scope.renderDivId}`); + // if the container is rendered and it already has had a render, + // redo the render + if(el && el.childNodes.length > 0){ + el.innerHTML = ''; + return PlotService.addPlot(scope.group, scope.itemId, scope.renderDivId, updatePlotConfig); + } + }; + + plotTimer = setTimeout(queueUpdate, 1000); + }; + + // if the user selected different plotter or config, rerender + scope.$watch( + () => `${scope.content.savedPlotter}|${scope.content.savedConfig}`, + updatePlot + ); + + scope.toggleMerged = () => { + scope.content.merged = !scope.content.merged; + updatePlot(); + }; + + scope.getPlotter = () => PlotService.getPlotter(scope.content); + scope.getPossiblePlotters = () => PlotService.plotters.filter(p => p.dataformat === scope.content.type).map(p => p.name); + scope.getPossibleConfigs = () => PlotService.getPossibleConfigs(scope.getPlotter()); + scope.isViewmode = UrlService.isViewmode; + }, + template: ` +<div panel-container dom-id='domId'> + <header> + <editable-label obj='content' field='itemName'></editable-label> + <div ng-if='!isViewmode()' class='btn-group action-buttons'> + <span + ng-click='group.removeReportItem(itemId)' + class="btn btn-default btn-delete" + data-toggle="tooltip" data-placement="top" title="Delete Plot"> + <i class="fa fa-times fa-lg"></i> + </span> + <span drag-handle handle-helper-class='dragItem'></span> + </div> + <div class='btn-group' role='group'> + <download-link + class='btn-group' + dom-id='{{ domId }}' + group='group' + item-id='itemId'> + </download-link> + <button ng-if='group.experiments.length > 1' class='btn btn-default' ng-click='toggleMerged()'> + <span ng-if='content.merged'><i class='fa fa-expand'></i> Expand</span> + <span ng-if='!content.merged'><i class='fa fa-compress'></i> Merge</span> + </button> + <div class='btn-group' role='group'> + <button + ng-class='{disabled: getPossiblePlotters().length === 1}' + class='btn btn-default dropdown-toggle' + type="button" + data-toggle="dropdown" + aria-haspopup="true" + aria-expanded="false"> + Plotter: {{ content.savedPlotter }} + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li ng-click='content.savedPlotter = p' ng-repeat='p in getPossiblePlotters()'><a>{{ p }}</a></li> + </ul> + </div> + <div class='btn-group' role='group'> + <button + ng-class='{disabled: getPossibleConfigs().length === 1}' + class='btn btn-default dropdown-toggle' + type="button" + data-toggle="dropdown" + aria-haspopup="true" + aria-expanded="false"> + Config: {{ content.savedConfig }} + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + <li ng-click='content.savedConfig = c' ng-repeat='c in getPossibleConfigs()'><a>{{ c }}</a></li> + </ul> + </div> + </div> + </header> + <content> + <div id='{{ renderDivId }}'></div> + </content> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/publish.js b/beat/web/reports/static/reports/app/directives/publish.js new file mode 100644 index 0000000000000000000000000000000000000000..96d295aa9b18f1146223dbd9fd9690555531d945 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/publish.js @@ -0,0 +1,116 @@ +/* + * 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/. + */ + +/* + * reportPublish + * Desc: + * Displays a modal for publishing the current report. + * It also lets the user select which algorithms will be made + * Visible or Public. + */ +angular.module('reportApp') +.directive("reportPublish", ['reportFactory', 'ReportService', '$timeout', 'ErrorService', function(reportFactory, ReportService, $timeout, ErrorService){ + return { + scope: { + }, + restrict: 'E', + link: function(scope, el){ + scope.algorithms = []; + scope.radios = {}; + + // sets up the data for the modal and opens it + scope.buildPublishModal = () => { + return reportFactory.publishReportAlgorithms(ReportService.author, ReportService.name, '') + .then(res => { + // rm past data + scope.algorithms.splice(0, scope.algorithms.length); + Object.keys(scope.radios).forEach(k => delete scope.radios[k]); + + // init new data + res.data.forEach(a => scope.algorithms.push(a)); + scope.algorithms.forEach(a => { scope.radios[a] = ''; }); + }) + .catch(e => { + $('#publishReportModal').modal('hide'); + // wait for the modal to close before logging error + setTimeout(() => ErrorService.logError(e, `Could not fetch the algorithms of the report, which is required to publish it.`), 500); + }) + ; + }; + + $timeout(() => $('#publishReportModal').on('show.bs.modal', function(e){ return scope.buildPublishModal(); }), 0); + + // sends the request to publish the report, along with the algs chosen to be OS + scope.publishReport = () => { + const openSourceAlgs = Object.entries(scope.radios) + .filter(([alg, val]) => val === 'openSource') + .map(([alg, val]) => alg); + + const data = JSON.stringify({ + visible_algorithms: openSourceAlgs + }); + + return ReportService.publishReport(openSourceAlgs) + .then(() => { + window.location.reload(); + }) + .catch(e => { + ErrorService.logError(e, `Could not publish the report.`); + }) + ; + }; + }, + template: ` +<bootstrap-modal dom-id='publishReportModal' button-submit-text='Publish' button-submit-func='publishReport'> + <b-title> + Publish Report + </b-title> + <b-content> + <p>Publishing your report will make it accessible to anyone. Your report will not be editable anymore.</p> + <div ng-if='algorithms.length > 0'> + <p>The following algorithms will become public. + They can either be <i>Visible</i> (no-one else can see inside the algorithm, but others can still use it) or <i>Open-Source</i> (the entire algorithm is visible as well as useable). + Choose which algorithms will be Visible, and which will be Open-Source:</p> + <form> + <table class='table table-striped'> + <thead> + <tr> + <td>Visible</td> + <td>Open-Source</td> + <td>Algorithm</td> + </tr> + </thead> + <tbody> + <tr ng-repeat='alg in algorithms'> + <td><input type='radio' name='{{ alg }}' ng-model='radios[alg]' value='visible'></td> + <td><input type='radio' name='{{ alg }}' ng-model='radios[alg]' value='openSource' checked></td> + <td>{{ alg }}</td> + </tr> + </tbody> + </table> + </form> + </div> + </b-content> +</bootstrap-modal> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/reportItemView.js b/beat/web/reports/static/reports/app/directives/reportItemView.js deleted file mode 100644 index 68f67287153717e141d39bc1c87cd9e2c158df41..0000000000000000000000000000000000000000 --- a/beat/web/reports/static/reports/app/directives/reportItemView.js +++ /dev/null @@ -1,2185 +0,0 @@ -/* - * 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/. -*/ -//Directive that returns an element which adds buttons on click which adds items -var app = angular.module('reportApp'); - -app.directive('loadedcontent', function() -{ - //return { - // link: function (scope, elem, attrs, ctrl) { - // angular.element(document).ready(function() { - // console.info(scope.report.content); - // - // }); - // //$(window).load(function() { - // // - // // console.info(scope.report.content); - // // - // // }); - // } - //} -}); - -//Directive for opening smart_selector list on "Add a report item" button click that displays options -app.directive("removeexperiment", function($compile) -{ - return function(scope, element, attrs) - { - //add new report item - element.bind("click", function() - { - removeExperiment(attrs.id); - }); - - - function removeExperiment(experiment_name) - { - beat.ui.report.remove_experiment('report_remove_experiment', experiment_name, scope); - } - } -}); - -//Directive for opening smart_selector list on "Add a report item" button click that displays options -app.directive("savereportitems", function($compile) -{ - return function(scope, element, attrs) - { - //add new report item - element.bind("click", function() - { - //var savecontent = []; - var savecontent = {}; - if(scope.plots_details != undefined) - { - angular.forEach(scope.plots_details, function(value, key) - { - //savecontent.push(key, value); - savecontent[key] = value; - }); - } - if(scope.tables_details != undefined) - { - angular.forEach(scope.tables_details, function(value, key) - { - savecontent[key] = value; - //savecontent.push(key, value); - }); - - if(scope.floating_point_precision.selected != undefined) - { - savecontent["floating_point_precision"] = scope.floating_point_precision.selected; - } - } - if(scope.report_experiments_alias != undefined) - { - var alias_experiments = {}; - angular.forEach(scope.report_experiments_alias, function(value, key) - { - alias_experiments[key] = value; - }); - savecontent["alias_experiments"] = alias_experiments; - } - - if(!(jQuery.isEmptyObject(scope.sorted_experiments_keys_tables)) && !(jQuery.isEmptyObject(scope.sorted_experiments_keys_reverse)) && !(jQuery.isEmptyObject(scope.sorted_experiments_keys_tables_sortkey))) - { - savecontent["sorted_tables_experiments"] = scope.sorted_experiments_keys_tables; - savecontent["sorted_tables_alias_experiments"] = scope.sorted_experiments_alias_keys_tables; - savecontent["sorted_tables_keys_reverse"] = scope.sorted_experiments_keys_reverse; - savecontent["sorted_tables_sortkey"] = scope.sorted_experiments_keys_tables_sortkey; - } - //call set report content from factory - var mydict = {}; - mydict["experiments"] = scope.report.experiments; - mydict["content"] = savecontent; - - for(var i = 0; i < scope.report.experiments.length; i++) - { - scope.report_experiments_alias_from_content[scope.report.experiments[i]] = scope.report_experiments_alias[scope.report.experiments[i]]; - } - - updateReport(mydict); - }); - - - function updateReport(data) - { - scope.reportFactory.updateReport(scope.user, scope.report_id, data, scope.url_prefix) - .success(function (reportData) - { - //alert("The report "+ scope.report_id +" has been saved."); - - beat.ui.report.report_saved('report_saved', scope); - }) - .error(function (error) - { - scope.status = 'Unable to update report data: ' + error.message; - - }); - - } - } -}); - -//Directive for opening smart_selector list on "Add a report item" button click that displays options -app.directive("lockreport", function($compile) -{ - return function(scope, element, attrs) - { - //add new report item - element.bind("click", function() - { - lockReport(); - }); - - - function lockReport() - { - beat.ui.report.lock_report('report_lock', scope); - } - } -}); - -//Directive for opening smart_selector list on "Add a report item" button click that displays options -app.directive("publishreport", function($compile) -{ - return function(scope, element, attrs) - { - //add new report item - element.bind("click", function() - { - publishReport(); - }); - - - function publishReport() - { - scope.reportFactory.publishReportAlgorithms(scope.user, scope.report_id, scope.url_prefix) - .success(function (reportData) - { - beat.ui.report.publish_report('report_publish', scope, reportData); - }) - .error(function (error) - { - scope.status = 'Unable to publish report data: ' + error; - $(".explanation_text").hide(); - $("#report_publish .warnings").hide(); - $("#report_publish .errors").show(); - if(error.detail != undefined) - scope.status = 'Unable to publish report data: ' + error.detail; - $("#report_publish .errors .errorslist").append(scope.status); - $("#button-report_publish-cancel").hide(); - }); - - - } - } -}); - -//Directive for opening smart_selector list on "Add a report item" button click that displays options -app.directive("addreportitem", function($compile) -{ - return function(scope, element, attrs) - { - scope.$on("addSavedElement", function(event, id_content, type) - { - var num_tables_in_content = 0; - var num_figures_in_content = 0; - for(var i = 0; i < Object.keys(scope.report.content).length; i++) - { - if(Object.keys(scope.report.content)[i].indexOf("table_") == 0) - { - num_tables_in_content++; - } - else if(Object.keys(scope.report.content)[i].indexOf("chart_") == 0) - { - num_figures_in_content++; - } - } - - if(type == "table") - { - var table_id = id_content; - var table_details = undefined; - - //take the one with more elements - if(Object.keys(scope.tables_details).length > num_tables_in_content) - { - table_details = scope.tables_details[id_content]; - } - else - { - table_details = scope.report.content[id_content]; - } - - var name_to_remove = "experiment"; - for(var i = 0; i < table_details.length; i++) - { - if(table_details[i].name == "experiment") - { - table_details.splice(i,1); - break; - } - } - - scope.tables_details[table_id] = table_details; - - var accessnumber = "no_number"; - if(scope.report.number == scope.report_number) - { - accessnumber = "number"; - } - var html_div_code = '<div class="panel panel-default" id="' + table_id + '"><div class="panel-heading" role="tab"><h4 class="panel-title"><a role="button" data-toggle="collapse" data-parent="#info-heading" href="#collapse-' + table_id + '" aria-expanded="true" aria-controls="collapse-info"><i class="fa fa-table"> Table</i></a></h4></div><div id="collapse-' + table_id + '" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="info-heading"><div class="panel-body" table-dynamic monster=' + scope.num_report_items + " tableid=" + table_id + " reportstatus=" + scope.report.status + " accessnumber=" + accessnumber + " urlprefix=" + scope.url_prefix + '></div></div></div>'; - - angular.element(document.getElementById('space-for-report-items')).append($compile(html_div_code)(scope)); - if(parseInt(table_id.split("_").pop(-1)) >= scope.max_num) - { - scope.max_num = parseInt(table_id.split("_").pop(-1)); - scope.num_report_items = scope.max_num; - } - - scope.num_report_items++; - - - } - else if(type == "chart") - { - - var content_detail = {}; - var chart_id = id_content; - - var plot_details = undefined; - - //take the one with more elements - if(Object.keys(scope.plots_details).length > num_figures_in_content) - { - plot_details = scope.plots_details[chart_id]; - } - else - { - plot_details = scope.report.content[chart_id]; - } - - - - content_detail["name"] = plot_details.data.output[0]; - //content_detail["description"] = scope.report.content[chart_id].data.plotter; - if(plot_details.data.plotter != undefined) - { - content_detail["selected_plotter"] = plot_details.data.plotter; - } - if(plot_details.selected_template != undefined) - { - content_detail["selected_template"] = plot_details.selected_template; - } - if(plot_details.data.merged != undefined) - { - content_detail["merged"] = plot_details.data.merged; - } - if(plot_details.merged != undefined) - { - content_detail["merged"] = plot_details.merged; - } - - var html_div_code = '<div class="panel panel-default" id="' + chart_id + '"><div class="panel-heading" role="tab"><h4 class="panel-title"><a role="button" data-toggle="collapse" data-parent="#info-heading" href="#collapse-' + chart_id + '" aria-expanded="true" aria-controls="collapse-info"><i class="fa fa-area-chart"> ' + content_detail.name + '</i></a></h4></div><div id="collapse-' + chart_id + '" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="info-heading"><div id="'+chart_id+'" class="panel-body chart {$ report.status $}"></div></div></div>'; - generate_element(scope, "plot", html_div_code); - var element = document.getElementById(chart_id); - var label_element = $(document.createElement('div')); - label_element.addClass('row'); - - var accessnumber = "no_number"; - if(scope.report.number == scope.report_number) - { - accessnumber = "number"; - } - - var prepend_buttons = '<div class="col-sm-12"><div class="action-buttons report-buttons pull-left">'; - var append_buttons = '</div></div>'; - - var a_export = '<div class="btn-group"><button type="button" class="btn btn-primary btn-sm dropdown-toggle item_export ' + scope.report.status + ' ' + accessnumber + '" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-download fa-lg"></i> Export Figure <span class="caret"></span></button><ul class="dropdown-menu"><li id="PNG" buttonexportitem><a>PNG</a></li><li id="JPEG" buttonexportitem><a>JPEG</a></li><li id="PDF" buttonexportitem><a>PDF</a></li></ul></div>'; - if(scope.report.status == "editable" && scope.report.number != scope.report_number) { - var a_element = '<button class="btn btn-danger btn-sm item_delete ' + scope.report.status + ' ' + accessnumber + '" buttondeleteitem><i class="fa fa-times fa-lg"></i> Delete</button>'; - label_element.html(prepend_buttons + a_element + a_export + append_buttons); - } - else { - label_element.html(prepend_buttons + a_export + append_buttons); - } -; - $(element).find('.panel-body').append(label_element); - angular.element(document.getElementById('space-for-report-items')).append($compile(element)(scope)); - //var html_dropdown = "<chartdropdown id='selector_" + chart_id +"'></chartdropdown>"; - //angular.element(document.getElementById(chart_id)).append($compile(html_dropdown)(scope)); - - _retrieve_chart(scope, content_detail, element); - - if(parseInt(chart_id.split("_").pop(-1)) >= scope.max_num) - { - scope.max_num = parseInt(chart_id.split("_").pop(-1)); - scope.num_report_items = scope.max_num; - } - - scope.num_report_items++; - - } - - } - ); - - scope.$on("redrawGraphElement", function(event, id_content, type) - { - var content_detail = {}; - var chart_id = id_content; - content_detail["name"] = scope.report.content[chart_id].data.output[0]; - //content_detail["description"] = scope.report.content[chart_id].data.plotter; - if(scope.report.content[chart_id].data.plotter != undefined) - { - content_detail["selected_plotter"] = scope.report.content[chart_id].data.plotter; - } - if(scope.report.content[chart_id].data.parameter != undefined) - { - content_detail["selected_template"] = scope.report.content[chart_id].data.parameter; - } - if(scope.report.content[chart_id].data.merged != undefined) - { - content_detail["merged"] = scope.report.content[chart_id].data.merged; - } - - var html_div_code = '<div class="panel panel-default" id="' + chart_id + '"><div class="panel-heading" role="tab"><h4 class="panel-title"><a role="button" data-toggle="collapse" data-parent="#info-heading" href="#collapse-' + chart_id + '" aria-expanded="true" aria-controls="collapse-info"><i class="fa fa-area-chart"> ' + content_detail.name + '</i></a></h4></div><div id="collapse-' + chart_id + '" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="info-heading"><div id="'+chart_id+'" class="panel-body chart"></div></div></div>'; - generate_element(scope, "plot", html_div_code); - var element = document.getElementById(chart_id); - var label_element = $(document.createElement('div')); - label_element.addClass('row'); - - var accessnumber = "no_number"; - if(scope.report.number == scope.report_number) - { - accessnumber = "number"; - } - - var prepend_buttons = '<div class="col-sm-12"><div class="action-buttons report-buttons pull-left">'; - var append_buttons = '</div></div>'; - - var a_export = '<div class="btn-group"><button type="button" class="btn btn-primary btn-sm dropdown-toggle item_export ' + scope.report.status + ' ' + accessnumber + '" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-download fa-lg"></i> Export Figure <span class="caret"></span></button><ul class="dropdown-menu"><li id="PNG" buttonexportitem><a>PNG</a></li><li id="JPEG" buttonexportitem><a>JPEG</a></li><li id="PDF" buttonexportitem><a>PDF</a></li></ul></div>'; - if(scope.report.status == "editable" && scope.report.number != scope.report_number) { - var a_element = '<button class="btn btn-danger btn-sm item_delete ' + scope.report.status + ' ' + accessnumber + '" buttondeleteitem><i class="fa fa-times fa-lg"></i> Delete</button>'; - label_element.html(prepend_buttons + a_element + a_export + append_buttons); - } - else { - label_element.html(prepend_buttons + a_export + append_buttons); - } -; - $(element).find('.panel-body').append(label_element); - angular.element(document.getElementById('space-for-report-items')).append($compile(element)(scope)); - //var html_dropdown = "<chartdropdown id='selector_" + chart_id +"'></chartdropdown>"; - //angular.element(document.getElementById(chart_id)).html($compile(html_dropdown)(scope)); - - _retrieve_chart(scope, content_detail, element); - - }); - - //add new report item - element.bind("click", function() - { - var left = $('.add_item').offset().left - $('.add_item').width() + 400; - var top = $('.add_item').offset().top; - smart_selector.display(scope.item_content.content, left, top); - - var allblocks = []; - for(var i = 0; i < scope.report_experiments_blocks_merged_blocks.length; i++) - { - allblocks.push(scope.report_experiments_blocks_merged_blocks[i]); - } - scope.report.common_blocks = arraysInCommon(allblocks); - }); - - //handles first selector selection (table/plot) - if(smart_selector != undefined) - { - smart_selector.onEntrySelected = function(item_selected) - { - - var left = $('.add_item').offset().left - $('.add_item').width() + 400; - var top = $('.add_item').offset().top; - - var next_items_for_selector = prepareContent(scope, smart_selector.entries[smart_selector.selected_entry].identifier); - smart_selector_detail.display(next_items_for_selector, left, top); - }; - } - - //handles next selector detail selection - if(smart_selector_detail != undefined) - { - smart_selector_detail.onEntrySelected = function(item_selected) - { - var left = $('.add_item').offset().left - $('.add_item').width() + 400; - var top = $('.add_item').offset().top; - - - var result_next_items = nextDetailContent(scope, smart_selector.entries[smart_selector.selected_entry].identifier, smart_selector_detail.entries[smart_selector_detail.selected_entry]); - - if(!result_next_items[0]) - { - multiple_selector.display(result_next_items[1], left, top); - } - }; - } - - //handles first creation of table based on options - if(multiple_selector != undefined) - { - multiple_selector.onEntrySelected = function(item_selected, data) - { - for(var i = 0; i < multiple_selector.entries.length; i++) - { - if(multiple_selector.entries[i].name == item_selected) - { - multiple_selector.entries[i].selected = true; - } - } - }; - - multiple_selector.onEntryDeselected = function(item_selected, data) - { - for(var i = 0; i < multiple_selector.entries.length; i++) - { - if(multiple_selector.entries[i].name == item_selected) - { - multiple_selector.entries[i].selected = false; - } - } - }; - - multiple_selector.onClose = function() - { - //check if at least one item is selected - var checkOneSelected = false; - angular.forEach(multiple_selector.entries, function(value, key) - { - if(value.selected) - checkOneSelected = true - }); - - if(checkOneSelected) - { - var table_id = 'table' + '_' + scope.num_report_items; - - scope.tables_details[table_id] = multiple_selector.entries; - - var html_div_code = '<div class="panel panel-default" id="' + table_id + '"><div class="panel-heading" role="tab"><h4 class="panel-title"><a role="button" data-toggle="collapse" data-parent="#info-heading" href="#collapse-' + table_id + '" aria-expanded="true" aria-controls="collapse-info"><i class="fa fa-table"> Table</i></a></h4></div><div id="collapse-' + table_id + '" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="info-heading"><div class="panel-body" table-dynamic monster=' + scope.num_report_items + " tableid=" + table_id + " reportstatus=" + scope.report.status + " urlprefix=" + scope.url_prefix + '></div></div></div>'; - - angular.element(document.getElementById('space-for-report-items')).append($compile(html_div_code)(scope)); - - scope.report.orderedcontent.push(table_id); - scope.num_report_items++; - } - } - } - - //handles single table updates via settings option - if(multiple_selector_updater != undefined) - { - multiple_selector_updater.onEntrySelected = function(item_selected, data) - { - for(var i = 0; i < multiple_selector_updater.entries.length; i++) - { - if(multiple_selector_updater.entries[i].name == item_selected) - { - multiple_selector_updater.entries[i].selected = true; - } - } - }; - - multiple_selector_updater.onEntryDeselected = function(item_selected, data) - { - for(var i = 0; i < multiple_selector_updater.entries.length; i++) - { - if(multiple_selector_updater.entries[i].name == item_selected) - { - multiple_selector_updater.entries[i].selected = false; - } - } - }; - - multiple_selector_updater.onClose = function() - { - - //check if at least one item is selected - var checkOneSelected = false; - angular.forEach(multiple_selector_updater.entries, function(value, key) - { - if(value.selected) - checkOneSelected = true - }); - - if(checkOneSelected) - { - var element = document.getElementById(multiple_selector_updater.current_table); - - $(element).attr('id', null); - $compile(element)(scope); - $(element).attr('id', multiple_selector_updater.current_table); - } - } - - }; - - //Prepare the content for selector initialization - function prepareContent(scope, content_identifier) - { - var next_content_items = []; - - switch(content_identifier) - { - case 'table': - next_content_items = scope.table_item_content.content; - break; - case 'plot': - angular.forEach(scope.report_experiments, function(value, key) - { - angular.forEach(value.declaration.analyzers, function(value_analyzer, key_analyzer) - { - if(value_analyzer.algorithm == scope.report.analyzer) - { - scope.report_experiments[key].analyzer_block = key_analyzer; - } - }); - - var plottable_result = []; - angular.forEach(value.results[scope.report_experiments[key].analyzer_block], function(value_result_item, key_result_item) - { - if(value_result_item.type.startsWith("plot/")) - { - plottable_result.push(key_result_item); - } - - }); - - scope.report_experiments[key].plottable_blocks = plottable_result; - - }); - - //Results block are the same for all experiments from same analyzer. - //So just grab information from one of them for smart_selector items - var single_experiment = scope.report.experiments[0]; - - //create smart_selector items - angular.forEach(scope.report_experiments[single_experiment].plottable_blocks, function(value_plottable, key_plottable) - { - var plot_type = scope.report_experiments[single_experiment].results[scope.report_experiments[single_experiment].analyzer_block][value_plottable].type; - var item_dict = {}; - item_dict["identifier"] = value_plottable; - item_dict["name"] = value_plottable; - item_dict["description"] = plot_type; - - next_content_items.push(item_dict); - }); - break; - } - - return next_content_items; - } - - //Get the detailed content for next selector - function nextDetailContent(scope, content_identifier, sub_content) - { - var is_end = false; - var next_items = []; - var return_contents = []; - switch(content_identifier) - { - case 'table': - is_end = false; - angular.forEach(scope.report_experiments, function(value, key) - { - angular.forEach(value.declaration.analyzers, function(value_analyzer, key_analyzer) - { - if(value_analyzer.algorithm == scope.report.analyzer) - { - scope.report_experiments[key].analyzer_block = key_analyzer; - } - }); - - var table_result = []; - angular.forEach(value.results[scope.report_experiments[key].analyzer_block], function(value_result_item, key_result_item) - { - if(!(value_result_item.type.startsWith("plot/"))) - { - table_result.push(key_result_item); - } - - }); - - scope.report_experiments[key].table_result_blocks = table_result; - - }); - - //Results block are the same for all experiments from same analyzer. - //So just grab information from one of them for smart_selector items - var single_experiment = scope.report.experiments[0]; - - //create smart_selector items - angular.forEach(scope.report_experiments[single_experiment].table_result_blocks, function(value_table_result, key_table_result) - { - - var table_item_data = scope.report_experiments[single_experiment].results[scope.report_experiments[single_experiment].analyzer_block][value_table_result]; - var item_dict = {}; - item_dict["identifier"] = value_table_result; - item_dict["name"] = value_table_result; - item_dict["description"] = table_item_data.type; - if(table_item_data.primary) - item_dict["selected"] = true; - else - item_dict["selected"] = false; - item_dict["data"] = table_item_data; - - next_items.push(item_dict); - }); - - //adding execution time entry information for common blocks - angular.forEach(scope.report.common_blocks, function(value_table_result, key_table_result) - { - var item_dict = {}; - item_dict["identifier"] = "execution_time." + value_table_result; - item_dict["name"] = "execution_time." + value_table_result; - item_dict["selected"] = false; - next_items.push(item_dict); - - }); - - //adding total execution time entry information for experiment - var item_dict = {}; - item_dict["identifier"] = "experiment.execution_time"; - item_dict["name"] = "experiment.execution_time"; - item_dict["selected"] = false; - next_items.push(item_dict); - - //adding globals algorithms parameters - angular.forEach(scope.report_algorithm_parameters, function(value_algorithm_parameters, key_algorithm_parameters) - { - for(var i = 0; i < Object.keys(value_algorithm_parameters).length; i++) - { - var item_dict = {}; - item_dict["identifier"] = "algorithm_parameter." + key_algorithm_parameters + "__" +Object.keys(value_algorithm_parameters)[i]; - item_dict["name"] = "algorithm_parameter." + key_algorithm_parameters + "__" +Object.keys(value_algorithm_parameters)[i]; - item_dict["selected"] = false; - next_items.push(item_dict); - } - - }); - - break; - case 'plot': - is_end = true; - var chart_id = 'chart_' + sub_content.identifier + '_graph' + '_' + scope.num_report_items; - var html_div_code = '<div class="panel panel-default" id="' + chart_id + '"><div class="panel-heading" role="tab"><h4 class="panel-title"><a role="button" data-toggle="collapse" data-parent="#info-heading" href="#collapse-' + chart_id + '" aria-expanded="true" aria-controls="collapse-info"><i class="fa fa-area-chart"> ' + sub_content.name + '</i></a></h4></div><div id="collapse-' + chart_id + '" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="info-heading"><div id="'+chart_id+'" class="panel-body chart {$ report.status $}"></div></div></div>'; - generate_element(scope, content_identifier, html_div_code); - var element = document.getElementById(chart_id); - var label_element = $(document.createElement('div')); - label_element.addClass('row'); - - var accessnumber = "no_number"; - if(scope.report.number == scope.report_number) - { - accessnumber = "number"; - } - - var prepend_buttons = '<div class="col-sm-12"><div class="action-buttons report-buttons pull-left">'; - var append_buttons = '</div></div>'; - - var a_export = '<div class="btn-group"><button type="button" class="btn btn-primary btn-sm dropdown-toggle item_export ' + scope.report.status + ' ' + accessnumber + '" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-download fa-lg"></i> Export Figure <span class="caret"></span></button><ul class="dropdown-menu"><li id="PNG" buttonexportitem><a>PNG</a></li><li id="JPEG" buttonexportitem><a>JPEG</a></li><li id="PDF" buttonexportitem><a>PDF</a></li></ul></div>'; - if(scope.report.status == "editable" && scope.report.number != scope.report_number) { - var a_element = '<button class="btn btn-danger btn-sm item_delete ' + scope.report.status + ' ' + accessnumber + '" buttondeleteitem><i class="fa fa-times fa-lg"></i> Delete</button>'; - label_element.html(prepend_buttons + a_element + a_export + append_buttons); - } - else { - label_element.html(prepend_buttons + a_export + append_buttons); - } - $(element).find('.panel-body').append(label_element); - - angular.element(document.getElementById('space-for-report-items')).append($compile(element)(scope)); - - _retrieve_chart(scope, sub_content, element); - scope.report.orderedcontent.push(chart_id); - break; - } - - return_contents = [is_end, next_items]; - - return return_contents; - - } - - //Generate and compile DOM element - function generate_element(scope, content_identifier, html_div_code) - { - switch(content_identifier) - { - case 'table': - angular.element(document.getElementById('space-for-report-items')).append($compile(html_div_code)(scope)); - scope.num_report_items++; - break; - case 'plot': - angular.element(document.getElementById('space-for-report-items')).append($compile(html_div_code)(scope)); - scope.num_report_items++; - break; - } - - } - - //Retrieve chart from api and display on proper item - function _get_chart(scope, sub_content, content_type, chart_id) - { - var required_plotter = []; - var set_plotter = '' - if(sub_content.selected_plotter != undefined) - { - //required_plotter.push(sub_content.selected_plotter); - set_plotter = sub_content.selected_plotter; - var requested_dataformat = ''; - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].name == sub_content.selected_plotter) - { - requested_dataformat = scope.report.plotters[i].dataformat; - break; - } - } - - //Get default plotter for required dataformat - for (var i = 0; i < scope.report.defaultplotters.length; i++) - { - if(scope.report.defaultplotters[i].dataformat == requested_dataformat) - { - sub_content.defaultplotter = scope.report.defaultplotters[i]; - break; - } - } - - required_plotter.push(sub_content.defaultplotter.plotter); - - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].dataformat == requested_dataformat && scope.report.plotters[i].name != sub_content.defaultplotter.plotter) - { - required_plotter.push(scope.report.plotters[i].name); - } - } - } - else - { - //Get default plotter for required dataformat - for (var i = 0; i < scope.report.defaultplotters.length; i++) - { - if(scope.report.defaultplotters[i].dataformat == sub_content.description) - { - sub_content.defaultplotter = scope.report.defaultplotters[i]; - break; - } - } - - required_plotter.push(sub_content.defaultplotter.plotter); - set_plotter = sub_content.defaultplotter.plotter; - - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].dataformat == sub_content.description && scope.report.plotters[i].name != sub_content.defaultplotter.plotter) - { - required_plotter.push(scope.report.plotters[i].name); - } - } - } - - var plotterparameter = []; - //get plotterparameters valid for requested plotter - var required_plotter_id = undefined; - for(var i = 0; i < scope.report.plotters.length; i++) - { - if(required_plotter[0] == scope.report.plotters[i].name) - { - required_plotter_id = scope.report.plotters[i].id - } - } - - //Get other plotterparameter - for (var i = 0; i < scope.report.plotterparameter.length; i++) - { - if(required_plotter_id == undefined) - { - plotterparameter.push(scope.report.plotterparameter[i].name); - } - else - { - if(scope.report.plotterparameter[i].plotter == required_plotter_id) - { - plotterparameter.push(scope.report.plotterparameter[i].name); - } - } - } - - var chart_name = sub_content.name; - - var legend_experiments = ''; - var alias_experiments = []; - for(var i = 0; i < scope.report.experiments.length; i++) - { - if(i == 0) - legend_experiments = scope.report_experiments_alias[scope.report.experiments[i]]; - else - legend_experiments = legend_experiments+ "&" +scope.report_experiments_alias[scope.report.experiments[i]]; - - alias_experiments.push(scope.report_experiments_alias[scope.report.experiments[i]]); - } - - var request_data = { - experiment: alias_experiments, - analyzer: [scope.report.analyzer], - output: [chart_name], - plotter: set_plotter, - legend: legend_experiments, - report_number: scope.report.number, - //height: 300, - //width: 400, - }; - - base_url = scope.report.url_prefix; - - var plot_detail = {}; - plot_detail["required_plotter"] = required_plotter; - plot_detail["data"] = { - analyzer: [scope.report.analyzer], - output: [chart_name], - plotter: set_plotter, - }; - - if(sub_content.selected_template != undefined)// && sub_content.selected_template != "Default") - { - plot_detail["selected_template"] = sub_content.selected_template; - request_data.parameter = plot_detail["selected_template"]; - } - else - { - plot_detail["selected_template"] = sub_content.defaultplotter.parameter; - request_data.parameter = plot_detail["selected_template"]; - } - - if(sub_content.merged != undefined)// && sub_content.selected_template != "Default") - { - plot_detail["merged"] = sub_content.merged; - request_data.merged = plot_detail["merged"]; - } - else - { - plot_detail["merged"] = true; - request_data.merged = plot_detail["merged"]; - } - - beat.experiments.utils.getPlotData(base_url, - request_data, required_plotter, plotterparameter, content_type, - function(r_image_data, selected_content_type) { - download(r_image_data, chart_id + "." + content_type.toLowerCase(), selected_content_type); - } - ); - } - - - //Retrieve chart from api and display on proper item - function _retrieve_chart(scope, sub_content, container) - { - var required_plotter = []; - var set_plotter = '' - if(sub_content.selected_plotter != undefined) - { - //required_plotter.push(sub_content.selected_plotter); - set_plotter = sub_content.selected_plotter; - var requested_dataformat = ''; - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].name == sub_content.selected_plotter) - { - requested_dataformat = scope.report.plotters[i].dataformat; - break; - } - } - - //Get default plotter for required dataformat - for (var i = 0; i < scope.report.defaultplotters.length; i++) - { - if(scope.report.defaultplotters[i].dataformat == requested_dataformat) - { - sub_content.defaultplotter = scope.report.defaultplotters[i]; - break; - } - } - - required_plotter.push(sub_content.defaultplotter.plotter); - - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].dataformat == requested_dataformat && scope.report.plotters[i].name != sub_content.defaultplotter.plotter) - { - required_plotter.push(scope.report.plotters[i].name); - } - } - } - else - { - //Get default plotter for required dataformat - for (var i = 0; i < scope.report.defaultplotters.length; i++) - { - if(scope.report.defaultplotters[i].dataformat == sub_content.description) - { - sub_content.defaultplotter = scope.report.defaultplotters[i]; - break; - } - } - - required_plotter.push(sub_content.defaultplotter.plotter); - set_plotter = sub_content.defaultplotter.plotter; - - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].dataformat == sub_content.description && scope.report.plotters[i].name != sub_content.defaultplotter.plotter) - { - required_plotter.push(scope.report.plotters[i].name); - } - } - } - - var plotterparameter = []; - //get plotterparameters valid for requested plotter - var required_plotter_id = undefined; - for(var i = 0; i < scope.report.plotters.length; i++) - { - if(required_plotter[0] == scope.report.plotters[i].name) - { - required_plotter_id = scope.report.plotters[i].id - } - } - - //Get other plotterparameter - for (var i = 0; i < scope.report.plotterparameter.length; i++) - { - if(required_plotter_id == undefined) - { - plotterparameter.push(scope.report.plotterparameter[i].name); - } - else - { - if(scope.report.plotterparameter[i].plotter == required_plotter_id) - { - plotterparameter.push(scope.report.plotterparameter[i].name); - } - } - } - - var chart_name = sub_content.name; - - var legend_experiments = ''; - var alias_experiments = []; - - for(var i = 0; i < scope.report.experiments.length; i++) - { - if(i == 0) - legend_experiments = scope.report_experiments_alias[scope.report.experiments[i]]; - else - legend_experiments = legend_experiments+ "&" +scope.report_experiments_alias[scope.report.experiments[i]]; - - if(Object.keys(scope.$parent.report_experiments_alias_from_content).length === 0 || scope.report_experiments_alias_from_content[scope.report.experiments[i]] == undefined ) - alias_experiments.push(scope.report_experiments_alias[scope.report.experiments[i]]); - else - alias_experiments.push(scope.report_experiments_alias_from_content[scope.report.experiments[i]]); - } - - var request_data = { - experiment: alias_experiments, - analyzer: [scope.report.analyzer], - output: [chart_name], - plotter: set_plotter, - legend: legend_experiments, - report_number: scope.report.number, - //height: 300, - //width: 400, - }; - - base_url = scope.report.url_prefix; - //scope.plots_details[container.id]; - - var plot_detail = {}; - plot_detail["required_plotter"] = required_plotter; - plot_detail["data"] = { - analyzer: [scope.report.analyzer], - output: [chart_name], - plotter: set_plotter, - }; - - - if(sub_content.selected_template != undefined)// && sub_content.selected_template != "Default") - { - plot_detail["selected_template"] = sub_content.selected_template; - request_data.parameter = plot_detail["selected_template"]; - } - else - { - plot_detail["selected_template"] = sub_content.defaultplotter.parameter; - request_data.parameter = plot_detail["selected_template"]; - } - - if(sub_content.merged != undefined)// && sub_content.selected_template != "Default") - { - plot_detail["merged"] = sub_content.merged; - request_data.merged = plot_detail["merged"]; - } - else - { - plot_detail["merged"] = true; - request_data.merged = plot_detail["merged"]; - } - - scope.plots_details[container.id]= plot_detail; - - if(scope.report.status == "editable" && scope.report.number != scope.report_number) - { - beat.experiments.utils.displayPlot(base_url, $(container).find('.panel-body')[0], - request_data, required_plotter, plotterparameter, false, - - function(r_plotter, r_plotterparameter, r_merged) { - scope.plots_details[container.id]["data"]["plotter"] = r_plotter; - scope.plots_details[container.id]["selected_template"] = r_plotterparameter; - scope.plots_details[container.id]["merged"] = r_merged; - } - ); - } - else - { - beat.experiments.utils.displayPlot(base_url, $(container).find('.panel-body')[0], - request_data, [], [], false, - - function(r_plotter, r_plotterparameter, r_merged) { - } - ); - } - } - - function arraysInCommon(arrays) - { - var i, common, - L= arrays.length, min= Infinity; - while(L){ - if(arrays[--L].length<min){ - min= arrays[L].length; - i= L; - } - } - common= arrays.splice(i, 1)[0]; - return common.filter(function(itm, indx){ - if(common.indexOf(itm)== indx){ - return arrays.every(function(arr){ - return arr.indexOf(itm)!= -1; - }); - } - }); - } - - } -}); - - -//Dynamic tables items -app.directive('item', function ($compile) { - function createTDElement(column) { - var table = angular.element('<table><tr><td class="tableitemspace" thecolumn="' + column + '"></td></tr></table>'); - return table.find('td'); - } - - function render(element, scope) { - var column, html, i; - var columns = scope.$parent.tables_details[scope.$parent.dattrs.tableid]; - - for (i = 0; i < columns.length ; i++) { - if(i == 0 && columns.length > 0) - { - html = $compile(createTDElement("experiment"))(scope); - - } - column = columns[i]; - if (column.selected) { - - html = $compile(createTDElement(column.name))(scope); - element.append(html); - } - } - - - } - - return { - // restrict: 'A', - scope: { - exp_name: "=", - item: "=", - columns: "=" - }, - compile: function () { - return function (scope, element) { - render(element, scope); - } - - } - }; - -}); - -//Directive used to set proper item value in selected column -app.directive("thecolumn",['$compile', function ($compile) { - - return { - scope: { - thecolumn: "@" - }, - restrict: 'A', - template: '{{itemValue}}', - controller: ['$scope', function ($scope) { - - var the_parent = $scope.$parent.$parent.$parent.$parent; - var report_experiments = $scope.$parent.$parent.$parent.$parent.report_experiments; - var report_experiments_alias = $scope.$parent.$parent.$parent.$parent.report_experiments_alias; - var floating_point_precision = $scope.$parent.$parent.$parent.$parent.floating_point_precision; - var report = $scope.$parent.$parent.$parent.report; - var experiment_name = $scope.$parent.item; - - if(jQuery.isEmptyObject(report_experiments) || (report_experiments[experiment_name] == undefined)) - { - return - } - - var analyzer_block = report_experiments[experiment_name].analyzer_block; - var report_algorithm_parameters_experiment = $scope.$parent.$parent.$parent.$parent.report_algorithm_parameters_experiment; - - if($scope.thecolumn != "experiment") - { - if(analyzer_block == undefined) - { - angular.forEach(report_experiments, function(value, key) - { - angular.forEach(value.declaration.analyzers, function(value_analyzer, key_analyzer) - { - if(value_analyzer.algorithm == report.analyzer && (key_analyzer in report_experiments[experiment_name].declaration.analyzers)) - { - report_experiments[experiment_name].analyzer_block = key_analyzer; - analyzer_block = report_experiments[experiment_name].analyzer_block; - } - }); - }); - } - - if($scope.thecolumn.indexOf("execution_time.") == 0) - { - //execution time information - var block_name = $scope.thecolumn.split("execution_time.")[1].split("[s]")[0]; - if(Object.keys(report_experiments[experiment_name].execution_info).length === 0) - { - $scope.itemValue = "-"; - } - else - { - if(report_experiments[experiment_name].execution_info[block_name] == undefined) - { - $scope.itemValue = "-"; - } - else - { - $scope.itemValue = (report_experiments[experiment_name].execution_info[block_name].linear_execution_time).toFixed(floating_point_precision.selected);; - } - } - } - else if($scope.thecolumn.indexOf("experiment.") == 0) - { - //total execution time information - var block_name = $scope.thecolumn.split("execution_time.")[1]; - var total_time = 0; - if(Object.keys(report_experiments[experiment_name].execution_info).length === 0) - { - total_time = "-"; - } - else - { - angular.forEach(report_experiments[experiment_name].execution_info, function(value, key) - { - total_time += value.linear_execution_time; - }); - } - - $scope.itemValue = total_time.toFixed(floating_point_precision.selected); - } - else if($scope.thecolumn.indexOf("algorithm_parameter.") == 0) - { - //total execution time information - var block_name = $scope.thecolumn.split("algorithm_parameter.")[1]; - var algorithm_name = block_name.split("__")[0]; - var parameter_name = block_name.split("__")[1]; - if(report_algorithm_parameters_experiment[experiment_name][algorithm_name] != undefined) - { - var value = ""; - if(report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name] != undefined) - { - for(var i = 0; i < report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name].length; i++) - { - if(i > 0 && i < report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name].length -1 ) - { - value += ","; - } - value = report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name][i]; - } - } - else if(report["all_experiments"][experiment_name]["declaration"]["globals"][algorithm_name][parameter_name] != undefined) - { - //get globals value - value = report["all_experiments"][experiment_name]["declaration"]["globals"][algorithm_name][parameter_name]; - } - else - { - //nothing is defined - value = "-"; - } - - $scope.itemValue = value; - } - else - { - $scope.itemValue = "-"; - } - } - else - { - //results information - $scope.itemValue = (report_experiments[experiment_name].results[analyzer_block][$scope.thecolumn].value).toFixed(floating_point_precision.selected); - } - } - else - { - var experiment_alias = report_experiments_alias[experiment_name]; - $scope.itemValue = experiment_alias; - } - }]//, - //link: function(scope, element, attrs) - //{ - // console.log(scope); - // var report_experiments_alias = scope.$parent.$parent.$parent.$parent.report_experiments_alias; - // var experiment_name = scope.$parent.item; - // var experiment_alias = report_experiments_alias[experiment_name]; - // scope.$watch(scope.$parent.$parent.$parent.$parent.report_experiments_alias,function(newValue, oldValue) - // { - // console.log("changed"); - // console.log(report_experiments_alias); - // console.log(oldValue); - // console.log(newValue); - // }); - // - //} - } -}]); - -//Directive used to generate the dynamic table loading partials -app.directive("tableDynamic", function(){ - - return { - restrict: 'A', - scope: true, - replace: true, - Â Â Â Â //templateUrl: "/reports/partials/reportTable/", - Â Â Â Â templateUrl: function(scope, elem, attrs) - { - var prefix = elem['urlprefix']; - Â Â Â Â var the_url = prefix + "/reports/partials/reportTable/"; - return the_url; - }, - link: function(scope, elem, attrs) - { - var prepend_item = {}; - prepend_item["name"] = "experiment"; - prepend_item["selected"] = true; - - scope.tables_details[attrs.tableid].unshift(prepend_item); - - angular.forEach(scope.tables_details, function(table_value, table_key) - { - for(var i = 0; i < table_value.length; i++) - { - if(table_value[i].name.indexOf("execution_time") > -1 && - table_value[i].name.indexOf("[s]") == -1) - { - table_value[i].name = table_value[i].name + "[s]"; - } - }; - }); - - scope.dattrs = attrs; - } - }; -}); - -//Directive used to generate the dynamic table loading partials -app.directive("myreportinfo", function(){ - - return { - restrict: 'E', - scope: true, - replace: true, - Â Â Â Â //templateUrl: "/reports/partials/reportTable/", - Â Â Â Â templateUrl: function(scope, elem, attrs) - { - var prefix = elem['urlprefix']; - Â Â Â Â var the_url = prefix + "/reports/partials/reportInfo/"; - return the_url; - }, - link: function(scope, elem, attrs) - { - }, - controller: ['$scope', function ($scope) { - }], - }; -}); - -//Directive used to handle table settings click -app.directive("buttonsettings", function() -{ - return { - link:function(scope, element, attrs) - { - element.bind("click", function() - { - var name_to_remove = "experiment"; - var tables_details = scope.$parent.tables_details[scope.dattrs.tableid]; - for(var i = 0; i < tables_details.length; i++) - { - if(tables_details[i].name == "experiment") - { - tables_details.splice(i,1); - break; - } - } - multiple_selector_updater.current_table = scope.dattrs.tableid; - multiple_selector_updater.display(tables_details); - }); - } - }; -}); - -//Directive used to handle table settings click -app.directive("sortdata", function($compile) -{ - return { - link:function(scope, element, attrs) - { - element.bind("click", function() - { - var the_parent = scope.$parent.$parent.$parent.$parent; - var analyzer_block = undefined; - var report_experiments = the_parent.report_experiments; - var report = the_parent.report; - var table_id = attrs.sorttblid; //get table id - the_parent.sorted_tables.push(table_id); - - if(the_parent.sorted_experiments_keys_reverse[table_id] == undefined) - { - the_parent.sorted_experiments_keys_reverse[table_id] = true; - } - else - { - the_parent.sorted_experiments_keys_reverse[table_id] = !the_parent.sorted_experiments_keys_reverse[table_id]; - } - - the_parent.sorted_experiments_keys_tables_sortkey[table_id] = attrs.sortth; //set the sortKey to the param passed - - if(the_parent.sorted_experiments_keys_tables_sortkey[table_id] != "experiment") - { - var local_sort_list = []; - - if(analyzer_block == undefined) - { - angular.forEach(report_experiments, function(value, key) - { - angular.forEach(value.declaration.analyzers, function(value_analyzer, key_analyzer) - { - if(value_analyzer.algorithm == report.analyzer && (key_analyzer in report_experiments[key].declaration.analyzers)) - { - report_experiments[key].analyzer_block = key_analyzer; - analyzer_block = report_experiments[key].analyzer_block; - } - }); - }); - } - - if(the_parent.sorted_experiments_keys_tables_sortkey[table_id].indexOf("execution_time.") == 0) - { - //execution time information - var block_name = the_parent.sorted_experiments_keys_tables_sortkey[table_id].split("execution_time.")[1].split("[s]")[0]; - angular.forEach(report_experiments, function(value, key) - { - var local_sort_dict = {}; - local_sort_dict["experiment"] = key; - var itemValue = ""; - - if(Object.keys(report_experiments[key].execution_info).length === 0) - { - itemValue = "-"; - } - else - { - if(report_experiments[key].execution_info[block_name] == undefined) - { - itemValue = "-"; - } - else - { - itemValue = (report_experiments[key].execution_info[block_name].linear_execution_time); - } - } - - local_sort_dict["value"] = itemValue; - local_sort_list.push(local_sort_dict); - }); - } - else if(the_parent.sorted_experiments_keys_tables_sortkey[table_id].indexOf("experiment.") == 0) - { - //total execution time information - var block_name = the_parent.sorted_experiments_keys_tables_sortkey[table_id].split("execution_time.")[1]; - - angular.forEach(report_experiments, function(value, key) - { - var local_sort_dict = {}; - local_sort_dict["experiment"] = key; - var itemValue = ""; - var total_time = 0; - - if(Object.keys(report_experiments[key].execution_info).length === 0) - { - total_time = "-"; - } - else - { - angular.forEach(report_experiments[key].execution_info, function(value, key) - { - total_time += value.linear_execution_time; - }); - } - - itemValue = total_time; - - local_sort_dict["value"] = itemValue; - local_sort_list.push(local_sort_dict); - }); - } - else if(the_parent.sorted_experiments_keys_tables_sortkey[table_id].indexOf("algorithm_parameter.") == 0) - { - //total execution time information - var block_name = the_parent.sorted_experiments_keys_tables_sortkey[table_id].split("algorithm_parameter.")[1]; - var algorithm_name = block_name.split("__")[0]; - var parameter_name = block_name.split("__")[1]; - - angular.forEach(report_experiments, function(value, key) - { - - var local_sort_dict = {}; - local_sort_dict["experiment"] = key; - var itemValue = ""; - - if(the_parent.report_algorithm_parameters_experiment[key][algorithm_name] != undefined) - { - var value = ""; - if(the_parent.report_algorithm_parameters_experiment[key][algorithm_name][parameter_name] != undefined) - { - for(var i = 0; i < the_parent.report_algorithm_parameters_experiment[key][algorithm_name][parameter_name].length; i++) - { - if(i > 0 && i < the_parent.report_algorithm_parameters_experiment[key][algorithm_name][parameter_name].length -1 ) - { - value += ","; - } - value = the_parent.report_algorithm_parameters_experiment[key][algorithm_name][parameter_name][i]; - } - } - else if(the_parent.report["all_experiments"][key]["declaration"]["globals"][algorithm_name][parameter_name] != undefined) - { - //get globals value - value = the_parent.report["all_experiments"][key]["declaration"]["globals"][algorithm_name][parameter_name]; - } - else - { - //nothing is defined - value = "-"; - } - - itemValue = value; - } - else - { - itemValue = "-"; - } - - local_sort_dict["value"] = itemValue; - local_sort_list.push(local_sort_dict); - }); - } - else - { - //results information - angular.forEach(report_experiments, function(value, key) - { - var local_sort_dict = {}; - local_sort_dict["experiment"] = key; - local_sort_dict["value"] = report_experiments[key].results[analyzer_block][the_parent.sorted_experiments_keys_tables_sortkey[table_id]].value; - - local_sort_list.push(local_sort_dict); - - }); - - } - - if(the_parent.sorted_experiments_keys_reverse[table_id]) - { - local_sort_list.sort(function(a, b) - { - return a.value - b.value; - }); - } - else - { - local_sort_list.sort(function(a, b) - { - return b.value - a.value; - }); - } - - the_parent.sorted_experiments_keys_tables[table_id] = []; - the_parent.sorted_experiments_alias_keys_tables[table_id] = []; - for(var i = 0; i < local_sort_list.length; i++) - { - the_parent.sorted_experiments_keys_tables[table_id].push(local_sort_list[i]["experiment"]); - if(the_parent.report.status == "editable") - { - the_parent.sorted_experiments_alias_keys_tables[table_id].push(the_parent.report_experiments_alias[local_sort_list[i]["experiment"]]); - } - } - - } - else - { - var experiments_aliases = []; - for(var i = 0; i < the_parent.report.experiments.length; i++) - { - var experiment_name = the_parent.report.experiments[i]; - var experiment_alias = the_parent.report_experiments_alias[experiment_name]; - experiments_aliases.push(experiment_alias); - } - - if(the_parent.sorted_experiments_keys_reverse[table_id]) - { - experiments_aliases.sort(function(a, b) - { - return a.value - b.value; - }); - } - else - { - experiments_aliases.reverse(function(a, b) - { - return b.value - a.value; - }); - } - - the_parent.sorted_experiments_keys_tables[table_id] = []; - the_parent.sorted_experiments_alias_keys_tables[table_id] = []; - for(var i = 0; i < experiments_aliases.length; i++) - { - for(var the_experiment_key in the_parent.report_experiments_alias) - { - if(the_parent.report_experiments_alias[the_experiment_key] == experiments_aliases[i]) - { - the_parent.sorted_experiments_keys_tables[table_id].push(the_experiment_key); - if(the_parent.report.status == "editable") - { - the_parent.sorted_experiments_alias_keys_tables[table_id].push(the_parent.report_experiments_alias[the_experiment_key]); - } - } - - } - } - } - - var name_to_remove = "experiment"; - var tables_details = the_parent.tables_details[table_id]; - for(var i = 0; i < tables_details.length; i++) - { - if(tables_details[i].name == "experiment") - { - tables_details.splice(i,1); - break; - } - } - - var element = document.getElementById(table_id); - - $(element).attr('id', null); - $compile(element)(scope.$parent.$parent); - $(element).attr('id', table_id); - }); - } - }; -}); - -//Directive used to handle table settings click -app.directive("tableprecision", function($compile) -{ - return { - link:function(scope, element, attrs) - { - element.bind("change", function() - { - fixFloatingPoint(scope.$parent.$parent.floating_point_precision.selected); - }); - - function fixFloatingPoint(selected_precision) - { - if(!scope.$$phase) - { - //$digest or $apply - var parent_scope = scope.$parent; - var table_details = parent_scope.tables_details; - - for(var i = 0; i < parent_scope.report.orderedcontent.length; i++) - { - var element = document.getElementById(parent_scope.report.orderedcontent[i]); - - $(element).remove(); - - var id_content = parent_scope.report.orderedcontent[i]; - var type = id_content.split("_")[0]; - parent_scope.$parent.$parent.$broadcast("addSavedElement", id_content, type); - } - } - } - } - }; -}); - -//Directive used to handle table settings click -app.directive("buttondeleteitem", function() -{ - return { - link:function(scope, element, attrs) - { - element.bind("click", function() - { - var elementToRemove = element.context.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement; - beat.ui.report.remove_item('report_remove_item', elementToRemove, scope); - }); - } - }; -}); - -//Directive used to export graph/tables on click -app.directive("buttonexportitem", function() -{ - return { - link:function(scope, element, attrs) - { - element.bind("click", function() - { - var the_element = element.context.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement; - if(the_element.id.indexOf("chart_") == 0) - { - var content_detail = {}; - var chart_id = the_element.id; - - content_detail["name"] = scope.plots_details[chart_id].data.output[0]; - - if(scope.plots_details[chart_id].data.plotter != undefined) - { - content_detail["selected_plotter"] = scope.plots_details[chart_id].data.plotter; - } - if(scope.plots_details[chart_id].data.parameter != undefined) - { - content_detail["selected_template"] = scope.plots_details[chart_id].data.parameter; - } - else if(scope.plots_details[chart_id]["selected_template"] != undefined) - { - content_detail["selected_template"] = scope.plots_details[chart_id]["selected_template"]; - } - if(scope.plots_details[chart_id].data.merged != undefined) - { - content_detail["merged"] = scope.plots_details[chart_id].data.merged; - } - - var accessnumber = "no_number"; - if(scope.report.number == scope.report_number) - { - accessnumber = "number"; - } - _get_chart(scope, content_detail, element[0].id, chart_id); - } - else if(the_element.id.indexOf("table_") == 0) - { - var separator = ","; - var csv_text = export_table_as_csv(scope, the_element.id, separator); - var output_filename = the_element.id + ".csv"; - download(csv_text, output_filename, "text/plain"); - } - }); - } - }; - - function export_table_as_csv(scope, table_id, separator) - { - var report_experiments = scope.$parent.report_experiments; - var report_experiments_alias = scope.$parent.report_experiments_alias; - var floating_point_precision = scope.$parent.floating_point_precision; - var report = scope.report; - var final_table = []; - var table_headers = []; - angular.forEach(scope.tables_details[table_id], function(table_value, table_key) - { - if(table_value.selected) - { - table_headers.push(table_value.name); - } - }); - - final_table.push(table_headers); - - angular.forEach(report_experiments, function(experiment_value, experiment_key) - { - var table_items = []; - - angular.forEach(table_headers, function(table_value, table_key) - { - var experiment_name = experiment_key; - var analyzer_block = report_experiments[experiment_name].analyzer_block; - var report_algorithm_parameters_experiment = scope.$parent.report_algorithm_parameters_experiment; - - if(table_value != "experiment") - { - if(analyzer_block == undefined) - { - angular.forEach(report_experiments, function(value, key) - { - angular.forEach(value.declaration.analyzers, function(value_analyzer, key_analyzer) - { - if(value_analyzer.algorithm == report.analyzer && (key_analyzer in report_experiments[experiment_name].declaration.analyzers)) - { - report_experiments[experiment_name].analyzer_block = key_analyzer; - analyzer_block = report_experiments[experiment_name].analyzer_block; - } - }); - }); - } - - if(table_value.indexOf("execution_time.") == 0) - { - //execution time information - var block_name = table_value.split("execution_time.")[1].split("[s]")[0]; - if(Object.keys(report_experiments[experiment_name].execution_info).length === 0) - { - table_items.push("-"); - } - else - { - if(report_experiments[experiment_name].execution_info[block_name] == undefined) - { - table_items.push("-"); - } - else - { - table_items.push((report_experiments[experiment_name].execution_info[block_name].linear_execution_time).toFixed(floating_point_precision.selected)); - } - } - } - else if(table_value.indexOf("experiment.") == 0) - { - //total execution time information - var block_name = table_value.split("execution_time.")[1]; - var total_time = 0; - if(Object.keys(report_experiments[experiment_name].execution_info).length === 0) - { - total_time = "-"; - } - else - { - angular.forEach(report_experiments[experiment_name].execution_info, function(value, key) - { - total_time += value.linear_execution_time; - }); - } - - table_items.push(total_time.toFixed(floating_point_precision.selected)); - } - else if(table_value.indexOf("algorithm_parameter.") == 0) - { - //total execution time information - var block_name = table_value.split("algorithm_parameter.")[1]; - var algorithm_name = block_name.split("__")[0]; - var parameter_name = block_name.split("__")[1]; - if(report_algorithm_parameters_experiment[experiment_name][algorithm_name] != undefined) - { - var value = ""; - for(var i = 0; i < report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name].length; i++) - { - if(i > 0 && i < report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name].length -1 ) - { - value += ","; - } - value = report_algorithm_parameters_experiment[experiment_name][algorithm_name][parameter_name][i]; - } - - table_items.push(value); - } - else - { - table_items.push("-"); - } - } - else - { - //results information - table_items.push((report_experiments[experiment_name].results[analyzer_block][table_value].value).toFixed(floating_point_precision.selected)); - } - } - else - { - var experiment_alias = report_experiments_alias[experiment_name]; - table_items.push(experiment_alias); - } - - }); - - final_table.push(table_items); - - }); - - var csv_text = ""; - for(var i = 0; i < final_table.length; i++) - { - if(i != 0) - { - csv_text += "\n"; - } - - for(var j = 0; j < final_table[i].length; j++) - { - if( j != 0) - { - csv_text += separator; - } - csv_text += final_table[i][j]; - } - } - - var pre_base64_text = "data:text/plain;base64,"; - var base_64_csv_text = pre_base64_text + Base64.encode(csv_text); - return base_64_csv_text; - } - - //Retrieve chart from api and display on proper item - function _get_chart(scope, sub_content, content_type, chart_id) - { - var required_plotter = []; - var set_plotter = '' - if(sub_content.selected_plotter != undefined) - { - //required_plotter.push(sub_content.selected_plotter); - set_plotter = sub_content.selected_plotter; - var requested_dataformat = ''; - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].name == sub_content.selected_plotter) - { - requested_dataformat = scope.report.plotters[i].dataformat; - break; - } - } - - //Get default plotter for required dataformat - for (var i = 0; i < scope.report.defaultplotters.length; i++) - { - if(scope.report.defaultplotters[i].dataformat == requested_dataformat) - { - sub_content.defaultplotter = scope.report.defaultplotters[i]; - break; - } - } - - required_plotter.push(sub_content.defaultplotter.plotter); - - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].dataformat == requested_dataformat && scope.report.plotters[i].name != sub_content.defaultplotter.plotter) - { - required_plotter.push(scope.report.plotters[i].name); - } - } - } - else - { - //Get default plotter for required dataformat - for (var i = 0; i < scope.report.defaultplotters.length; i++) - { - if(scope.report.defaultplotters[i].dataformat == sub_content.description) - { - sub_content.defaultplotter = scope.report.defaultplotters[i]; - break; - } - } - - required_plotter.push(sub_content.defaultplotter.plotter); - set_plotter = sub_content.defaultplotter.plotter; - - //Get other plotters for required dataformat - for (var i = 0; i < scope.report.plotters.length; i++) - { - if(scope.report.plotters[i].dataformat == sub_content.description && scope.report.plotters[i].name != sub_content.defaultplotter.plotter) - { - required_plotter.push(scope.report.plotters[i].name); - } - } - } - - var plotterparameter = []; - //get plotterparameters valid for requested plotter - var required_plotter_id = undefined; - for(var i = 0; i < scope.report.plotters.length; i++) - { - if(required_plotter[0] == scope.report.plotters[i].name) - { - required_plotter_id = scope.report.plotters[i].id - } - } - - //Get other plotterparameter - for (var i = 0; i < scope.report.plotterparameter.length; i++) - { - if(required_plotter_id == undefined) - { - plotterparameter.push(scope.report.plotterparameter[i].name); - } - else - { - if(scope.report.plotterparameter[i].plotter == required_plotter_id) - { - plotterparameter.push(scope.report.plotterparameter[i].name); - } - } - } - - var chart_name = sub_content.name; - - var legend_experiments = ''; - for(var i = 0; i < scope.report.experiments.length; i++) - { - if(i == 0) - legend_experiments = scope.report_experiments_alias[scope.report.experiments[i]]; - else - legend_experiments = legend_experiments+ "&" +scope.report_experiments_alias[scope.report.experiments[i]]; - } - - var request_data = { - experiment: scope.report.experiments, - analyzer: [scope.report.analyzer], - output: [chart_name], - plotter: set_plotter, - legend: legend_experiments, - //height: 300, - //width: 400, - }; - - base_url = scope.report.url_prefix; - - var plot_detail = {}; - plot_detail["required_plotter"] = required_plotter; - plot_detail["data"] = request_data; - if(sub_content.selected_template != undefined)// && sub_content.selected_template != "Default") - { - plot_detail["selected_template"] = sub_content.selected_template; - request_data.parameter = plot_detail["selected_template"]; - } - else - { - plot_detail["selected_template"] = sub_content.defaultplotter.parameter; - request_data.parameter = plot_detail["selected_template"]; - } - - if(sub_content.merged != undefined)// && sub_content.selected_template != "Default") - { - plot_detail["merged"] = sub_content.merged; - request_data.merged = plot_detail["merged"]; - } - else - { - plot_detail["merged"] = true; - request_data.merged = plot_detail["merged"]; - } - - beat.experiments.utils.getPlotData(base_url, - request_data, required_plotter, plotterparameter, content_type, - function(r_image_data, selected_content_type) { - download(r_image_data, chart_id + "." + content_type.toLowerCase(), selected_content_type); - } - ); - } - - -}); - - -//Directive used to handle table settings click -app.directive("aliasexperiment", function($compile) -{ - return { - link:function(scope, element, attrs) - { - var alias_name = scope.$parent.$parent.$parent.$parent.report_experiments_alias[scope.name]; - createAlias(scope.name, alias_name); - - /* - element.on("blur", function() - { - val = $(this).val(); - if (!val) { - alert("Alias for experiment" + scope.name + " can't be empty!"); - } - else { - var experiment_name = this.id.split("input_")[1]; - createAlias(experiment_name, val); - } - }); - */ - - element.bind("click", function() - { - var input_element = document.getElementById("input_"+scope.name); - var icon_element = document.getElementById("icon_"+scope.name); - var button_element = document.getElementById("button_alias_"+scope.name); - if(! $(input_element).hasClass("input-disabled")) - { - if ($(input_element).val()) - { - $(input_element).addClass("input-disabled"); - $(input_element).attr("disabled", true); - $(icon_element).removeClass("fa-unlock"); - $(icon_element).addClass("fa-lock"); - $(button_element).addClass("setalias"); - var experiment_name = attrs.id.split("button_alias_")[1]; - var alias_name = $(input_element).val(); - $(input_element).val(alias_name); - createAlias(experiment_name, alias_name); - } - else - { - alert("Alias for experiment" + scope.name + " can't be empty!"); - } - } - else - { - $(input_element).removeClass("input-disabled"); - $(input_element).attr("disabled", false); - $(icon_element).removeClass("fa-lock"); - $(icon_element).addClass("fa-unlock"); - $(button_element).removeClass("setalias"); - - } - }); - - function createAlias(experiment_name, alias_name) - { - scope.report_experiments_alias[experiment_name] = alias_name; - scope.$parent.$parent.$parent.$parent.report_experiments_alias[experiment_name] = alias_name; - - if(!scope.$$phase) - { - //$digest or $apply - var parent_scope = scope.$parent.$parent.$parent.$parent - var table_details = parent_scope.tables_details; - - for(var i = 0; i < parent_scope.report.orderedcontent.length; i++) - { - var element = document.getElementById(parent_scope.report.orderedcontent[i]); - - $(element).remove(); - - var id_content = parent_scope.report.orderedcontent[i]; - var type = id_content.split("_")[0]; - parent_scope.$parent.$broadcast("addSavedElement", id_content, type); - } - } - } - } - }; -}); - - -////Directive used to generate the dynamic table loading partials -//app.directive("chartdropdown", function(){ -// return { -// restrict: 'E', -// scope: true, -// replace: true, -// Â Â Â Â templateUrl:"/reports/partials/reportChartDropDown/", -// link: function(scope, element, attrs) -// { -// scope.dattrs = attrs; -// var element_id = (element.context.id).split("selector_"); -// var selected_template = scope.plots_details[element_id[1]].selected_template; -// if(selected_template != undefined) -// { -// -// scope.dattrs.selected_template = selected_template; -// var selector_name = "#"+element.context.id + " select"; -// } -// else -// { -// scope.dattrs.selected_template = "Default"; -// } -// -// element.bind("change", function() -// { -// var selected_template = element.context.children[0].selectedOptions[0].label; -// var elem_id = (attrs.id).split("selector_"); -// -// scope.plots_details[elem_id[1]].selected_template = selected_template; -// -// -// var type = "afdaf"; -// var id_content = elem_id[1]; -// scope.$parent.report.content[id_content].selected_template = selected_template; -// scope.$parent.$broadcast("redrawGraphElement", id_content, type); -// -// }); -// -// } -// -// }; -//}); diff --git a/beat/web/reports/static/reports/app/directives/save.js b/beat/web/reports/static/reports/app/directives/save.js new file mode 100644 index 0000000000000000000000000000000000000000..60d7c576b5fe3ab2c57f87367bdc8c2a9a769525 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/save.js @@ -0,0 +1,60 @@ +/* + * 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/. + */ + +/* + * reportSave + * Desc: + * saves the current report + */ +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'); + scope.$apply(); + }) + .catch(e => { + ErrorService.logError(e, `Could not save the report.`); + }); + }; + + el.bind('click', saveReport); + }, + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/tableItem.js b/beat/web/reports/static/reports/app/directives/tableItem.js new file mode 100644 index 0000000000000000000000000000000000000000..e157b69f6039b476303bbedc5baa28ea4c27317b --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/tableItem.js @@ -0,0 +1,293 @@ +/* + * 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/. + */ + +/* + * tableItem + * Desc: + * displays a table report item and lets the user + * manage this table's selected cols and float precision + */ +angular.module('reportApp') +.directive("groupTableItem", ['GroupsService', 'ExperimentsService', 'UrlService', function(GroupsService, ExperimentsService, UrlService){ + return { + scope: { + group: '=', + itemId: '=', + content: '=' + }, + link: function(scope){ + // aliases + scope.fields = scope.content.fields; + // ids + scope.domId = `${scope.group.name}_${scope.itemId}`; + scope.colSelectorId = `${scope.domId}_columnSelector`; + + // 1 - 10 + // probably the most concise way of generating 1-10 computationally + // also vastly more complex than writing [1,2,3,4,5,6,7,9,10] + scope.floatingPointRange = [...(new Array(10)).keys()].map(i => i + 1); + + // init the chosen cols for the table with the saved cols + scope.chosenCols = Array.from(scope.fields); + + // save new cols choice + // due to how angular handles functions passed as props to an element, + // it must return a function that does the actual work. + scope.saveChosenCols = () => () => { + // we want to keep the selected columns in order, + // so try to add new columns in their correct indices, + // and rm cols by mutating the array + + const newCols = scope.chosenCols + .filter(c => !scope.fields.includes(c)); + + const rmCols = scope.fields + .filter(f => !scope.chosenCols.includes(f) && f !== 'Experiment'); + + rmCols.forEach(rf => scope.fields.splice(scope.fields.indexOf(rf), 1)); + + newCols.forEach(nf => scope.fields.push(nf)); + }; + + // toggle val for viewing CSV + scope.isViewingCSV = { val: false }; + scope.toggleViewingCSV = () => { + scope.isViewingCSV.val = !scope.isViewingCSV.val; + }; + + // aliases + scope.fields = scope.content.fields; + + // add 'expName' to the beginning of the fields to show in the table + // if it isnt already there + if(scope.fields.length === 0 || scope.fields[0] !== 'Experiment'){ + scope.fields.unshift('Experiment'); + } + + // get possible table entries + scope.tableables = ExperimentsService.tableables || {}; + + // gets the field type (int, float, string, nothing) + scope.getFieldType = (field) => { + if(field === scope.fields[0]){ + return 'string'; + } + + let hasFieldObj = Object.values(scope.tableables) + .find(o => o[field]); + let fVal = hasFieldObj ? hasFieldObj[field] : {}; + let type; + if(fVal.type){ + type = fVal.type; + } else if(Number.isSafeInteger(fVal)){ + type = 'integer'; + } else if(Number.isFinite(fVal)){ + type = 'float'; + } else if(typeof fVal === 'string'){ + type = 'string'; + } else { + type = undefined; + } + + return type; + }; + // gets the field val for the given exp + scope.getFieldVal = (expName, field) => { + const alias = scope.group.aliases[expName].length > 0 ? + scope.group.aliases[expName] : expName; + + const name = scope.tableables[alias] ? + alias : expName; + + let fVal = scope.tableables[name] ? scope.tableables[name][field] : undefined; + let val; + + if(field === scope.fields[0]){ + val = alias; + } else if(!fVal){ + val = '-'; + } else { + let tmp; + if(fVal.value){ + tmp = fVal.value; + } else { + tmp = fVal; + } + + let type = scope.getFieldType(field); + if(type && type.startsWith('float')){ + val = tmp.toFixed(parseInt(scope.content.precision)); + } else { + val = tmp; + } + } + + return val; + }; + + // need to nest actual value in an obj to get angular + // to watch it correctly + scope.sortField = { val: 'Experiment', isReversed: false }; + // sort rows (one row per exp) + scope.sortFunc = (expName) => { + return scope.getFieldType(scope.sortField.val) ? + scope.getFieldVal(expName, scope.sortField.val) : expName; + }; + // sets the new sort field and direction + scope.setSortField = (field) => { + if(scope.sortField.val === field){ + scope.sortField.isReversed = !scope.sortField.isReversed; + } else { + scope.sortField.val = field; + scope.sortField.isReversed = false; + } + }; + + // a different view of the table + scope.getCSV = () => { + let fields = scope.fields; + let exps = scope.group.experiments + // clone arr + .map(e => `${e}`) + .sort((ea, eb) => (scope.sortField.isReversed ? -1 : 1) * (scope.sortFunc(ea) < scope.sortFunc(eb) ? -1 : 1)) + ; + + let str = ''; + + let fieldsStr = fields + .map(f => `${f}(${scope.getFieldType(f)})`) + .join(','); + + let expsStrs = exps + .map(e => fields.map(f => `${scope.getFieldVal(e, f)}`).join(',')) + .join('\n'); + + str = `${fieldsStr}\n${expsStrs}`; + + return str; + }; + + // get experiment url for linking to exps + // returns the url if successful else false + scope.getExperimentUrl = (expName) => { + // if theres more than 2 '/' in the expName, + // its the absolute URL, and the user has access + // to view the experiment. + if(expName.split('/').length > 3){ + return UrlService.getExperimentUrl(expName); + } + + // else, its the short name + // (just the exp name, not including author/toolchain) + // and the user cant see the exp + return false; + }; + + scope.sortableOptions = { + items: `th:not(:first-child)` + }; + + scope.isViewmode = UrlService.isViewmode; + }, + template: ` +<div panel-container dom-id='domId'> + <header> + <editable-label obj='content' field='itemName'></editable-label> + <div ng-if='!isViewmode()' class='btn-group action-buttons'> + <span + ng-click='group.removeReportItem(itemId)' + class="btn btn-default btn-delete" + data-toggle="tooltip" data-placement="top" title="Delete Table"> + <i class="fa fa-times fa-lg"></i> + </span> + <span drag-handle handle-helper-class='dragItem'></span> + </div> + <div class="btn-group" role="group" role='tab'> + <div ng-if='!isViewmode()' class="btn-group" role="group" + group-table-field-selector + id='colSelectorId' + group='group' + cols-selected='chosenCols' + button-action='saveChosenCols()' + title="Choose Columns" + button-text="Save Column Choices" + > + </div> + <div class='btn-group' role='group'> + <button class='btn btn-default' id="{{domId}}-precision" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Float Precision: {{ content.precision }} + <span class="caret"></span> + </button> + <ul class="dropdown-menu" aria-labelledby="{{domId}}-precision"> + <li ng-click='content.precision = i' ng-repeat='i in floatingPointRange'><a>{{ i }}</a></li> + </ul> + </div> + <button class='btn btn-default' ng-click='toggleViewingCSV()'> + Toggle CSV View + </button> + </div> + </header> + <content> + <div class='panel-body'> + <div ng-if='isViewingCSV.val'> + <pre>{{ getCSV() }}</pre> + </div> + <div ng-if='!isViewingCSV.val' style='height: 100%; overflow-x: auto;'> + <table class="table table-striped table-hover"> + <thead> + <tr ui-sortable='sortableOptions' ng-model='fields'> + <th ng-repeat='field in fields'> + <span + ng-if="sortField.val == field" + class='glyphicon' + ng-class="{ + 'glyphicon-chevron-up': sortField.isReversed, + 'glyphicon-chevron-down': !sortField.isReversed + }" + > + </span> + <a role='button' ng-click='setSortField(field)'> + {{ field }} <i>({{ getFieldType(field) }})</i> + </a> + </th> + </tr> + </thead> + <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) }}'> + {{ getFieldVal(exp, field) }} + </a> + <span ng-if='!$index == 0 || !getExperimentUrl(exp)'> + {{ getFieldVal(exp, field) }} + </span> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </content> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/directives/textItem.js b/beat/web/reports/static/reports/app/directives/textItem.js new file mode 100644 index 0000000000000000000000000000000000000000..72a2c9967bb523a1fdefbad820dc022c0748a2b5 --- /dev/null +++ b/beat/web/reports/static/reports/app/directives/textItem.js @@ -0,0 +1,152 @@ +/* + * 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/. + */ + +/* + * groupTextItem + * Desc: + * displays a text report item: + * - sends the raw RST to the server to be compiled, + * and displays the returned HTML + * - the user can edit the RST using codemirror + * - the compile is async and doesnt require refreshing the page + */ +angular.module('reportApp') +.directive("groupTextItem", ['GroupsService', '$sce', 'UrlService', 'reportFactory', function(GroupsService, $sce, UrlService, reportFactory){ + return { + scope: { + group: '=', + reportItem: '=', + }, + link: function(scope){ + // aliases + // angular requires that compiling raw html be sanitized + scope.trustAsHtml = $sce.trustAsHtml; + scope.item = scope.reportItem; + scope.domId = `${scope.group.name}_${scope.item.id}`; + + scope.isViewmode = UrlService.isViewmode; + + // codemirror options + scope.uicmOptions = { + mode: 'rst', + readOnly: scope.isViewmode() + }; + + // handle compiling content + // holds the last response from the server + scope.compiledContent = { val: '' }; + scope.compileContent = () => { + let url = UrlService.getCompileRstUrl(); + let content = !scope.isViewmode() ? scope.item.content.text : + `${scope.group.name}|${scope.group.reportItems.indexOf(scope.reportItem)}`; + + return reportFactory.compileRST(url, content) + .then(data => { + // when compiled, save the raw html + scope.compiledContent.val = data.data.html_str; + }); + }; + + // handle edit/save/cancel buttons + scope.isSrcMode = { val: false }; + // when editing, use a buffer to hold the raw text + scope.unsavedContent = { val: `${scope.item.content.text}` }; + // save the buffer to the actual report item content + scope.saveAction = () => { + scope.item.content.text = scope.unsavedContent.val; + scope.compileContent(); + scope.isSrcMode.val = false; + }; + // discard buffer and use report item content + scope.cancelAction = () => { + scope.unsavedContent.val = `${scope.item.content.text}`; + scope.isSrcMode.val = false; + }; + + // compile the content when loaded + scope.compileContent(); + }, + template: ` +<div panel-container dom-id='domId'> + <header> + <editable-label obj='item.content' field='itemName'></editable-label> + <div ng-if='!isViewmode()' class='btn-group action-buttons'> + <span + ng-click='group.removeReportItem(item.id)' + class="btn btn-default btn-delete" + data-toggle="tooltip" data-placement="top" title="Delete Text Block"> + <i class="fa fa-times fa-lg"></i> + </span> + <span drag-handle handle-helper-class='dragItem'></span> + </div> + <button ng-if='isViewmode()' class='btn btn-primary' ng-click='isSrcMode.val = !isSrcMode.val'> + Toggle Source View + </button> + </header> + <content> + <div ng-if='!isViewmode()' class='row'> + <div class='col-sm-10'> + <div ng-if='!isSrcMode.val' ng-bind-html='trustAsHtml(compiledContent.val)'></div> + <div ng-if='isSrcMode.val'> + <ui-codemirror ng-model='unsavedContent.val' ui-codemirror-opts='uicmOptions'></ui-codemirror> + <p class='help-block'> + Describe the object thoroughly using <a href="http://docutils.sourceforge.net/rst.html">reStructuredText mark-up</a><br><i class="fa fa-thumbs-up"></i> The ruler at 80 columns indicate suggested <a href="https://en.wikipedia.org/wiki/POSIX">POSIX line breaks</a> (for readability).<br><i class="fa fa-thumbs-up"></i> The editor will automatically enlarge to accomodate the entirety of your input<br><i class="fa fa-thumbs-up"></i> Use <a href="http://codemirror.net/doc/manual.html#commands">keyboard shortcuts</a> for search/replace and faster editing. For example, use Ctrl-F (PC) or Cmd-F (Mac) to search through this box + </p> + </div> + </div> + <div class='col-sm-2'> + <div class="pull-right action-buttons"> + <a + ng-if='!isSrcMode.val' + ng-click='isSrcMode.val = !isSrcMode.val' + class="btn btn-primary btn-sm"> + <i class="fa fa-edit fa-lg"></i> Edit + </a> + <a + ng-if='isSrcMode.val' + ng-click='cancelAction()' + class="btn btn-danger btn-sm"> + <i class="fa fa-times fa-lg"></i> Cancel + </a> + <a + ng-if='isSrcMode.val' + ng-click='saveAction()' + class="btn btn-success btn-sm"> + <i class="fa fa-save fa-lg"></i> Save + </a> + </div> + </div> + </div> + <div ng-if='isViewmode()' class='row'> + <div class='col-sm-12'> + <div ng-if='!isSrcMode.val' ng-bind-html='trustAsHtml(compiledContent.val)'></div> + <div ng-if='isSrcMode.val'> + <i>Readonly view</i> + <ui-codemirror ng-model='item.content.text' ui-codemirror-opts='uicmOptions'></ui-codemirror> + </div> + </div> + </div> + </content> +</div> +` + }; +}]); diff --git a/beat/web/reports/static/reports/app/factories/experimentFactory.js b/beat/web/reports/static/reports/app/factories/experimentFactory.js index b80feaae54b00caccc5f2a21c775b686d1ade1ec..6ef4f0b32436f78da2d77a8a02909ce5f88134b6 100644 --- a/beat/web/reports/static/reports/app/factories/experimentFactory.js +++ b/beat/web/reports/static/reports/app/factories/experimentFactory.js @@ -1,54 +1,49 @@ /* * 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/. -*/ + */ //This factory retrieves data from the REST API and associates it with the $scope -app.factory('experimentFactory', function($http, $q) -{ - return { - getExperimentInformation: function (url_prefix, experiment_id) - { - urlBase = url_prefix + '/api/v1/experiments'; - return $http.get(urlBase + '/' + experiment_id + '/') - .then(function(response){ - return response.data; - }); - }, +angular.module('reportApp').factory('experimentFactory', function($http, $q){ + return { + getExperimentInformation: function (url_prefix, experiment_id){ + urlBase = url_prefix + '/api/v1/experiments'; + return $http.get(urlBase + '/' + experiment_id + '/') + .then(function(response){ + return response.data; + }); + }, - getAllExperimentResultsForAuthor: function (user, report_id, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - return $http.get(urlBase + '/' + user + '/' + report_id + '/results_author/') - .then(function(response){ - return response.data; - }); - }, + getAllExperimentResultsForAuthor: function (user, report_id, url_prefix){ + urlBase = url_prefix + '/api/v1/reports'; + return $http.get(urlBase + '/' + user + '/' + report_id + '/results_author/') + .then(function(response){ + return response.data; + }); + }, - getAllExperimentResults: function (url_prefix, report_number) - { - urlBase = url_prefix + '/api/v1/reports'; - return $http.get(urlBase + '/' + report_number + '/results/') - .then(function(response){ - return response.data; - }); - } - - } + getAllExperimentResults: function (url_prefix, report_number){ + urlBase = url_prefix + '/api/v1/reports'; + return $http.get(urlBase + '/' + report_number + '/results/') + .then(function(response){ + return response.data; + }); + } + }; }); diff --git a/beat/web/reports/static/reports/app/factories/plotterFactory.js b/beat/web/reports/static/reports/app/factories/plotterFactory.js index fa1937528dab26c01a0016207f427a76e63e6c3f..232d70245363a5950baef16039f5f47ec3b2302f 100644 --- a/beat/web/reports/static/reports/app/factories/plotterFactory.js +++ b/beat/web/reports/static/reports/app/factories/plotterFactory.js @@ -1,46 +1,43 @@ /* * 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/. -*/ + */ //This factory retrieves data from the REST API and associates it with the $scope -app.factory('plotterFactory', ['$http', function($http) -{ - var plotterFactory = {}; +angular.module('reportApp').factory('plotterFactory', ['$http', function($http){ + let plotterFactory = {}; - plotterFactory.getPlotters = function (url_prefix) - { - urlBase = url_prefix + '/api/v1/plotters'; - return $http.get(urlBase + '/'); - }; + plotterFactory.getPlotters = function (url_prefix){ + urlBase = url_prefix + '/api/v1/plotters'; + return $http.get(urlBase + '/'); + }; - plotterFactory.getDefaultPlotters = function (url_prefix) - { - urlBase = url_prefix + '/api/v1/plotters/defaultplotters'; - return $http.get(urlBase + '/'); - }; + plotterFactory.getDefaultPlotters = function (url_prefix){ + urlBase = url_prefix + '/api/v1/plotters/defaultplotters'; + return $http.get(urlBase + '/'); + }; - plotterFactory.getPlotterParameter = function (url_prefix) - { - urlBase = url_prefix + '/api/v1/plotters/plotterparameters'; - return $http.get(urlBase + '/'); - }; + plotterFactory.getPlotterParameter = function (url_prefix) + { + urlBase = url_prefix + '/api/v1/plotters/plotterparameters'; + return $http.get(urlBase + '/'); + }; - return plotterFactory; + return plotterFactory; }]); diff --git a/beat/web/reports/static/reports/app/factories/reportFactory.js b/beat/web/reports/static/reports/app/factories/reportFactory.js index ee21e1ffa99faa00eb82d46ed30c4ba6c34a3dd1..7c06a21430dc2f0bc97b98c32dea6ed08159d58a 100644 --- a/beat/web/reports/static/reports/app/factories/reportFactory.js +++ b/beat/web/reports/static/reports/app/factories/reportFactory.js @@ -1,114 +1,116 @@ /* * 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/. -*/ + */ //This factory retrieves data from the REST API and associates it with the $scope -app.factory('reportFactory', ['$http', 'experimentFactory', function($http, experimentFactory) -{ - - var urlBase = '/api/v1/reports'; - var reportFactory = {}; - - reportFactory.getReportInformation = function (user, report_id, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - return $http.get(urlBase + '/' + user + '/' + report_id + '/'); - }; - - reportFactory.getReportInformationFromNumber = function (report_number, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - return $http.get(urlBase + '/' + report_number + '/'); - }; - - reportFactory.updateReport = function (user, report_id, reportData, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); - - return $http({ - headers: {'Content-Type': 'application/json'}, - url: urlBase + '/' + user + '/' + report_id + '/', - method: "PUT", - data: reportData, - }) - - }; - - reportFactory.lockReport = function (user, report_id, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); - - return $http({ - headers: {'Content-Type': 'application/json'}, - url: urlBase + '/' + user + '/' + report_id + '/lock/', - method: "POST", - //data: reportData, - }) - - }; - - reportFactory.publishReport = function (url_prefix, user, report_id, reportData) - { - urlBase = url_prefix + '/api/v1/reports'; - //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); - - return $http({ - headers: {'Content-Type': 'application/json'}, - url: urlBase + '/' + user + '/' + report_id + '/publish/', - method: "POST", - data: reportData, - }) - - }; - - reportFactory.publishReportAlgorithms = function (user, report_id, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); - - return $http({ - headers: {'Content-Type': 'application/json'}, - url: urlBase + '/' + user + '/' + report_id + '/algorithms/', - method: "GET", - //data: reportData, - }) - - }; - - - reportFactory.removeExperiment = function (user, report_id, experiment, url_prefix) - { - urlBase = url_prefix + '/api/v1/reports'; - //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); - - return $http({ - headers: {'Content-Type': 'application/json'}, - url: urlBase + '/' + user + '/' + report_id + '/remove/', - method: "POST", - data: experiment, - }) - - }; - - - return reportFactory; +angular.module('reportApp').factory('reportFactory', ['$http', 'experimentFactory', 'UrlService', function($http, experimentFactory, UrlService) { + let urlBase = '/api/v1/reports'; + let reportFactory = {}; + + reportFactory.getReportInformation = function (user, report_id, url_prefix){ + urlBase = url_prefix + '/api/v1/reports'; + return $http.get(urlBase + '/' + user + '/' + report_id + '/'); + }; + + reportFactory.getReportInformationFromNumber = function (report_number, url_prefix){ + urlBase = url_prefix + '/api/v1/reports'; + return $http.get(urlBase + '/' + report_number + '/'); + }; + + reportFactory.updateReport = function (user, report_id, reportData, url_prefix){ + urlBase = url_prefix + '/api/v1/reports'; + //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); + + return $http({ + headers: {'Content-Type': 'application/json'}, + url: urlBase + '/' + user + '/' + report_id + '/', + method: "PUT", + data: reportData, + }); + }; + + reportFactory.lockReport = function (user, report_id, url_prefix){ + urlBase = url_prefix + '/api/v1/reports'; + //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); + + return $http({ + headers: {'Content-Type': 'application/json'}, + url: urlBase + '/' + user + '/' + report_id + '/lock/', + method: "POST", + //data: reportData, + }); + }; + + reportFactory.publishReport = function (url_prefix, user, report_id, reportData){ + urlBase = url_prefix + '/api/v1/reports'; + //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); + + return $http({ + headers: {'Content-Type': 'application/json'}, + url: urlBase + '/' + user + '/' + report_id + '/publish/', + method: "POST", + data: reportData, + }); + }; + + reportFactory.publishReportAlgorithms = function (user, report_id, url_prefix){ + urlBase = url_prefix + '/api/v1/reports'; + //return $http.put(urlBase + '/' + user + '/' + report_id + '/'); + + return $http({ + headers: {'Content-Type': 'application/json'}, + url: urlBase + '/' + user + '/' + report_id + '/algorithms/', + method: "GET", + //data: reportData, + }); + }; + + reportFactory.removeExperiments = function (expNames){ + if(!expNames || expNames.length === 0){ + return Promise.resolve(); + } + + let url = UrlService.getRemoveExperimentUrl(); + return $http({ + headers: {'Content-Type': 'application/json'}, + url, + method: "POST", + data: { + experiments: expNames + } + }) + }; + + reportFactory.compileRST = (url, raw) => { + let data = { + raw + }; + + return $http({ + headers: {'Content-Type': 'application/json'}, + url, + method: "POST", + data + }); + }; + + + return reportFactory; }]); diff --git a/beat/web/reports/templates/reports/dialogs/report_remove_item.html b/beat/web/reports/static/reports/app/services/errorService.js similarity index 52% rename from beat/web/reports/templates/reports/dialogs/report_remove_item.html rename to beat/web/reports/static/reports/app/services/errorService.js index 4bc2feeabf65b8a9a093798ab2506847894d316e..562c8a3261e40dd61b42d84fb0ecc1dfdb88f1bf 100644 --- a/beat/web/reports/templates/reports/dialogs/report_remove_item.html +++ b/beat/web/reports/static/reports/app/services/errorService.js @@ -1,40 +1,58 @@ -{% 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 %} -<div id="{{ dialog_id }}" class="report_remove_item" title="Report" style="display:none"> - <p class="explanation">Remove item from report</p> + */ +/* + * ErrorService + * Desc: + * Centralizes user-facing error-handling in the reports app. + * Other parts of the app can register errors (i.e. 404s, server errors, + * invalid input, etc.) with the ErrorService. + * The ErrorService will (one at a time & synchronously!) pop up an error + * modal (directives/error.js, "report-error") to let the user know. + */ +angular.module('reportApp').factory('ErrorService', ['$rootScope', function($rootScope){ + const es = { + }; - <div class="explanation_text"> - <p class="experiments"> - Do you wish to delete this item from your report? - </p> - </div> - <div class="warnings" style="display:none"> - <p class="experiments">Sucessfully removed report item</p> - <ul class="experimentslist"></ul> - </div> + class ReportError { + constructor (errorObj, message) { + this._error = errorObj; + this._message = message || ''; + } - <div class="errors" style="display:none"> - <p class="errors">Some errors occured while attempting to remove the report item:</p> - <ul class="errorslist"></ul> - </div> -</div> + get error () { + return this._error; + } + + get message () { + return this._message; + } + } + + es.logError = (error, message) => { + const newErr = new ReportError(error, message); + + $rootScope.$broadcast('user:error', newErr); + }; + + return es; +}]); diff --git a/beat/web/reports/static/reports/app/services/experimentsService.js b/beat/web/reports/static/reports/app/services/experimentsService.js new file mode 100644 index 0000000000000000000000000000000000000000..1adef6fec8598c6668ba386141e3973caeb9f76a --- /dev/null +++ b/beat/web/reports/static/reports/app/services/experimentsService.js @@ -0,0 +1,243 @@ +/* + * 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/. + */ + +/* + * ExperimentsService + * Desc: + * Manages the experiments data, including storing it and generating + * different views of the data + */ +angular.module('reportApp').factory('ExperimentsService', ['experimentFactory', 'GroupsService', 'UrlService', function(experimentFactory, GroupsService, UrlService){ + // holds the raw exp data received from the server + const expData = {}; + // holds the view generated from `getTableablesFromExpName` + const tableData = {}; + // holds the view generated from `getPlottablesFromExpName` + const plotData = {}; + const rawExpNames = []; + + // lots of experiments have fields that are differently-named + // but mean the same thing. + // These fields are the *only* available field in the obj. + const getVarProp = (obj, dontLogErrors) => { + let keys = Object.keys(obj); + if(keys.length !== 1 && !dontLogErrors){ + //console.warn(`Found bad obj for getVarProp:`); + //console.log(obj); + } + + return obj[keys[0]]; + }; + + // analyzer + const getAnalyzerFromExpName = (expName) => getVarProp(expData[expName].declaration.analyzers, true).algorithm; + + // get all possible objects that could be shown in a table + const getTableablesFromExpName = (expName) => { + let currExp = expData[expName]; + + // get all result objs that have a type that doesnt start with 'plot/' + // these results can be displayed in a table + let results = Object.entries(getVarProp(currExp.results)) + // type not starting with plot + .filter(([res_name, obj]) => !obj.type.startsWith('plot/')) + // after we've filtered fields, make it into an obj + .map(([res_name, obj]) => { return {[res_name]: obj}; }) + // concat objs via reduce + .reduce((o, curr) => Object.assign(o, curr), {}) + ; + + // get block parameters + let blockVars = Object.entries(currExp.declaration.blocks) + // only look at blocks with parameters + .filter(([blockName, block]) => block.parameters) + // loop through each block + .map(([blockName, block]) => { + // flatten the block's execution infos + return Object.entries(block.parameters) + .map(([fieldName, field]) => { + return { [`${blockName}.${fieldName}`]: field }; + }) + .reduce((o, fieldObj) => Object.assign(o, fieldObj), {}) + ; + }) + // flatten the exp's blocks' params + .reduce((o, blockObj) => Object.assign(o, blockObj), {}) + ; + + // get global experiment variables + let globalVars = Object.entries(currExp.declaration.globals) + // properties that have a '/' in the name + .filter(([name, obj]) => name.includes('/')) + .map(([algName, alg]) => { + // flatten the alg's fields + return Object.entries(alg) + .map(([fieldName, field]) => { + return { [`${algName}.${fieldName}`]: field }; + }) + .reduce((o, fieldObj) => Object.assign(o, fieldObj), {}) + ; + }) + // flatten the exp's algs' fields + .reduce((o, algObj) => Object.assign(o, algObj), {}) + ; + + // get block timing infos + let blockTiming = Object.entries(currExp.execution_info) + // loop through each block + .map(([blockName, block]) => { + // flatten the block's execution infos + return Object.entries(block) + .map(([fieldName, field]) => { + return { [`${blockName}.${fieldName}`]: field }; + }) + .reduce((o, fieldObj) => Object.assign(o, fieldObj), {}) + ; + }) + // flatten the exp's execution infos + .reduce((o, blockObj) => Object.assign(o, blockObj), {}) + ; + + // get total timing info + let expTiming = { + 'total_execution_time': Object.entries(blockTiming) + .filter(([name, val]) => name.includes('linear_execution_time')) + .map(([name, val]) => val) + .reduce((n, v) => n + v, 0) + }; + + // concat all objs + let allObj = Object.assign({}, + results, + globalVars, + blockVars, + blockTiming, + expTiming + ); + + return allObj; + }; + + // get all result objs that have a type that starts with 'plot/' + // these results can be displayed in a plot + const getPlottablesFromExpName = (expName) => { + return Object.entries(getVarProp(expData[expName].results)) + .filter(([res_name, obj]) => obj.type.startsWith('plot/')) + .map(([res_name, obj]) => Object.assign(obj, { label: res_name })); + }; + + // make sure there are no invalid experiments in the report's groups + // invalid -> in a group but not in the report + const cleanGroups = (validExpNames) => { + GroupsService.groups.forEach(g => { + g.experiments.forEach(e => { + // when the user can see the exp, the full name of the exp is available, + // including the user who made it, and the toolchain + const fullNameValid = validExpNames.includes(e); + // when the exp isnt able to be seen by the user, + // only the actual name of the exp is available + const partNameValid = validExpNames.includes(e.split('/').pop()); + if(!(fullNameValid || partNameValid)){ + //console.log(`INVALID EXPERIMENT: ${e}`); + g.removeExperiment(e); + } + }); + }); + }; + + // fetch the exp data and process it + const loadExperiments = () => { + let expFetch; + const namePath = UrlService.getByNamePath(); + const numberPath = UrlService.getByNumberPath(); + + if(namePath && namePath.length > 0){ + // get exps by report num + const user = namePath.split('/')[1]; + const name = namePath.split('/')[2]; + expFetch = experimentFactory.getAllExperimentResultsForAuthor(user, name, ''); + } else if(numberPath && numberPath.length > 0){ + //by report author + const num = numberPath.split('/')[1]; + expFetch = experimentFactory.getAllExperimentResults('', num); + } + + return expFetch + .then((exps) => { + // save the exp names as they were given by the server + Object.keys(exps).forEach(e => rawExpNames.push(e)); + + // process the given exp data + Object.entries(exps) + .forEach(([expName, exp]) => { + // depending on the permissions of the user + // when viewing this report + // the author/toolchain may be hidden. + // to be safe, always process + // both the given name (either the full or short name) + const shortName = expName.split('/').pop(); + for(let n of [shortName, expName]){ + expData[n] = exp; + tableData[n] = getTableablesFromExpName(n); + plotData[n] = getPlottablesFromExpName(n); + } + }); + + console.log(expData); + console.log(tableData); + console.log(plotData); + + cleanGroups(rawExpNames); + + return expData; + }); + }; + + const cachedDeletedExperiments = []; + + const deleteExperiment = function(expName) { + // delete from all groups + GroupsService.groups.forEach(g => g.removeExperiment(expName)); + + // delete from expNames + const expNameIdx = this.experimentNames.indexOf(expName); + this.experimentNames.splice(expNameIdx, 1); + + // delete from expData + delete this.experiments[expName]; + + cachedDeletedExperiments.push(expName); + }; + + loadExperiments(); + + return { + experimentNames: rawExpNames, + experiments: expData, + plottables: plotData, + tableables: tableData, + getAnalyzerFromExpName, + loadExperiments, + deleteExperiment, + cachedDeletedExperiments + }; +}]); diff --git a/beat/web/reports/static/reports/app/services/groupsService.js b/beat/web/reports/static/reports/app/services/groupsService.js new file mode 100644 index 0000000000000000000000000000000000000000..89913dccb3b6cf7e1e0351a56ab40bff78d1dfee --- /dev/null +++ b/beat/web/reports/static/reports/app/services/groupsService.js @@ -0,0 +1,261 @@ +/* + * 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/. + */ + +/* + * GroupsService + * Desc: + * The main datastore for the reports app, holding the tree of + * relationships between groups, experiments, aliases, and report items + */ +angular.module('reportApp').factory('GroupsService', ['reportFactory', function(reportFactory){ + let groupsServiceInstance = {}; + // experiments of reports are in arbitrary groups, + // in a many-to-many relationship + let groupData = []; + + // is the report editable? + // set by the reportController when saving report data + groupsServiceInstance.isEditable = undefined; + groupsServiceInstance.setEditable = (val) => { + if(val !== true && val !== false){ + throw new Error(`invalid isEditable: ${JSON.stringify(val)}`); + } + if(groupsServiceInstance.isEditable !== undefined){ + throw new Error(`isEditable already set: ${JSON.stringify(groupsServiceInstance.isEditable)}`); + } + groupsServiceInstance.isEditable = val; + }; + + // represents a Group in the report + // has a name and a list of experiments that belong to it + class Group { + constructor (name) { + this._name = name; + this._analyzer = ''; + this._experimentNames = new Set(); + this._reportItems = []; + this._aliases = {}; + } + + // get the experiment names in this group + get experiments () { + return Array.from(this._experimentNames); + } + + // get the report items in this group + get reportItems () { + return Array.from(this._reportItems); + } + + // get the group name + get name () { + return this._name; + } + + // get the analyzer of the experiments in this group + get analyzer () { + return this._analyzer; + } + + // gets the aliases for the experiments in the group + get aliases () { + return this._aliases; + } + + set analyzer (analyzer) { + this._analyzer = analyzer; + } + + // add an exp to this group + // optionally sets the analyzer + // initializes the new exp's alias to its name + addExperiment (expName, analyzer) { + let res = this._experimentNames.add(expName); + if(this._experimentNames.size === 1 && analyzer && analyzer.length > 0){ + this.analyzer = analyzer; + } + if(!this.aliases[expName]){ + let autoAlias = expName.split('/').pop(); + this.setAliasToExperiment(autoAlias, expName); + } + + return res; + } + + // rm an exp from this group as well as its alias + removeExperiment (expName) { + let res = this._experimentNames.delete(expName); + if(this._experimentNames.size === 0){ + this.analyzer = ''; + this.reportItems.forEach(i => this.removeReportItem(i.id)); + } + this.unsetExperimentAlias(expName); + + return res; + } + + // add an item (table, plot, or text block) to this group + // if the id already exists, it just replaces the old + // element with the new one + addReportItem (id, content) { + let newEl = { + id, + content + }; + + let alreadyAddedEl = this._reportItems.find(i => i.id === id); + if(alreadyAddedEl){ + let idx = this._reportItems.indexOf(alreadyAddedEl); + return this._reportItems.splice(idx, 1, newEl); + } else { + return this._reportItems.push(newEl); + } + } + + // rm a report item from this group + removeReportItem (id) { + let idx = this._reportItems + .indexOf(this._reportItems.find(o => o.id === id)); + + return this._reportItems.splice(idx, 1); + } + + // (re)sets an alias to an experiment in the group + setAliasToExperiment (alias, expName) { + if(!this.experiments.includes(expName)){ + return false; + } + + this._aliases[expName] = alias; + } + + // unsets an alias for an experiment + unsetExperimentAlias (expName) { + return delete this._aliases[expName]; + } + }; + + // gets groups + groupsServiceInstance.groups = groupData; + + // serializes groups as an object with form: + // { + // <group name 1>: { + // experiments: [], + // reportItems: [], + // analyzer: '', + // aliases: {}, + // idx: 1 + // }, + // ... + // } + // the 'idx' saves the ordering of the groups + groupsServiceInstance.serializeGroups = () => { + return groupData + .map((g, i) => { return { + [g.name]: { + experiments: g.experiments, + reportItems: g.reportItems, + analyzer: g.analyzer, + aliases: g.aliases, + idx: i + } + }; + }) + .reduce((o, g) => Object.assign(o, g), {}); + }; + + // create a new group for the report + // returns false if it already exists + // returns the newly added group if successful + groupsServiceInstance.createGroup = (name) => { + if(typeof name !== 'string'){ + throw new Error(`new group name is not a string: ${JSON.stringify(name)}`); + } + + if(groupData.find(g => g.name === name)){ + return false; + } + + let g = new Group(name); + + groupData.push(g); + return g; + }; + + // delete a group + // via MUTATING the groupdata + groupsServiceInstance.deleteGroup = (name) => { + let idx = groupData.indexOf(groupData.find(g => g.name === name)); + if (idx > -1) { + groupData.splice(idx, 1); + } + }; + + // load group info from the serialized format: + // { + // <group name 1>: { + // experiments: [], + // reportItems: [], + // analyzer: '', + // aliases: {}, + // idx: 1 + // }, + // ... + // } + groupsServiceInstance.loadGroups = (data) => { + // wipe data + groupData.splice(0, groupData.length); + let safeData = data || {}; + + Object.entries(safeData) + // sometimes we get an empty string for name for some reason + .filter(([groupName, gData]) => groupName.length > 0) + // sort using the saved index values + .sort(([aName, a], [bName, b]) => { + // if these are undefined, the sort func handles it + let ia = a.idx; + let ib = b.idx; + return ia - ib; + }) + .forEach(([groupName, gData]) => { + let g = groupsServiceInstance.createGroup(groupName); + + // default group data to empty + let analyzer = gData.analyzer || ''; + let experiments = gData.experiments || []; + let reportItems = gData.reportItems || []; + let aliases = gData.aliases || {}; + + // save fields to group + // by MUTATING the group obj + g.analyzer = analyzer; + experiments.forEach(n => g.addExperiment(n)); + reportItems.forEach(i => g.addReportItem(i.id, i.content)); + Object.entries(aliases).forEach(([e, a]) => g.setAliasToExperiment(a, e)); + }); + + //console.log(groupsServiceInstance.groups); + }; + + return groupsServiceInstance; +}]); diff --git a/beat/web/reports/static/reports/app/services/plotService.js b/beat/web/reports/static/reports/app/services/plotService.js new file mode 100644 index 0000000000000000000000000000000000000000..350936756563e17922756861ad07433ce4091cc1 --- /dev/null +++ b/beat/web/reports/static/reports/app/services/plotService.js @@ -0,0 +1,250 @@ +/* + * 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/. + */ + +/* + * PlotService + * Desc: + * Manages the plots in the report, + * including: + * - Adding new/saved + * - Configuring + * - Deleting + * - Rendering + */ +angular.module('reportApp').factory('PlotService', ['UrlService', function(UrlService){ + const ps = { + // these are provided by ReportService + plotters: [], + defaultPlotters: [], + plotterParameters: [], + reportNumber: undefined + }; + + // this 'queue' idea is the solution to trying to load plots before we receive all the data + // from the server that we need (report info, exp info, etc.). + // until the queue is processed (after which, no queue is needed), + // plots that want to be rendered are added to a queue to wait. + const queue = []; + let noQueueNeeded = false; + + const getDefaults = (plotType) => ps.defaultPlotters.find(p => p.dataformat === plotType); + + // data to be used in to interact with the plot + // users can change the plotter used if theres more than 1 available for that + // plot type + const getPossiblePlotters = (itemContent) => + ps.plotters.filter(p => p.dataformat === itemContent.type).map(p => p.name); + + // users can choose which params to use if more than 1 available + // for that plotter + const getPossibleConfigs = (plotter) => + ps.plotterParameters.filter(pp => plotter.id === pp.plotter).map(pp => pp.name); + + const getPlotter = (itemContent) => { + const savedPlotterName = itemContent.savedPlotter; + // defaults obj, in case we're using defaults + const defaults = getDefaults(itemContent.type); + + // a plot obj can have different plotters, + const plotter = ps.plotters.find(p => p.name === itemContent.savedPlotter) || + ps.plotters.find(p => p.name === defaults.plotter); + + return plotter; + }; + + const getPlotterConfig = (itemContent) => { + // defaults obj, in case we're using defaults + const defaults = getDefaults(itemContent.type); + + // each plotter obj can each have different configurations (plotter parameter instances) + const config = ps.plotterParameters.find(pp => pp.name === itemContent.savedConfig) || + ps.plotterParameters.find(pp => pp.name === defaults.parameter); + + return config; + }; + + // constructs the payload to send to the server + const constructPlotInfo = (group, itemId) => { + const content = group.reportItems.find(i => i.id === itemId).content; + const plotter = getPlotter(content); + const config = getPlotterConfig(content); + + /* + if(!content || !plotter || !config){ + console.error(`plotter info not found: content: ${content} defaults: ${defaults} plotter: ${plotter} config: ${config}`); + console.log(ps.plotters); + console.log(ps.defaultPlotters); + console.log(ps.plotterParameters); + return; + } + + // sanity check for the plotter & config + // the config's "plotter" field should be equal to the plotter's id field + if(config.plotter !== plotter.id){ + console.error(`Config plotter val "${config.plotter}" != plotter id "${plotter.id}":`); + console.log(config); + console.log(plotter); + return; + }; + */ + + // the data to be sent to the server + const requestData = { + report_number: ps.reportNumber, + // exps in the group + experiment: group.experiments, + // group's analyzer + analyzer: [group.analyzer], + // ? + output: [content.name], + // plotter to use + plotter: plotter.name, + // config to use + parameter: config.name, + // string for making the legend in the plot + legend: group.experiments.map(e => group.aliases[e]).join('&'), + // whether one plot with all the exps' data, + // or one plot for each exp + merged: content.merged === undefined ? true : content.merged + }; + + const possiblePlotters = getPossiblePlotters(content); + const possibleConfigs = getPossibleConfigs(plotter); + + /* + console.log(`Request data:`); + console.log(requestData); + console.log(`possible plotters:`); + console.log(possiblePlotters); + console.log(`possible parameters:`); + console.log(possibleConfigs); + */ + + const returnStruct = [requestData, possiblePlotters, possibleConfigs]; + + return returnStruct; + }; + + const fetchDownload = (requestData, contentType) => { + const urlPrefix = ''; + + // override the 'merged' property to always request the merged version. + // requested the unmerged version only downloads the first image. + requestData[0] = Object.assign({}, requestData[0], { merged: true }); + + return new Promise((resolve, reject) => { + beat.experiments.utils.getPlotData( + // url_prefix + urlPrefix, + // spread out the request data to fill the next 3 spots + ...requestData, + // content type: png, jpeg, pdf + contentType, + // callback + (...args) => { + // resolve promise + resolve(...args); + } + ); + }); + }; + + // makes the call to the server, via some helper funcs found in the global namespace + const fetchRender = (requestData, containerId, onRenderCallback) => { + // the 'url_prefix' field found throughout the BEAT code is confusing, + // but it seems to always be an empty string now + const urlPrefix = '';//UrlService.getApiSegment().split('/').filter(s => s.length > 0).join('/'); + + // this func *cannot* be promisified! the callback is fired whenever the plot is re-rendered. + beat.experiments.utils.displayPlot( + // url_prefix + urlPrefix, + // element to append render to + document.querySelector(`#${containerId}`), + // spread out the request data to fill the next 3 spots + ...requestData, + // dont replace inner content + false, + // callback + (plotter, config, merged) => { + onRenderCallback(plotter, config, merged); + } + ); + }; + + // helper func to process a request to plot + const processDownload = (group, itemId, contentType) => { + const reqData = constructPlotInfo(group, itemId); + + return fetchDownload(reqData, contentType); + }; + + // helper func to process a request to plot + const processItem = (group, itemId, containerId, onRenderCallback) => { + const reqData = constructPlotInfo(group, itemId); + + fetchRender(reqData, containerId, onRenderCallback); + }; + + // used if we arent ready to directly service requests + const addItemToQueue = (group, itemId, containerId, onRenderCallback) => { + queue.push([ + group, + itemId, + containerId, + onRenderCallback + ]); + }; + + // called by ReportService, + // or by someone having this report-level info. + // processes the queue and sets the plotService state such that we dont need to use + // the queue after this + ps.processQueue = (rsPlotters, rsDefaultPlotters, rsPlotterParameters, rsReportNumber) => { + rsPlotters.forEach(p => ps.plotters.push(p)); + rsDefaultPlotters.forEach(dp => ps.defaultPlotters.push(dp)); + rsPlotterParameters.forEach(pp => ps.plotterParameters.push(pp)); + ps.reportNumber = rsReportNumber; + + noQueueNeeded = true; + const promises = queue.map(q => processItem(...q)); + queue.length = 0; + + return promises; + }; + + // chooses whether to add the plot request to a queue or service directly + // args: group, itemId, containerId, onRenderCallback + ps.addPlot = (...args) => noQueueNeeded ? processItem(...args) : addItemToQueue(...args); + + // args: group, itemId, contentType + ps.downloadPlot = (...args) => processDownload(...args); + + ps.getPlotter = getPlotter; + ps.getPlotterConfig = getPlotterConfig; + ps.getPossiblePlotters = getPossiblePlotters; + ps.getPossibleConfigs = getPossibleConfigs; + + //console.log(ps); + + return ps; +}]); diff --git a/beat/web/reports/static/reports/app/services/reportService.js b/beat/web/reports/static/reports/app/services/reportService.js new file mode 100644 index 0000000000000000000000000000000000000000..a5e98181aa6b7390f16fbd1b7f1909eec339d380 --- /dev/null +++ b/beat/web/reports/static/reports/app/services/reportService.js @@ -0,0 +1,145 @@ +/* + * 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/. + */ + +/* + * ReportService + * Desc: + * Consumes the "report" object from the API and digests it into helper + * funcs and report-wide info. Basically an adaptor & bootstrap-er. + */ +angular.module('reportApp').factory('ReportService', ['GroupsService', 'plotterFactory', 'PlotService', 'reportFactory', 'UrlService', 'ErrorService', function(GroupsService, plotterFactory, PlotService, reportFactory, UrlService, ErrorService){ + const rs = {}; + + rs.isAnonymous = undefined; + rs.isOwner = undefined; + rs.status = undefined; + rs.number = undefined; + rs.author = undefined; + rs.name = undefined; + + rs.plotters = []; + rs.defaultPlotters = []; + rs.plotterParameters = []; + + // processed the report data received from the server, + // and bootstraps the state of various services + rs.processReport = (report) => { + //console.log(report); + + // useful info about the app + rs.isAnonymous = report.anonymous; + rs.isOwner = report.is_owner; + rs.status = report.status; + rs.number = report.number; + rs.author = report.author; + rs.name = report.name.split('/').length > 1 ? report.name.split('/')[1] : null; + + // start up our GroupsService + GroupsService.loadGroups(report.content.groups); + // if the report should not change, + // add a nice layer of immutability by freezing the Group tree + // (see GroupsService for more info) + const isEditable = rs.isOwner && rs.status === 'editable' && !rs.isAnonymous; + GroupsService.setEditable(isEditable); + + //console.log(GroupsService.groups); + + // fetch all our plotter data + // these three fetches do not depend on eachother + const pPlotters = plotterFactory.getPlotters('') + .then(res => { + res.data.forEach(p => rs.plotters.push(p)); + }); + + const pDefaults = plotterFactory.getDefaultPlotters('') + .then(res => { + res.data.forEach(p => rs.defaultPlotters.push(p)); + }); + + const pParams = plotterFactory.getPlotterParameter('') + .then(res => { + res.data.forEach(p => rs.plotterParameters.push(p)); + }); + + // process the fetched plot info + return Promise.all([pPlotters, pDefaults, pParams]) + .then(() => PlotService.processQueue(rs.plotters, rs.defaultPlotters, rs.plotterParameters, rs.number)) + ; + }; + + // fetch the report data using either the by-name or by-number scheme, + // according to what URLService found + rs.fetchReport = () => { + const nameSeg = UrlService.getNameSegment(); + const numSeg = UrlService.getNumberSegment(); + + if(nameSeg){ + // the nameSeg is something like 'report/<user>/<report name>/' + let [user, reportId] = nameSeg.split('/').filter(s => s.length > 0).slice(1); + + return reportFactory.getReportInformation(user, reportId, '') + .then(res => rs.processReport(res.data)); + } else if(numSeg) { + // the numSeg is something like 'report/<report number>/' + let [number] = numSeg.split('/').filter(s => s.length > 0).slice(1); + + return reportFactory.getReportInformationFromNumber(number, '') + .then(res => rs.processReport(res.data)); + } else { + throw new Error('UrlService could not parse the current URL'); + } + }; + + // publish the report + rs.publishReport = (openSourceAlgs) => { + return reportFactory.publishReport( + '', + rs.author, + rs.name, + openSourceAlgs.length && openSourceAlgs.length > 0 ? data : undefined + ) + .catch(error => { + throw error; + }); + ; + }; + + // lock the report + rs.lockReport = () => { + return reportFactory.lockReport( + rs.author, + rs.name, + '' + ) + .catch(error => { + throw error; + }); + ; + }; + + rs.fetchReport() + .catch(e => ErrorService.logError(e, `Could not load report.`)); + + //console.log(rs); + + return rs; +}]); diff --git a/beat/web/reports/static/reports/app/services/urlService.js b/beat/web/reports/static/reports/app/services/urlService.js new file mode 100644 index 0000000000000000000000000000000000000000..f42d6af73be62de0ce0559909d5c5f0ac3578fda --- /dev/null +++ b/beat/web/reports/static/reports/app/services/urlService.js @@ -0,0 +1,100 @@ +/* + * 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/. + */ + +/* + * UrlService + * Desc: + * Helper functionality to generate URLs for the reports app + */ +angular.module('reportApp').factory('UrlService', [function(){ + // const path segments + const experimentSegment = 'experiments/'; + const blockSegment = 'algorithms/'; + const databaseSegment = 'databases/'; + const apiSegment = 'api/v1/'; + + // the protocol, address, and port number + let prefix = ''; + // the path to the current report, by username & report name + let reportByName; + // the path to the current report, by report number + let reportByNumber; + + // extracts info from jQuery's ajaxSettings current URL + const extractUsingCurrentUrl = () => { + const url = $.ajaxSettings.url; + const idxSplit = url.indexOf('reports'); + const path = url.slice(idxSplit).replace(/#\/?$/, ''); + + prefix = url.slice(0, idxSplit); + // find how many '/' are in path via splitting str on '/' + if(path.split('/').length === 3){ + // report number + reportByNumber = path; + } else { + // report user & name + reportByName = path; + } + }; + + const getPrefix = () => prefix; + + const getApiSegment = () => apiSegment; + const getNameSegment = () => reportByName; + const getNumberSegment = () => reportByNumber; + + const experimentPath = () => `${prefix}${experimentSegment}`; + const blockPath = () => `${prefix}${blockSegment}`; + const databasePath = () => `${prefix}${databaseSegment}`; + const apiPath = () => `${prefix}${apiSegment}`; + + const getExperimentUrl = (experimentName) => `${experimentPath()}${experimentName}/`; + const getBlockUrl = (blockName) => `${blockPath()}${blockName}/`; + const getDatabaseUrl = (databaseName) => `${databasePath()}${databaseName}/`; + const getApiUrl = (apiSubpath) => `${apiPath()}${apiSubpath}`; + + const getCompileRstUrl = () => `${getApiUrl(`${reportByName || reportByNumber}rst/`)}`; + const getRemoveExperimentUrl = () => `${getApiUrl(`${reportByName}remove/`)}`; + const getByNamePath = () => reportByName; + const getByNumberPath = () => reportByNumber; + const getExperimentListPath = () => experimentPath(); + + const isViewmode = () => reportByNumber ? true : false; + + extractUsingCurrentUrl(); + + return { + getExperimentUrl, + getBlockUrl, + getDatabaseUrl, + getCompileRstUrl, + getRemoveExperimentUrl, + getByNamePath, + getByNumberPath, + getApiSegment, + getNameSegment, + getNumberSegment, + getPrefix, + getExperimentListPath, + isViewmode + }; +}]); diff --git a/beat/web/reports/static/reports/css/style.css b/beat/web/reports/static/reports/css/style.css index 30cc23ef886cbbfb166f3758ae4acb09eafb89ed..8ced0afd2c0a74f9f652a9118be7f4716eeebb68 100644 --- a/beat/web/reports/static/reports/css/style.css +++ b/beat/web/reports/static/reports/css/style.css @@ -58,165 +58,6 @@ div.algorithms ul.algorithmslist li margin-left: 0; } - -div#space-for-report-items -{ - padding-top: 1em; -} - -div#space-for-report-items span.settings -{ - background-image: url("../../ui/images/icons/png/16/filled/gear.png"); - background-position: center center; - background-repeat: no-repeat; - background-size: 16px auto; - display: inline-block; - height: 16px; - vertical-align: middle; - width: 16px; - cursor: pointer; - border: 1px solid #DDDDDD; - margin-top: 3px; - float: left; -} - -div#space-for-report-items a.item_delete -{ - background-color: #880000; - /*background-image: linear-gradient(#ffa84c, #ff7b0d);*/ - background-image: linear-gradient(#ff0000, #880000); - border-color: #cc0000; - color: #FFFFFF; - line-height: 1.4em; - padding-bottom: 1px; - padding-left: 3px; - padding-right: 3px; - width: auto; - height: auto; - min-width: 5em; - float:right; - margin-bottom: 3px; -} - -div#space-for-report-items a.item_delete:hover -{ - background-color: #880000; - /*background-image: linear-gradient(#ff670f, #ff670f);*/ - background-image: linear-gradient(#cc0000, #880000); - border-color: #cc0000; -} - -div#space-for-report-items a.item_delete.number -{ - display:none; -} - -div#space-for-report-items a.item_delete.locked -{ - display:none; -} - -div#space-for-report-items a.item_delete.published -{ - display:none; -} - -div#space-for-report-items a.item_export -{ - background-color: #880000; - /*background-image: linear-gradient(#ffa84c, #ff7b0d);*/ - background-image: linear-gradient(#1975A3, #006699); - border-color: #00293D; - color: #FFFFFF; - line-height: 1.4em; - padding-bottom: 1px; - padding-left: 3px; - padding-right: 3px; - width: auto; - height: auto; - min-width: 5em; - float:right; - margin-bottom: 3px; - margin-right: 0.3em; -} - -div#space-for-report-items a.item_export:hover -{ - background-color: #880000; - /*background-image: linear-gradient(#ff670f, #ff670f);*/ - background-image: linear-gradient(#00476B, #00334C); - border-color: #00293D; -} - -div#space-for-report-items a.item_export.number -{ - display:none; -} - -div#space-for-report-items a.item_export.locked -{ - display:none; -} - -div#space-for-report-items a.item_export.published -{ - display:none; -} - - -div#space-for-report-items span.settings.locked -{ - display:none; -} - -div#space-for-report-items span.settings.number -{ - display:none; -} - - -div#space-for-report-items span.settings.published -{ - display:none; -} - -div#space-for-report-items table.report-table -{ - width: 100%; - border: 1px solid #DDDDDD; - text-align: center; -} - -div#space-for-report-items table.report-table th.tableitemspace -{ - padding-right: 1em; -} - -div#space-for-report-items table.report-table td.tableitemspace -{ - padding-right: 1em; -} - -div#space-for-report-items div.chart -{ - margin-bottom: 1.6em; -} - -div#space-for-report-items div.chart table -{ - margin: 0; - min-width: 100%; - text-align: center; - border: 1px solid #DDDDDD; -} - -div#space-for-report-items div.chart p -{ - text-align: center; - font-weight: bold; - margin-bottom: -5px; -} - div.reportInfo { padding-bottom: 1em; @@ -253,16 +94,6 @@ div.reportInfo div.reportHeader.published background-color: #4f85bb; } -div.reportInfo div.reportBody -{ - margin-bottom: 20px; -} - -div.reportInfo div.reportBody ul.menu2 -{ - margin-bottom: 0px; -} - div.report_commands { margin-bottom: 0px; @@ -380,213 +211,25 @@ div.report_commands div.report_buttons_bloc .publish_report:hover border-color: #4f85bb; } -div.report_lock .explanation_text -{ - font-weight: bold; -} - -div.report_lock .warnings -{ - color: #04B431; +.td_title > h3 { + margin-left: 20px; + margin-bottom: 0; } -div.report_lock .errors -{ - color: #FF0000; +.lastEdited { } -button#button-report_lock-ok -{ - font-weight: bold; - color: #2E2E2E; +.lastEditedAnimating { + animation-duration: 3s; + animation-name: reportSaved; } -button#button-report_lock-ok:hover -{ - background-color: #424242; - background-image: linear-gradient(#6E6E6E, #424242); - border-color: #2E2E2E; - color: #FFFFFF; -} +@keyframes reportSaved { + from { + background-color: #dff0d8; + } -button#button-report_lock-cancel -{ - font-weight: bold; - color: #2E2E2E; -} - -button#button-report_lock-cancel:hover -{ - background-color: #424242; - background-image: linear-gradient(#6E6E6E, #424242); - border-color: #2E2E2E; - color: #FFFFFF; -} - -div.report_publish .explanation_text -{ - font-weight: bold; -} - -div.report_publish .warnings -{ - color: #04B431; -} - -div.report_publish .errors -{ - color: #FF0000; -} - -div.reportBody a.button.remove_experiment -{ - background-color: #880000; - background-image: linear-gradient(#ff0000, #880000); - border-color: #cc0000; - color: #FFFFFF; - width: auto; - min-width: 2em; - -} - -div.reportBody a.button.remove_experiment:hover -{ - background-color: #880000; - background-image: linear-gradient(#cc0000, #880000); - border-color: #cc0000; -} - -div.reportBody span.experiment_alias_title -{ - width: 28.8em; - float: right; -} - -div.reportBody span.experiment_alias -{ - float: right; -} - -div.reportBody a.button.edit_alias -{ - background-color: #880000; - background-image: linear-gradient(#ff0000, #880000); - border-color: #cc0000; - color: #FFFFFF; - width: auto; - min-width: 2em; - -} - -div.reportBody a.button.edit_alias:hover -{ - background-color: #880000; - background-image: linear-gradient(#cc0000, #880000); - border-color: #cc0000; -} - -div.reportBody a.button.edit_alias.setalias -{ - background-color: #60B044; - background-image: linear-gradient(#8ADD6D, #60B044); - border-color: #5CA941; - color: #FFFFFF; - width: auto; - min-width: 2em; -} - -div.reportBody a.button.edit_alias.setalias:hover -{ - background-color: #5BA740; - background-image: linear-gradient(#82DB63, #5BA740); - border-color: #4A993E; -} - - -div.reportBody span.experiment_alias a -{ - margin-right: 0.5em; -} - -div.reportBody input.experiment_alias -{ - width: 35em; - float: right; -} -div.reportBody .input-disabled -{ - background-color:#EBEBE4; - border:1px solid #ABADB3; - padding:2px 1px; -} - -div.report_remove_experiment .explanation_text -{ - font-weight: bold; -} - -div.report_remove_experiment .warnings -{ - color: #04B431; -} - -div.report_remove_experiment .errors -{ - color: #FF0000; -} - -button#button-report_publish-ok -{ - font-weight: bold; - color: #2E2E2E; -} - -button#button-report_publish-ok:hover -{ - background-color: #424242; - background-image: linear-gradient(#6E6E6E, #424242); - border-color: #2E2E2E; - color: #FFFFFF; -} - -button#button-report_publish-cancel -{ - font-weight: bold; - color: #2E2E2E; -} - -button#button-report_publish-cancel:hover -{ - background-color: #424242; - background-image: linear-gradient(#6E6E6E, #424242); - border-color: #2E2E2E; - color: #FFFFFF; -} - -button#button-report_remove_experiment-ok -{ - font-weight: bold; - color: #2E2E2E; -} - -button#button-report_remove_experiment-ok:hover -{ - background-color: #424242; - background-image: linear-gradient(#6E6E6E, #424242); - border-color: #2E2E2E; - color: #FFFFFF; -} - -button#button-report_remove_experiment-cancel -{ - font-weight: bold; - color: #2E2E2E; -} - -button#button-report_remove_experiment-cancel:hover -{ - background-color: #424242; - background-image: linear-gradient(#6E6E6E, #424242); - border-color: #2E2E2E; - color: #FFFFFF; + to { + background-color: white; + } } diff --git a/beat/web/reports/static/reports/test/report-spec.js b/beat/web/reports/static/reports/test/report-spec.js index 93df73d7b8ea98608ccf19a09f0f86d435bb4442..aee6bb81907e3951d82b4df3246366f3b7dc2b5c 100644 --- a/beat/web/reports/static/reports/test/report-spec.js +++ b/beat/web/reports/static/reports/test/report-spec.js @@ -1,6 +1,30 @@ // general tests for the reports app describe('reports app', function(){ + // contain helpers for browser.wait + const until = protractor.ExpectedConditions; + // just use enter.perform() to send the enter key + const enter = browser.actions().sendKeys(protractor.Key.ENTER); + // since angular isnt configured correctly for protractor, + // dont make protractor wait for angular browser.ignoreSynchronization = true; + // just to make sure the window is maximized during these tests. + // helps with button clicking & such + browser.driver.manage().window().maximize(); + + // login to the default user ('user') once before running all these tests + beforeAll(function(){ + browser.get('http://localhost:8000/login/?next=/'); + //browser.findElement(by.partialLinkText('Sign-in')).click(); + browser.findElement(by.id('id_username')).sendKeys('user'); + browser.findElement(by.id('id_password')).sendKeys('user'); + browser.findElement(by.partialButtonText('Sign-in')).click(); + return browser.wait(function(){ + return browser.getCurrentUrl().then(function(url){ + const rxUserLoggedIn = /events\/user\//; + return rxUserLoggedIn.test(url); + }); + }); + }); // if there's an error in the web browser's console, // fail the test and print the error @@ -18,50 +42,31 @@ describe('reports app', function(){ console.log(`logs: ${util.inspect(failingLogs)}`); } }); - }) + }); // /reports describe('home', function(){ beforeEach(function(){ - browser.get('http://localhost:8000/reports'); + browser.get('http://localhost:8000/reports/user'); }); it('should load', function(){ - expect(browser.getTitle()).toEqual('BEAT - Public Reports'); - }); - - it('should have no reports', function(){ - let noReportsText = browser.findElement(by.className('not-found')); - expect(noReportsText.getText()).toBe('No report found'); + expect(browser.getTitle()).toEqual("BEAT - user's Reports"); }); }); // /reports/user describe('home for the test user', function(){ - // login to the default user ('user') once before running all these tests - beforeAll(function(){ - browser.get('http://localhost:8000/login/?next=/'); - //browser.findElement(by.partialLinkText('Sign-in')).click(); - browser.findElement(by.id('id_username')).sendKeys('user'); - browser.findElement(by.id('id_password')).sendKeys('user'); - browser.findElement(by.partialButtonText('Sign-in')).click(); - return browser.wait(function(){ - return browser.getCurrentUrl().then(function(url){ - const rxUserLoggedIn = /events\/user\//; - return rxUserLoggedIn.test(url); - }); - }); - }); // go to user's reports page before each test - beforeEach(function(){ + beforeAll(function(){ browser.get('http://localhost:8000/reports/user/'); }); // before adding a report, there shouldn't be any - it('should have no reports', function(){ - let noReportsText = browser.findElement(by.className('not-found')); - expect(noReportsText.getText()).toBe('No report found'); + it('should not have the "user/test" report', function(){ + let noReportsText = browser.findElement(by.css('.name > a')); + expect(noReportsText.getText()).not.toBe('user/test'); }); // create a report @@ -87,4 +92,865 @@ describe('reports app', function(){ expect(browser.getTitle()).toBe('BEAT - Report'); }); }); + + // go to the experiments page and add up to 5 already-ran experiments + describe('adding experiments to the "test" report', function(){ + // go to experiments page + beforeAll(function(){ + browser.get('http://localhost:8000/experiments/user/'); + }); + + it('should show the experiments list page', function(){ + expect(browser.getTitle()).toEqual("BEAT - user's Experiments"); + }); + + it('should list successfully-ran experiments accessible by user', function(){ + expect(browser.isElementPresent(by.css('.Done'))).toBeTruthy(); + }); + + it('should add up to the first 5 experiments to the "test" report', function(){ + let finishedExpTableRows = element.all(by.css('.Done')); + let addButton = element(by.css('#add-to-report')); + expect(addButton.getAttribute('disabled')).toBe('true'); + + let fiveRows = finishedExpTableRows.filter((r, i) => i < 5); + + fiveRows + .then(rs => Promise.all(rs.map(r => r.element(by.css('.report-checkbox')).element(by.css('input')).click()))) + .then(() => { + browser.wait(until.elementToBeClickable(addButton), 5000, 'Button still isnt clickable!'); + return browser.executeScript('arguments[0].click();', addButton.getWebElement()); + }) + .then(() => browser.wait(until.presenceOf(element(by.css('.modal'))), 5000, 'Element taking too long to appear in the DOM')) + .then(() => element(by.css('.chosen-single')).click()) + .then(() => element(by.css('.chosen-results')).element(by.css('.active-result')).click()) + .then(() => { + let submitButton = element(by.buttonText('Add')); + return submitButton.click(); + }) + .then(() => browser.wait(until.presenceOf(element(by.buttonText("View Report"))), 5000)) + .then(() => { + let headerText = element(by.css('.report-results > h5')); + expect(headerText.getText()).toContain('Successfully added'); + + return browser.get('http://localhost:8000/reports/user/test/'); + }) + .then(() => { + expect(element.all(by.css('#experiment-list-test > tbody > tr')).count()).toBeGreaterThan(0); + }) + ; + }); + }); + + // create 2 groups, 'group1' & 'group2' + describe('creating groups', function(){ + let newGroupInput = element(by.css('#createNewGroupInput')); + + it('should create the "group1" group using the enter key', function(){ + newGroupInput.sendKeys('group1') + .then(() => enter.perform()) + .then(() => browser.wait(until.textToBePresentInElementValue(element(by.css('#createNewGroupInput')), ''), 1000)) + .then(() => expect(element.all(by.css('#groupsLayout > div')).count()).toBe(1)) + ; + }); + + it('should create the "group2" group using the "+" button', function(){ + newGroupInput.sendKeys('group2') + .then(() => element(by.css('#space-for-report-items + div button')).click()) + .then(() => browser.wait(until.textToBePresentInElementValue(element(by.css('#createNewGroupInput')), ''), 1000)) + .then(() => expect(element.all(by.css('#groupsLayout > div')).count()).toBe(2)) + ; + }); + }); + + // make sure the initial report layouts page is correct + describe('report page state with <6 experiments & two groups', function(){ + describe('header block', function(){ + const header = element(by.css('.col-sm-12 > p.bs-callout.bs-callout-danger')); + + it('has 4 labels', function() { + expect(header.all(by.tagName('br')).count()).toBe(4); + }); + + it('shows the unique report id', function() { + expect(header.element(by.css('.fa-arrow-circle-right + a')).getAttribute('href')).toMatch(/\/reports\/[0-9]+/); + }); + + it('shows the created date', function() { + expect(header.element(by.css('.fa-calendar-o + strong')).getText()).toMatch(/.+ago/); + }); + + it('shows the "last edited" date', function() { + expect(header.element(by.css('.fa-calendar-o + strong + br + .fa-calendar-o + strong')).getText()).toMatch(/.+ago/); + }); + + it('shows that the report is editable', function() { + expect(header.element(by.css('.fa-warning + strong')).getText()).toBe('Editable'); + }); + }); + + describe('documentation panel', function(){ + it('shows the empty warning', function() { + expect(element(by.css('#description-display > div')).getAttribute('class')).toContain('alert-warning'); + }); + + it('has an "Add" button', function() { + expect(element(by.css('#btn-edit-doc > i')).getAttribute('class')).toContain('fa-edit'); + }); + }); + + describe('experiments list panel', function(){ + describe('table', function(){ + const table = element(by.css('#experiment-list-test')); + + it('has 8 columns', function(){ + const cols = table.all(by.css('thead th')); + expect(cols.count()).toBe(8); + + expect(cols.get(0).getAttribute('class')).toBe('delete'); + expect(cols.get(1).getAttribute('class')).toBe('attestation'); + expect(cols.get(2).getAttribute('class')).toBe('privacy'); + expect(cols.get(3).getAttribute('class')).toBe('status'); + expect(cols.get(4).getAttribute('class')).toBe('date'); + expect(cols.get(5).getText()).toBe('Name'); + expect(cols.get(6).getAttribute('class')).toBe('datasets'); + expect(cols.get(7).getAttribute('class')).toBe('analyzers'); + }); + + it('has 5 or less rows', function() { + const rows = table.all(by.css('tbody > tr')); + expect(rows.count()).toBeLessThan(6); + }); + + it('has a "Remove Selected Experiments" button', function() { + const b = element(by.buttonText('Remove Selected Experiments')); + expect(b.getAttribute('disabled')).toBeDefined(); + }); + }); + }); + + describe('report content block', function(){ + const groupsLayout = element(by.css('#groupsLayout')); + + describe('group1 container header', function(){ + const header = element(by.css('#group1-heading')); + const children = element.all(by.css('#group1-heading > h4 > *')); + + it('has 5 children: a collapse link, name widget, a button group, and the add items menu', function(){ + expect(children.count()).toBe(4); + }); + + describe('collapse link', function(){ + it('toggles "#collapse-group1"', function(){ + expect(children.get(0).getAttribute('href')).toContain('#collapse-group1'); + }); + }); + + describe('group name widget', function(){ + const widgetEls = children.get(1).all(by.tagName('span')); + const label = widgetEls.get(0); + const button = widgetEls.get(1); + + it('has value "group1"', function(){ + expect(label.getText()).toBe('group1'); + }); + + describe('edit button', function(){ + it('has the pencil glyphicon', function(){ + expect(button.getAttribute('class')).toBe('glyphicon glyphicon-pencil'); + }); + + it('is clickable', function(){ + // no direct way to check if clickable... + // so make sure that its not disabled & its displayed instead + expect(button.getAttribute('disabled')).toBeNull(); + expect(button.isDisplayed()).toBeTruthy(); + }); + }); + + }); + + describe('button group', function(){ + const grp = children.get(2); + const btnChildren = grp.all(by.className('btn')); + + it('is a btn group', function(){ + expect(grp.getAttribute('class')).toContain('btn-group'); + }); + + it('is an action buttons', function(){ + expect(grp.getAttribute('class')).toContain('action-buttons'); + }); + + it('has btn children', function() { + const btnChildren = grp.all(by.className('btn')); + expect(btnChildren.count()).toBe(2); + }); + + it('has a button to delete a group with a red "X"', function(){ + expect(btnChildren.get(0).getAttribute('title')).toBe('Delete Group'); + expect(btnChildren.get(0).element(by.css('i')).getAttribute('class')).toContain('fa-times'); + }); + + it('has a button to drag & sort the group with a 4-directional arrow', function(){ + expect(btnChildren.get(1).getAttribute('title')).toBe('Drag to re-order group'); + expect(btnChildren.get(1).element(by.css('i')).getAttribute('class')).toContain('fa-arrows'); + }); + }); + + describe('add items menu', function(){ + const grp = children.get(3); + const buttons = grp.all(by.tagName('button')); + + it('has 3 buttons', function(){ + expect(buttons.count()).toBe(3); + }); + + it('is all disabled', function(){ + expect(buttons.filter(b => b.getAttribute('disabled').then(d => d)).count()).toBe(3); + }); + + it('has a button to add plots', function(){ + const el = element(by.buttonText('Add Plot')); + expect(el).toBeDefined(); + }); + + it('has a button to add tables', function(){ + const el = element(by.buttonText('Add Table')); + expect(el).toBeDefined(); + }); + + it('has a button to add text blocks', function(){ + const el = element(by.buttonText('Add Text Block')); + expect(el).toBeDefined(); + }); + }); + }); + + describe('group1 experiments panel', function(){ + const header = element(by.css('#group1-explist-heading')); + const body = element(by.css('#collapse-group1-explist')); + + it('is visible', function(){ + expect(header.isDisplayed()).toBeTruthy(); + expect(body.isDisplayed()).toBeTruthy(); + }); + + it('is empty', function(){ + expect(element.all(by.css('#collapse-group1-explist > .panel-body > *')).count()).toBe(0); + }); + + describe('header button', function(){ + const button = element(by.css('#group1_exp_add_dropdown')); + + it('is non-disabled', function(){ + expect(button.getAttribute('disabled')).toBeNull(); + }); + + it('has the text "Add Experiment"', function(){ + expect(button.getText()).toBe('Add Experiment'); + }); + }); + }); + + describe('group1 content panel', function(){ + it('doesnt exist', function(){ + expect(element.all(by.css('#collapse-group1 > *')).count()).toBe(1); + }); + }); + }); + }); + + describe('group experiments panel management', function(){ + const g1ExpPanel = element(by.css('#collapse-group1 > .panel-body > .panel')); + const addButton = element(by.css('#group1_exp_add_dropdown')); + + // add back the exp to group1 + afterAll(function(){ + const parent = addButton.element(by.xpath('..')); + + parent.getAttribute('class') + .then(cls => cls.includes('open') || addButton.click()) + .then(() => { + // make sure list opened correctly & add an exp + const list = parent.element(by.css('ul')); + const first = list.element(by.tagName('li')); + browser.wait(until.elementToBeClickable(first), 5000, 'First add button isnt becoming clickable') + + return first.click(); + }); + }); + + it('adds 1 experiment to group1', function(){ + // open add exp menu + addButton.click() + .then(() => { + // make sure list opened correctly & add an exp + const parent = addButton.element(by.xpath('..')); + expect(parent.getAttribute('class')).toContain('open'); + const list = parent.element(by.css('ul')); + const lis = list.all(by.css('li')); + expect(lis.count()).toBeGreaterThan(0); + + return lis.get(0).element(by.css('a')).click(); + }) + .then(() => { + // check state of group after adding exp + const analyzer = element(by.css('#group1-explist-heading > h4 > i')); + expect(analyzer.isDisplayed()).toBeTruthy(); + expect(analyzer.getText()).not.toBe(''); + + const expsBody = element(by.css('#collapse-group1-explist > .panel-body')); + expect(expsBody.getText()).not.toBe(''); + }); + }); + + describe('group1 experiment table state with 1 experiment', function(){ + const expsTable = element(by.css('#collapse-group1-explist > .panel-body > table')); + const headers = expsTable.all(by.css('thead > tr > th')); + const row = expsTable.element(by.css('tbody > tr')); + + describe('column layout', function(){ + it('has 4 columns', function(){ + expect(headers.count()).toBe(4); + }); + + it('has an empty col for rm exp buttons', function() { + expect(headers.get(0).getText()).toBe(''); + }); + + it('has experiment names col', function() { + expect(headers.get(1).getText()).toBe('Experiment'); + }); + + it('has dbs/protocols col', function() { + expect(headers.get(2).getText()).toBe('Databases/Protocols'); + }); + + it('has aliases col', function() { + expect(headers.get(3).getText()).toBe('Alias'); + }); + }); + + describe('row layout', function(){ + const cells = row.all(by.css('td')); + + it('has 4 entries', function() { + expect(cells.count()).toBe(4); + }); + + it('has a delete button in the first row', function() { + expect(cells.get(0).element(by.tagName('span')).getAttribute('class')).toContain('btn-delete'); + }); + + it('has the experiment name and a link to the experiment in the second row', function() { + const a = cells.get(1).element(by.css('a')); + expect(a.getText()).not.toBe(''); + }); + + it('has a list of formatted dbs & protocols in the third row', function() { + const els = cells.get(2).all(by.css('span')); + expect(els.count()).toBeGreaterThan(0); + expect(els.get(0).element(by.css('a')).getText()).toMatch(/\S+@\S+/); + }); + + it('has an editable alias input in the fourth row', function() { + const input = cells.get(3).element(by.css('input')); + const pExpName = cells.get(1).element(by.css('a')).getText(); + pExpName + .then(expName => { + const lastSeg = expName.split('/').filter(s => s.length > 0).pop(); + expect(input.getAttribute('value')).toBe(lastSeg); + expect(input.getAttribute('disabled')).toBeNull(); + }); + }); + + }); + }); + + describe('removing 1 experiment', function(){ + beforeAll(function(){ + const rmExpButton = element(by.css('#collapse-group1-explist tbody .btn-delete')); + return rmExpButton.click(); + }); + + it('removes the exp table', function(){ + const explistPanel = element(by.css('#collapse-group1-explist > .panel-body')); + expect(explistPanel.getText()).toBe(''); + }); + + it('removes the analyzer tag', function() { + expect(element.all(by.css('#group1-explist-heading > h4 > *')).count()).toBe(2); + }); + + it('lets the user add any experiment again', function() { + const expRowsCount = element.all(by.css('#experiment-list-test tbody > tr')).count(); + + Promise.all([expRowsCount, element(by.css('#group1_exp_add_dropdown')).click()]) + .then(([expCount]) => + Promise.all([ + expCount, + element('#group1_exp_add_dropdown').element(by.xpath('..')).element(by.css('ul')).all(by.css('li')).count() + ]) + ) + .then(([expCount, optsCount]) => { + expect(expCount).toBe(optsCount); + }); + }); + }); + }); + + // should have 1 exp in group + describe('group report items panel management', function(){ + const addPlot = () => { + const addButton = element(by.partialButtonText('Add Plot')); + return addButton.click() + .then(() => { + const parent = addButton.element(by.xpath('..')); + const firstPlotLi = parent.element(by.css('ul > li')); + return firstPlotLi.click(); + }) + ; + }; + + const addTable = () => { + const addButton = element(by.partialButtonText('Add Table')); + return addButton.click(); + }; + + const addTextBlock = () => { + const addButton = element(by.partialButtonText('Add Text Block')); + return addButton.click(); + }; + + afterAll(function() { + // add a plot, table, & text block + return addPlot() + .then(() => addTable()) + .then(() => addTextBlock()) + ; + }); + + describe('plot items', function(){ + describe('adding a plot', function() { + beforeAll(function() { + // add plot here + return addPlot(); + }); + + it('adds a plot', function() { + const plotContainer = element(by.css('#collapse-group1_plot_0')); + expect(plotContainer.isDisplayed()).toBeTruthy(); + }); + + it('eventually renders the plot', function() { + const plotContainer = element(by.css('#collapse-group1_plot_0 > .panel-body > div')); + + browser.wait(until.presenceOf(plotContainer.element(by.css('img'))), + 5000, 'Plot render hasnt been inserted in Angular for 5s'); + + const img = plotContainer.element(by.css('img')); + expect(img.isDisplayed()).toBeTruthy(); + }); + }); + + describe('removing a plot', function() { + beforeAll(function() { + // rm plot here + const delButton = element(by.css('#group1_plot_0-heading .btn-delete')); + + return delButton.click(); + }); + + it('removes the plot and has no report items', function() { + const groupBodyChildren = element(by.model('group._reportItems')).all(by.css('*')); + expect(groupBodyChildren.count()).toBe(0); + }); + }); + }); + + describe('table items', function() { + describe('adding a table', function() { + beforeAll(function() { + // add table here + return addTable(); + }); + + it('adds a table', function() { + const tableContainer = element(by.css('#collapse-group1_table_0 .panel-body')); + expect(tableContainer.isDisplayed()).toBeTruthy(); + }); + + describe('layout', function() { + const panelHeaderButtonGroup = element.all(by.css('#group1_table_0-heading > h4 > .btn-group')).get(1); + const colsButton = panelHeaderButtonGroup.element(by.css('#group1_table_0_columnSelector')); + const precButton = panelHeaderButtonGroup.element(by.css('#group1_table_0-precision')); + const tcsvButton = panelHeaderButtonGroup.element(by.buttonText('Toggle CSV View')); + + const table = element(by.css('#collapse-group1_table_0 table')); + const headers = table.all(by.css('thead > tr > th')); + const rows = table.all(by.css('tbody > tr')); + + it('has at least one column for the name of the experiments', function() { + expect(headers.count()).toBeGreaterThan(0); + expect(headers.get(0).element(by.css('a')).getText()).toContain('Experiment'); + }); + + it('has a default precision of 10', function() { + expect(precButton.getText()).toBe('Float Precision: 10'); + }); + + it('has enabled button for choosing columns', function() { + expect(colsButton.getAttribute('disabled')).toBeNull(); + }); + + it('has enabled button for choosing precision', function() { + expect(precButton.getAttribute('disabled')).toBeNull(); + }); + + it('has enabled button for toggling the csv view', function() { + expect(tcsvButton.getAttribute('disabled')).toBeNull(); + }); + + it('has 1 row with the name col matching the alias', function() { + const pName = rows.get(0).element(by.css('td')).getText(); + const pCurrAlias = element(by.css('#collapse-group1-explist table > tbody input')).getAttribute('value'); + Promise.all([pName, pCurrAlias]) + .then(([name, currAlias]) => expect(name).toBe(currAlias)); + }); + }); + }); + + describe('removing a table', function() { + beforeAll(function() { + // rm table here + const delButton = element(by.css('#group1_table_0-heading .btn-delete')); + + return delButton.click(); + }); + + it('removes the table and has no report items', function() { + const groupBodyChildren = element(by.model('group._reportItems')).all(by.css('*')); + expect(groupBodyChildren.count()).toBe(0); + }); + }); + }); + + describe('text block items', function() { + describe('adding a text block', function() { + beforeAll(function(){ + // add text block here + return addTextBlock(); + }); + + it('added a text block to the group', function() { + const textContainer = element(by.css('#collapse-group1_text_0 .panel-body')); + expect(textContainer.isDisplayed()).toBeTruthy(); + }); + + it('is blank/empty', function() { + const emptyContainer = element(by.css('#collapse-group1_text_0 > .panel-body > .row > .col-sm-10 > div')); + expect(emptyContainer.getText()).toBe(''); + }); + + it('shows the "edit" button', function() { + const editButton = element(by.css('#collapse-group1_text_0 > .panel-body > .row > .col-sm-2 > div > a')); + expect(editButton.getText()).toContain('Edit'); + }); + }); + + describe('removing a text block', function() { + beforeAll(function() { + // rm text here + const delButton = element(by.css('#group1_text_0-heading .btn-delete')); + + return delButton.click(); + }); + + it('removes the text and has no report items', function() { + const groupBodyChildren = element(by.model('group._reportItems')).all(by.css('*')); + expect(groupBodyChildren.count()).toBe(0); + }); + }); + }); + }); + + describe('collapse functionality', function() { + // why are there all these 'sleep' calls before expanding the collapsed panel? + // cuz, for some reason, theres a slight delay in it changing to the collapsed state in the DOM, + // and it being available for being expanded + describe('of groups', function() { + const collapseButton = element(by.css('#group1-heading > h4 > a')); + + it('should collapse', function() { + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('collapsed')) + ; + }); + + it('should expand', function() { + browser.sleep(500); + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('')) + ; + }); + }); + + describe('of experiments panel', function() { + const collapseButton = element(by.css('#group1-explist-heading > h4 > a')); + + it('should collapse', function() { + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('collapsed')) + ; + }); + + it('should expand', function() { + browser.sleep(500); + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('')) + ; + }); + }); + + describe('of plots', function() { + const collapseButton = element(by.css('#group1_plot_0-heading > h4 > a')); + + it('should collapse', function() { + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('collapsed')) + ; + }); + + it('should expand', function() { + browser.sleep(500); + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('')) + ; + }); + }); + + describe('of tables', function() { + const collapseButton = element(by.css('#group1_table_0-heading > h4 > a')); + + it('should collapse', function() { + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('collapsed')) + ; + }); + + it('should expand', function() { + browser.sleep(500); + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('')) + ; + }); + }); + + describe('of text blocks', function() { + const collapseButton = element(by.css('#group1_text_0-heading > h4 > a')); + + it('should collapse', function() { + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('collapsed')) + ; + }); + + it('should expand', function() { + browser.sleep(500); + browser.executeScript('arguments[0].click();', collapseButton.getWebElement()) + .then(() => expect(collapseButton.getAttribute('class')).toBe('')) + ; + }); + }); + }); + + describe('group experiment panel functionality', function() { + const firstRowCells = element(by.css('#collapse-group1-explist tbody > tr')).all(by.css('td')); + + describe('alias field', function() { + const alias = element(by.css('#collapse-group1-explist table > tbody input')); + const strToType = 'asdf'; + + it('lets you change the alias', function() { + alias.sendKeys(strToType) + .then(() => expect(alias.getAttribute('value')).toContain(strToType)) + ; + }); + + it('changed the experiment name in the first row in the table item', function(){ + const nameEl = element(by.css('#collapse-group1_table_0 td')); + + expect(nameEl.getText()).toContain(strToType); + }); + }); + }); + + describe('group report items functionality', function() { + describe('plot', function() { + const plotContainer = element(by.css('#group1_plot_0-render')); + }); + + describe('table', function() { + const panelHeaderButtonGroup = element.all(by.css('#group1_table_0-heading > h4 > .btn-group')).get(1); + const colsButton = panelHeaderButtonGroup.element(by.css('#group1_table_0_columnSelector')); + const precButton = panelHeaderButtonGroup.element(by.css('#group1_table_0-precision')); + const tcsvButton = panelHeaderButtonGroup.element(by.buttonText('Toggle CSV View')); + + const table = element(by.css('#collapse-group1_table_0 table')); + const headers = table.all(by.css('thead > tr > th')); + const rows = table.all(by.css('tbody > tr')); + + it('changes columns to include "total_execution_time"', function() { + const b = element(by.partialButtonText('Choose Columns')); + b.click() + .then(() => { + const execTimeCol = element.all(by.css('#colSelectorId > .dropdown-menu > form > fieldset > div')) + .filter(e => e.element(by.tagName('input')).getAttribute('value').then(v => v == 'total_execution_time')) + .first() + ; + + return execTimeCol.click(); + }) + .then(() => element(by.buttonText('Save Column Choices')).click()) + .then(() => { + const timeCols = element.all(by.css('#collapse-group1_table_0 thead a')) + .filter(a => a.getText().then(t => t.includes('total_execution_time'))); + + return expect(timeCols.count()).toBe(1); + }) + ; + }); + + it('changes precision from 10 to 7', function() { + const b = element(by.partialButtonText('Float Precision')); + b.click() + .then(() => element(by.linkText('7')).click()) + .then(() => expect(element(by.partialButtonText('Float Precision')).getText()).toContain('7')) + .then(() => maps = element.all(by.css('#collapse-group1_table_0 tbody tr td')) + .map(td => td.getText())) + .then(txts => txts + .filter(t => !isNaN(t) && t.includes('.')) + .forEach(t => expect(t.split('.')[1].length).toBeLessThan(8)) + ) + ; + }); + }); + + /* + * the 'x' cancels tests from running + * unfortunately there seems to be a selenium/protractor/webdriver bug + * with looking into the codemirror editor (`ui-codemirror`) + * none of the codemirror HTML elements are registering with + * selenium/protractor as being visible + */ + xdescribe('text block', function() { + const getHtmlContainer = () => element(by.css('#collapse-group1_text_0 > .panel-body > .row > .col-sm-10 > div')); + const getEditButton = () => element(by.css('#collapse-group1_text_0')).element(by.partialLinkText('Edit')); + + const text = '1. asdf'; + + describe('edit mode', function() { + it('turns on after clicking the edit button', function(){ + getEditButton().click() + .then(() => { + browser.wait(until.presenceOf(element(by.css('.CodeMirror'))), 5000, 'Nope'); + const editor = element.all(by.css('.CodeMirror')); + expect(editor.count()).toBeGreaterThan(0); + }) + ; + }) + + it('has cancel button', function() { + expect(element(by.css('#collapse-group1_text_0')).element(by.partialLinkText('Cancel')) + .isDisplayed()).toBeTruthy(); + }); + + it('has save button', function() { + expect(element(by.css('#collapse-group1_text_0')).element(by.partialLinkText('Save')) + .isDisplayed()).toBeTruthy(); + }); + + it('lets you edit the RST', function() { + const textArea = element(by.css('.CodeMirror textarea')); + expect(textArea.isDisplayed()).toBeTruthy(); + + textArea.sendKeys(text) + .then(() => expect(textArea.getAttribute('value')).toBe(text)) + ; + }); + }); + + describe('cancelling edits', function() { + it('moves to view mode', function() { + element(by.css('#collapse-group1_text_0')).element(by.partialLinkText('Cancel')) + .click() + .then(() => expect(getEditButton().isDisplayed()).toBeTruthy()) + }); + + it('didnt save the typed RST', function(){ + expect(getHtmlContainer().getText()).toBe(''); + }); + }); + + describe('submitting edits', function() { + beforeAll(function(){ + getEditButton().click() + .then(() => element(by.css('#collapse-group1_text_0 textarea')).sendKeys(text)) + .then(() => element(by.css('#collapse-group1_text_0')).element(by.partialLinkText('Save')).click()) + .then(() => browser.wait(until.presenceOf(element(by.css('#collapse-group1_text_0 ol.arabic.simple'))), 5000, 'HTML didnt render correctly')) + ; + }); + + it('renders the text correctly', function() { + expect(getHtmlContainer().isDisplayed()).toBeTruthy(); + + expect(getHtmlContainer().getText()).toBe( + `<ol class="arabic simple"> + <li>asdf</li> + </ol>` + ); + }); + }); + }); + }); + + // need to save report before doing these + describe('links outside the report page', function(){ + beforeAll(function(){ + const saveButton = element(by.css('#save-button')); + + return browser.executeScript('arguments[0].click();', saveButton.getWebElement()); + }); + + afterEach(function(){ + return browser.get('http://localhost:8000/reports/user/test/'); + }); + + const firstRowCells = element(by.css('#collapse-group1-explist tbody > tr')).all(by.css('td')); + const link1 = firstRowCells.get(1).element(by.css('a')); + const link2 = firstRowCells.get(2).element(by.css('a')); + + describe('experiment link', function() { + it('links to a valid experiment', function() { + Promise.all([link1.getText(), link1.click()]) + .then(([expName]) => expect(browser.getTitle()).toBe(`BEAT - ${expName}`)) + ; + }); + }); + + describe('database~protocol link', function() { + afterAll(function(){ + return browser.navigate().back(); + }); + + it('links to a valid database~protocol', function() { + Promise.all([link2.getText(), link2.click()]) + .then(([dbProtName]) => { + expect(browser.getTitle()).toBe(`BEAT - ${dbProtName.split('@')[0]}`) + }) + ; + }); + }); + }); }); diff --git a/beat/web/reports/templates/reports/dialogs/report_publish.html b/beat/web/reports/templates/reports/dialogs/report_publish.html deleted file mode 100644 index 5019d7770872948345e16fa00f1cfe6975227cbb..0000000000000000000000000000000000000000 --- a/beat/web/reports/templates/reports/dialogs/report_publish.html +++ /dev/null @@ -1,45 +0,0 @@ -{% 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 %} -<div id="{{ dialog_id }}" class="report_publish" title="Report" style="display:none"> - <p class="explanation">Publishing a report</p> - - - <div class="explanation_text"> - <p class="experiments">1) Publishing your report will make it accessible to anyone.</p> - <p>2) Your report will not be editable anymore.</p> - <p>Do you wish to proceed?</p> - <div class="algorithms" style="display:none"> - <p class="algorithms_text">The following algorithms will become public. - Select those that will become open-source:</p> - <ul class="algorithmslist"></ul> - </div> - </div> - <div class="warnings" style="display:none"> - <p class="experiments">Sucessfully Published the report</p> - <ul class="experimentslist"></ul> - </div> - - <div class="errors" style="display:none"> - <p class="errors">Some errors occured while attempting to publish the report:</p> - <ul class="errorslist"></ul> - </div> -</div> diff --git a/beat/web/reports/templates/reports/dialogs/report_remove_experiment.html b/beat/web/reports/templates/reports/dialogs/report_remove_experiment.html deleted file mode 100644 index 04f837d2ae2fa97f3ebe71169ca86bac21ccc3a1..0000000000000000000000000000000000000000 --- a/beat/web/reports/templates/reports/dialogs/report_remove_experiment.html +++ /dev/null @@ -1,41 +0,0 @@ -{% 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 %} -<div id="{{ dialog_id }}" class="report_remove_experiment" title="Report" style="display:none"> - <p class="explanation">Remove experiment from report</p> - - - <div class="explanation_text"> - <p class="experiments"> - 1) This will permanently remove the experiment from your current report and reload the report.</p> - <p>2) Unsaved modifications will be lost. If you have modifications, click on "Cancel" and "Save" the report first.</p> - <p>Do you wish to proceed?</p> - </div> - <div class="warnings" style="display:none"> - <p class="experiments">Sucessfully removed the experiment</p> - <ul class="experimentslist"></ul> - </div> - - <div class="errors" style="display:none"> - <p class="errors">Some errors occured while attempting to remove the experiment:</p> - <ul class="errorslist"></ul> - </div> -</div> diff --git a/beat/web/reports/templates/reports/panels/actions.html b/beat/web/reports/templates/reports/panels/actions.html index 2994f3698cc8e69171b75bb46bf80d47e8042a73..823d722c81f164b3b71c2af04373e4465555447a 100644 --- a/beat/web/reports/templates/reports/panels/actions.html +++ b/beat/web/reports/templates/reports/panels/actions.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 %} @@ -39,12 +39,20 @@ {% 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" savereportitems><i class="fa fa-floppy-o fa-lg"></i></a> - <a class="btn btn-default btn-report" data-toggle="tooltip" data-placement="bottom" title="Lock" lockreport><i class="fa fa-lock fa-lg"></i></a> + <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> + <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> + </a> + </span> {% endif %} {% elif status == 'Locked' and not display_count %} - <a class="btn btn-default btn-report" data-toggle="tooltip" data-placement="bottom" title="Publish" publishreport><i class="fa fa-globe fa-lg"></i></a> + <span class="btn btn-default" data-toggle='modal' data-target='#publishReportModal'> + <a class="btn-report" data-toggle="tooltip" data-placement="bottom" title="Publish"> + <i class="fa fa-globe fa-lg"></i> + </a> + </span> {% endif %} {% endifequal %} @@ -56,6 +64,15 @@ <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> + {% endif %} + + {% if not display_count and status == 'Locked' %} + <report-publish></report-publish> + {% endif %} + + </div> {% endwith %} diff --git a/beat/web/reports/templates/reports/panels/viewer.html b/beat/web/reports/templates/reports/panels/viewer.html index 9031b7d794c8d0b715e25c4b562daa26c3ab7dba..75091caadff4c44ad3ddbcf57b63c26c17ce9491 100644 --- a/beat/web/reports/templates/reports/panels/viewer.html +++ b/beat/web/reports/templates/reports/panels/viewer.html @@ -1,78 +1,59 @@ +{# '{#' is a trigger for django highlighting in editors #} {% 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/. +* 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 %} {% load ui_tags %} +{% load report_tags %} {% with object.get_status_display as status %} <div class="row"> - <div class="col-sm-12"> - <div class="panel-group" id="information-accordion" role="tablist" aria-multiselectable="true"> - - {# DOCUMENTATION DISPLAY/EDITOR #} - <div class="panel panel-default"> - <div class="panel-heading" role="tab" id="docs-heading"> - <h4 class="panel-title"> - <a {% if owner %}class="collapsed" {%endif%}role="button" data-toggle="collapse" data-parent="#docs-heading" href="#collapse-docs" aria-expanded="{% if owner %}false{% else %}true{% endif %}" aria-controls="collapse-docs">Documentation{% if not object.description %} <i class="fa fa-warning"></i>{% endif %}</a> - </h4> - </div>{# panel-heading #} - <div id="collapse-docs" class="panel-collapse collapse{% if not owner %} in{% endif %}" role="tabpanel" aria-labelledby="docs-heading"> - <div class="panel-body"> - {% if status == 'Editable' and owner %} - {% doc_editor object 'api_reports:object' %} - {% else %} - {% doc_editor object 'api_reports:object' False %} - {% endif %} - </div>{# panel-body #} - </div>{# collapse #} - </div>{# panel #} - - {% if status != 'Locked' %} - <div class="panel panel-default" ng-if="report.experiments.length != 0" > - <div class="panel-heading" role="tab" id="info-heading"> - <h4 class="panel-title"> - <a role="button" data-toggle="collapse" data-parent="#info-heading" href="#collapse-info" aria-expanded="true" aria-controls="collapse-info">Configuration</a> - </h4> - </div>{# panel-heading #} - <div id="collapse-info" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="info-heading"> - <div class="panel-body"> - <myreportinfo urlprefix={{URL_PREFIX}}></myreportinfo> - </div>{# panel-body #} - </div>{# collapse #} - </div>{# panel #} - {% endif %} - - </div>{# panel-group #} - <div ng-if="report.experiments.length != 0" class="alert alert-success"{% if not owner or status != 'Editable' %}style="display:none;"{% endif %}> - <a class="btn btn-success btn-sm add_item" data-toggle="tooltip" data-placement="bottom" title="Add item" addreportitem><i class="fa fa-plus fa-lg"></i> Add a report item</a> - </div> - <div ng-if="report.experiments.length == 0" class="alert alert-warning"{% if not owner or status != 'Editable' %}style="display:none;"{% endif %}> - <i class="fa fa-warning fa-lg"></i> You have <strong>not added any experiments</strong> in this report yet. You may add experiments from <a href="{{ URL_PREFIX }}/experiments/{{ object.author.username }}/">any experiment list page</a> to unlock editing features for this report. - </div> - - {# DISPLAYED TABLES AND FIGURES #} - <div class="panel-group" id="space-for-report-items" role="tablist" aria-multiselectable="true"> - </div> - - </div>{# col-sm-12 #} + <div class="col-sm-12"> + <div class="panel-group" id="information-accordion" role="tablist" aria-multiselectable="true"> + + {# DOCUMENTATION DISPLAY/EDITOR #} + <div class="panel panel-default"> + <div class="panel-heading" role="tab" id="docs-heading"> + <h4 class="panel-title"> + <a {% if owner %}class="collapsed" {%endif%}role="button" data-toggle="collapse" data-parent="#docs-heading" href="#collapse-docs" aria-expanded="{% if owner %}false{% else %}true{% endif %}" aria-controls="collapse-docs">Documentation{% if not object.description %} <i class="fa fa-warning"></i>{% endif %}</a> + </h4> + </div>{# panel-heading #} + <div id="collapse-docs" class="panel-collapse collapse{% if not owner %} in{% endif %}" role="tabpanel" aria-labelledby="docs-heading"> + <div class="panel-body"> + {% if status == 'Editable' and owner %} + {% doc_editor object 'api_reports:object' %} + {% else %} + {% doc_editor object 'api_reports:object' False %} + {% endif %} + </div>{# panel-body #} + </div>{# collapse #} + </div>{# panel #} + </div> + </div>{# col-sm-12 #} </div>{# row #} +<div class="row"> + <div class="col-sm-12"> + <report-error></report-error> + <div groups-layout></div> + </div> +</div> {% endwith %} {% csrf_token %} diff --git a/beat/web/reports/templates/reports/partials/reportChartDropDown.html b/beat/web/reports/templates/reports/partials/reportChartDropDown.html deleted file mode 100644 index fa1e939d54acb1ee1cb9879086059829fdd77dbc..0000000000000000000000000000000000000000 --- a/beat/web/reports/templates/reports/partials/reportChartDropDown.html +++ /dev/null @@ -1,26 +0,0 @@ -{% 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 %} -<div> -<select> - <option ng-model="selectedName" ng-repeat="(key, value) in report.plotterparameter" ng-selected="'{$ value.name $}' == '{$ dattrs.selected_template $}'" value="{$ value.name $}">{$ value.name $}</i></p> -</select> -</div> diff --git a/beat/web/reports/templates/reports/partials/reportGeneralInfo.html b/beat/web/reports/templates/reports/partials/reportGeneralInfo.html deleted file mode 100644 index 23564e2ec0341a4254ee42619454eefdd39c8f4c..0000000000000000000000000000000000000000 --- a/beat/web/reports/templates/reports/partials/reportGeneralInfo.html +++ /dev/null @@ -1,70 +0,0 @@ -{% 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 %} -<div class="reportInfo view"> -<div class="reportInfoContainer"> - <div class="span3 report"> - <div class="reportBody"> - <div class="row table-responsive"> - <div class="col-sm-12"> - <table class="table table-hover table-striped table-condensed"> - <thead> - <tr> - <th>Experiments</th> - <th ng-if=" report.status == 'editable' && report.number != report_number" class="experiment_alias_title">Experiments Alias</th> - </tr> - </thead> - <tbody> - <tr class="counter" ng-repeat="(name, information) in report.all_experiments"> - <td ng-if=" report.status == 'editable' && report.number != report_number"> - <a id='{$ name $}' class='button remove_experiment {$ report.status $}' removeexperiment> - <i class='fa fa-times fa-lg'></i> - </a> - <a ng-if="report.status == 'editable' && report.number != report_number" href="{$ report.url_prefix $}/experiments/{$ name $}/">{$ name $} - </a> - <a ng-if="report.status != 'editable' && report.number != report_number" href="{$ report.url_prefix $}/experiments/{$ name $}/">{$ report_experiments_alias[name] $} - </a> - </td> - <td> - <a ng-if="report.number == report_number || (report.status == 'published' && report.number != report_number)" href="{$ report.url_prefix $}/experiments/{$ name $}/">{$ report_experiments_alias[name] $} - </a> - <div ng-if=" report.status == 'editable' && report.number != report_number" id='{$ name $}' class="experiment_alias"> - <a ng-if=" report.status == 'editable' && report.number != report_number" id='button_alias_{$ name $}' class='button edit_alias setalias {$ report.status $}' aliasexperiment> - <i id ='icon_{$ name $}' class='fa fa-lock fa-lg'></i> - </a> - <input id='input_{$ name $}' class="form-control input-sm experiment_alias input-disabled" type="text" name="experiment_alias" value='{$ report_experiments_alias[name] $}' disabled="disabled" style="width:90%;display:inline"> - </div> - </td> - </tr> - </tbody> - </table> - </div> - </div> - <div class="form form-inline"> - <div ng-if="report.status == 'editable' && report.number != report_number" class="form-group tableprecision {$ dattrs.reportstatus $} {$ dattrs.accessnumber $}" tableprecision> - <label for="float-precision">Floating-point Precision: </label> - <select id="float-precision" ng-model="floating_point_precision.selected" class="form-control input-sm" ng-options="item for item in floating_point_precision ">Select the floating point precision for your tables in the report</select> - <p class="help">Select the floating point precision for your tables in the report.</p> - </div> - </div> - </div> - </div> -</div> diff --git a/beat/web/reports/templates/reports/partials/reportSingleTable.html b/beat/web/reports/templates/reports/partials/reportSingleTable.html deleted file mode 100644 index 5d2e92a631a9812484a9ad50f040a1856ba27694..0000000000000000000000000000000000000000 --- a/beat/web/reports/templates/reports/partials/reportSingleTable.html +++ /dev/null @@ -1,58 +0,0 @@ -{% 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 %} -<div id="{$ dattrs.tableid $}"> - -<div class="row"> - <div class="col-sm-12"> - <div class="pull-left action-buttons"> - <a ng-if="dattrs.reportstatus == 'editable' && dattrs.accessnumber != 'number'" class="btn btn-success btn-sm settings {$ dattrs.reportstatus $} {$ dattrs.accessnumber $}" buttonsettings><i class="fa fa-cog fa-lg"></i> Settings</a> - <a ng-if="dattrs.reportstatus == 'editable' && dattrs.accessnumber != 'number'" class="btn btn-danger btn-sm item_delete {$ dattrs.reportstatus $} {$ dattrs.accessnumber $}" buttondeleteitem><i class="fa fa-times fa-lg"></i> Delete</a> - <a class="btn btn-primary btn-sm item_export {$ dattrs.reportstatus $} {$ dattrs.accessnumber $}" buttonexportitem><i class="fa fa-download fa-lg"></i> Export Table</a> - </div> - </div> -</div> - -<div class="row table-responsive"> - <div class="col-sm-12"> - <table class="table table-hover table-striped table-condensed report-table"> - <thead> - <tr> - <th ng-repeat="(key, value) in tables_details[dattrs.tableid]" ng-if="value.selected" class="tableitemspace" sortth="{$ value.name $}" sorttblid="{$ dattrs.tableid $}" sortdata> - <span style="white-space:nowrap"> - <span ng-if="(sorted_tables | contains:dattrs.tableid)" class="glyphicon sort-icon" ng-show="sorted_experiments_keys_tables_sortkey[dattrs.tableid]==value.name" ng-class="{'glyphicon-chevron-up':sorted_experiments_keys_reverse[dattrs.tableid],'glyphicon-chevron-down':!(sorted_experiments_keys_reverse[dattrs.tableid])}"></span> - <span ng-if="!(sorted_tables | contains:dattrs.tableid) && sorted_experiments_keys_reverse[dattrs.tableid]!=undefined" class="glyphicon sort-icon" ng-show="sorted_experiments_keys_tables_sortkey[dattrs.tableid]==value.name" ng-class="{'glyphicon-chevron-up':sorted_experiments_keys_reverse[dattrs.tableid],'glyphicon-chevron-down':!(sorted_experiments_keys_reverse[dattrs.tableid])}"></span> - <small style="white-space:nowrap"><span style="color:#D3D3D3" ng-if="!(sorted_tables | contains:dattrs.tableid) || sorted_experiments_keys_reverse[dattrs.tableid]==undefined || sorted_experiments_keys_tables_sortkey[dattrs.tableid]!=value.name" class="glyphicon glyphicon-sort sort-icon" ng-show="sorted_experiments_keys_tables_sortkey[dattrs.tableid]!=value.name"></span></small> - {$ value.name $} - </span> - </th> - </tr> - </thead> - <tbody> - <tr ng-if="!(sorted_tables | contains:dattrs.tableid)" ng-repeat="(exp_key, exp_val) in report_experiments" item="exp_key"> - <tr ng-if="(dattrs.reportstatus == 'editable' || dattrs.reportstatus == 'published') && sorted_tables | contains:dattrs.tableid" ng-repeat="(exp_key, exp_val) in sorted_experiments_keys_tables[dattrs.tableid]" item="exp_val"> - <tr ng-if="(dattrs.reportstatus == 'locked') && sorted_tables | contains:dattrs.tableid" ng-repeat="(exp_key, exp_val) in sorted_experiments_keys_tables[dattrs.tableid]" item="exp_val"> - </tr> - </tbody> - </table> - </div> -</div> -</div> diff --git a/beat/web/reports/templates/reports/report.html b/beat/web/reports/templates/reports/report.html index 30a0da96b75384d4dc1820fa47db21b13d54ad92..a5ec574f2d3bc49681be4c96341a89e696c7f98c 100644 --- a/beat/web/reports/templates/reports/report.html +++ b/beat/web/reports/templates/reports/report.html @@ -2,21 +2,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 %} @@ -42,6 +42,7 @@ <link rel="stylesheet" href="{% fingerprint "chosen-bootstrap/chosen.bootstrap.min.css" %}" type="text/css" media="screen" /> <link rel="stylesheet" href="{% fingerprint "datatables/media/css/dataTables.bootstrap.min.css" %}" type="text/css" media="screen" /> <link rel="stylesheet" href="{% fingerprint "jquery-ui/themes/base/minified/jquery-ui.min.css" %}" type="text/css" media="screen" /> + <link rel="stylesheet" href="{% fingerprint "reports/css/style.css" %}" type="text/css" media="screen" /> {% code_editor_css %} {% endblock %} @@ -55,7 +56,7 @@ <script src="{% fingerprint "datatables/media/js/dataTables.bootstrap.min.js" %}" type="text/javascript" charset="utf-8"></script> <!-- Use Google's CDN for angular-js with a local fallback --> - <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script> + <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script> <script>window.angular || document.write('<script src="{% fingerprint "angular/angular.min.js" %}"><\/script>');</script> <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-filter/0.5.8/angular-filter.min.js"></script> <script src="{% fingerprint "angular-ui-router/release/angular-ui-router.min.js" %}" type="text/javascript" charset="utf-8"></script> @@ -64,18 +65,64 @@ <script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.widget.min.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.button.min.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.dialog.min.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.mouse.min.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "jquery-ui/ui/minified/jquery.ui.sortable.min.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "angular-ui-sortable/sortable.min.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "angular-ui-codemirror/ui-codemirror.min.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "experiments/js/utils.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "reports/js/base_64_encoder_decoder.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "reports/js/download.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "reports/app/app.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "reports/app/app.config.js" %}" type="text/javascript" charset="utf-8"></script> + + <!-- controllers --> <script src="{% fingerprint "reports/app/controllers/reportController.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/controllers/groupsController.js" %}" type="text/javascript" charset="utf-8"></script> + + <!-- factories --> <script src="{% fingerprint "reports/app/factories/reportFactory.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "reports/app/factories/experimentFactory.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "reports/app/factories/plotterFactory.js" %}" type="text/javascript" charset="utf-8"></script> - <script src="{% fingerprint "reports/app/factories/dataFactory.js" %}" type="text/javascript" charset="utf-8"></script> - <script src="{% fingerprint "reports/app/directives/reportItemView.js" %}" type="text/javascript" charset="utf-8"></script> + + <!-- services --> + <script src="{% fingerprint "reports/app/services/groupsService.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/services/experimentsService.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/services/urlService.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/services/reportService.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/services/plotService.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/services/errorService.js" %}" type="text/javascript" charset="utf-8"></script> + + <!-- directives --> + + <!-- new --> + <script src="{% fingerprint "reports/app/directives/downloadLink.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/bootstrapModal.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/publish.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/lock.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/save.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/error.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/experimentsTable.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/editableLabel.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/panelExperiments.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/panelContainer.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/dragHandle.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/panelContent.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/panelItems.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/layout.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/tableItem.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/plotItem.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/textItem.js" %}" type="text/javascript" charset="utf-8"></script> + + <!-- edit view --> + {% if not report_number and report.get_status_display == 'Editable' and owner %} + <script src="{% fingerprint "reports/app/directives/edit/addGroupMenu.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/edit/addItemsMenu.js" %}" type="text/javascript" charset="utf-8"></script> + <script src="{% fingerprint "reports/app/directives/edit/tableFieldSelector.js" %}" type="text/javascript" charset="utf-8"></script> + {% else %} + <!-- readonly view --> + {% endif %} + <script src="{% fingerprint "ui/js/smartselector.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "ui/js/multipleselector.js" %}" type="text/javascript" charset="utf-8"></script> <script src="{% fingerprint "ui/js/multipleselector_for_report.js" %}" type="text/javascript" charset="utf-8"></script> @@ -86,73 +133,61 @@ {% block content %} -{% if author %} - <div class="col-sm-12" ng-app="reportApp" ng-controller="reportController" ng-init="init('{{ author }}', '{{report_name}}', '{{ URL_PREFIX }}', '{% fingerprint "reports/app/data/itemcontent.json" %}', '{% fingerprint "reports/app/data/table_itemcontent.json" %}')"> -{% endif %} -{% if report_number %} - <div class="col-sm-12" ng-app="reportApp" ng-controller="reportController" ng-init="initWithReportNumber('{{report_number}}', '{{ URL_PREFIX }}', '{% fingerprint "reports/app/data/itemcontent.json" %}', '{% fingerprint "reports/app/data/table_itemcontent.json" %}')"> +<div ng-app="reportApp" ng-controller="reportController"> + {% csrf_token %} -{% endif %} -{% csrf_token %} + {% if report %} + <div id="title" class="row"> + <div class="{% if show_actionbar %}col-sm-9 vertical-center{% else %}col-sm-12{% endif %}"{% if show_actionbar %} onmouseover="expand_breadcrumb(this, 9, 3);" onmouseout="reset_breadcrumb(this, 9, 3);"{% endif %}> + {% report_breadcrumb report %} + <!-- Note: keep no space between divs here! --> + </div>{% if show_actionbar %}<div class="col-sm-3 vertical-center"> + {% report_actions report False %} + </div>{% endif %} -{% if report %} -<div id="title" class="row"> - <div class="{% if owner %}col-sm-9 vertical-center{% else %}col-sm-12{% endif %}"{% if owner %} onmouseover="expand_breadcrumb(this, 9, 3);" onmouseout="reset_breadcrumb(this, 9, 3);"{% endif %}> - {% report_breadcrumb report %} - <!-- Note: keep no space between divs here! --> - </div>{% if owner %}<div class="col-sm-3 vertical-center"> - {% report_actions report False %} - </div>{% endif %} + </div> -</div> + {% with report.get_status_display as status %} + <div class="row"> + <div class="col-sm-12"> -{% with report.get_status_display as status %} -<div class="row"> - <div class="col-sm-12"> + <p class="bs-callout bs-callout-{% if status == 'Editable' %}danger{% elif status == 'Locked' %}warning{% else %}info{% endif %} lastEdited"> - <p class="bs-callout bs-callout-{% if status == 'Editable' %}danger{% elif status == 'Locked' %}warning{% else %}info{% endif %}"> + {% if report.short_description %} + <i class="fa fa-file-text"></i> {{ report.short_description }}<br/> + {% endif %} -{% if report.short_description %} -<i class="fa fa-file-text"></i> {{ report.short_description }}<br/> -{% endif %} + {% ifequal request.user.username author %} + <i class="fa fa-arrow-circle-right"></i> Unique report id: <a href="{{ report.get_absolute_url }}" title="Use this link on your report or to share access with users and reviewers" data-toggle="tooltip" data-placement="top">{{ report.number }}</a> <span class="help">(follow link to see review/publication URL.)</span><br/> + {% endifequal %} -{% ifequal request.user.username author %} -<i class="fa fa-arrow-circle-right"></i> Unique report id: <a href="{{ report.get_absolute_url }}" title="Use this link on your report or to share access with users and reviewers" data-toggle="tooltip" data-placement="top">{{ report.number }}</a> <span class="help">(follow link to see review/publication URL.)</span><br/> -{% endifequal %} + {% if report.publication_date %} + <i class="fa fa-calendar-o"></i> Published: <strong>{{ report.publication_date|naturaltime }}</strong><br/> + {% else %} + <i class="fa fa-calendar-o"></i> Created: <strong>{{ report.creation_date|naturaltime }}</strong><br/> + {% endif %} + {% if report.last_edited_date != None and not report.publication_date %} -{% if report.publication_date %} -<i class="fa fa-calendar-o"></i> Published: <strong>{{ report.publication_date|naturaltime }}</strong><br/> -{% else %} -<i class="fa fa-calendar-o"></i> Created: <strong>{{ report.creation_date|naturaltime }}</strong><br/> -{% endif %} + <i class="fa fa-calendar-o"></i> Last Edited: <strong>{{ report.last_edited_date|naturaltime }}</strong><br/> + {% endif %} -{% if status == 'Editable' %} -<i class="fa fa-warning"></i> This report is <strong class="text-danger">{{ status }}</strong> (it may change in time)<br/> -{% if owner %} -<i class="fa fa-code"></i> Analyzer: <strong><a target="_blank" href="{{ report.analyzer.get_absolute_url }}" title="View analyzer in a new window">{{ report.analyzer.fullname }}</a></strong><br/> -{% endif %} -{% elif status == 'Locked' %} -<i class="fa fa-warning"></i> This report is <strong class="text-warning">{{ status }}</strong> (not yet published)<br/> -<i class="fa fa-calendar-o"></i> Expires in <strong>{{ report.expiration_date|naturaltime }}</strong>, on {{ report.expiration_date }} (publish it to make it permanent)<br/> -{% endif %} - - </p> - </div> -</div> -{% endwith %} + {% if status == 'Editable' %} + <i class="fa fa-warning"></i> This report is <strong class="text-danger">{{ status }}</strong> (it may change in time)<br/> + {% elif status == 'Locked' %} + <i class="fa fa-warning"></i> This report is <strong class="text-warning">{{ status }}</strong> (not yet published)<br/> + <i class="fa fa-calendar-o"></i> Expires in <strong>{{ report.expiration_date|naturaltime }}</strong>, on {{ report.expiration_date }} (publish it to make it permanent)<br/> + {% endif %} + </p> + </div> + </div> + {% endwith %} -{% report_viewer report owner %} -{% endif %} - -<div id="tabs_progress" class="progress"> - <i class="fa fa-spin fa-refresh fa-1x"></i> -</div> + {% report_viewer report owner report_number %} -<div id="report_items" class="row"> - <div id="space-for-report-items"></div> </div> +{% endif %} {% list_selector "list_selector" %} {% multiple_selector "multiple_selector" %} @@ -160,11 +195,6 @@ {% save_as_dialog "save_as_dialog" %} {% smart_selector "smart_selector" %} {% smart_selector "smart_selector_detail" %} -{% report_lock "report_lock" %} -{% report_publish "report_publish" %} -{% report_remove_experiment "report_remove_experiment" %} -{% report_remove_item "report_remove_item" %} -{% report_saved "report_saved" %} <script type="text/javascript"> var smart_selector = new beat.ui.SmartSelector('smart_selector'); diff --git a/beat/web/reports/templatetags/report_tags.py b/beat/web/reports/templatetags/report_tags.py index 5ae90d30f05f9c30e530d18705bf2fa7f7011142..427402f92f05c34c50714050f733276f47679359 100644 --- a/beat/web/reports/templatetags/report_tags.py +++ b/beat/web/reports/templatetags/report_tags.py @@ -105,7 +105,7 @@ def report_actions(context, object, display_count): @register.inclusion_tag('reports/panels/viewer.html', takes_context=True) -def report_viewer(context, object, owner): +def report_viewer(context, object, owner, report_number): '''Composes the search results for visualization Parameters: @@ -125,6 +125,8 @@ def report_viewer(context, object, owner): request=context['request'], object=object, owner=owner, + exps=object.experiments.all(), + report_number=report_number, URL_PREFIX=context['URL_PREFIX'], ) @@ -186,63 +188,3 @@ def report_deletion(id): register.inclusion_tag('ui/contribution_deletion.html')(report_deletion) - - -#-------------------------------------------------- - - -def report_lock(id): - return { 'dialog_id': id, - 'URL_PREFIX': settings.URL_PREFIX - } - - -register.inclusion_tag('reports/dialogs/report_lock.html')(report_lock) - - -#-------------------------------------------------- - - -def report_publish(id): - return { 'dialog_id': id, - 'URL_PREFIX': settings.URL_PREFIX - } - - -register.inclusion_tag('reports/dialogs/report_publish.html')(report_publish) - - -#-------------------------------------------------- - - -def report_remove_experiment(id): - return { 'dialog_id': id, - 'URL_PREFIX': settings.URL_PREFIX - } - - -register.inclusion_tag('reports/dialogs/report_remove_experiment.html')(report_remove_experiment) - - -#-------------------------------------------------- - - -def report_remove_item(id): - return { 'dialog_id': id, - 'URL_PREFIX': settings.URL_PREFIX - } - - -register.inclusion_tag('reports/dialogs/report_remove_item.html')(report_remove_item) - - -#-------------------------------------------------- - - -def report_saved(id): - return { 'dialog_id': id, - 'URL_PREFIX': settings.URL_PREFIX - } - - -register.inclusion_tag('reports/dialogs/report_saved.html')(report_saved) diff --git a/beat/web/reports/tests.py b/beat/web/reports/tests.py index bd1fc642bb8081abd8da59ba92207bbcf6d48593..693a884b1097e1a96aca4e9456975451e7ea0900 100755 --- a/beat/web/reports/tests.py +++ b/beat/web/reports/tests.py @@ -861,7 +861,7 @@ class ReportCreationTestCase(ReportTestCase): self.experiment_analyzer1.fullname(), ], "content": {}, - "analyzer": self.analyzer1.fullname() + "analyzer": None } self.client.login(username=self.johndoe.username, password=self.password) @@ -889,7 +889,7 @@ class ReportCreationTestCase(ReportTestCase): self.experiment_two_analyzers.fullname(), ], "content": {}, - "analyzer": self.analyzer1.fullname() + "analyzer": None } self.client.login(username=self.johndoe.username, password=self.password) @@ -910,21 +910,19 @@ class ReportCreationTestCase(ReportTestCase): self.experiment_analyzer2.fullname(), ], "content": {}, - "analyzer": self.analyzer1.fullname() + "analyzer": None } self.client.login(username=self.johndoe.username, password=self.password) response = self.client.post(self.url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertTrue(len(response.content) > 0) def test_logged_in_user__one_experiment__two_analyzers(self): expected_answer = { - 'common_analyzers': [ - self.analyzer1.fullname(), - self.analyzer2.fullname(), - ] + 'name': 'johndoe/report_new', + 'url': '/api/v1/reports/johndoe/report_new/' } data = { @@ -935,13 +933,13 @@ class ReportCreationTestCase(ReportTestCase): self.experiment_two_analyzers.fullname(), ], "content": {}, - "analyzer": self.analyzer1.fullname() + "analyzer": None } self.client.login(username=self.johndoe.username, password=self.password) response = self.client.post(self.url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(json.loads(response.content), expected_answer) @@ -1027,7 +1025,7 @@ class LockedReportRetrievalTestCase(ReportTestCase): self.experiment_analyzer1.fullname(), ], "content": {}, - "analyzer": self.report.analyzer.fullname() + "analyzer": None } self.client.login(username=self.johndoe.username, password=self.password) @@ -1073,7 +1071,7 @@ class PublishedReportRetrievalTestCase(ReportTestCase): self.experiment_analyzer1.fullname(), ], "content": {}, - "analyzer": self.report.analyzer.fullname() + "analyzer": None } response = self.client.get(self.url, format='json') @@ -1096,7 +1094,7 @@ class PublishedReportRetrievalTestCase(ReportTestCase): self.experiment_analyzer1.fullname(), ], "content": {}, - "analyzer": self.report.analyzer.fullname() + "analyzer": None } self.client.login(username=self.johndoe.username, password=self.password) @@ -1121,7 +1119,7 @@ class PublishedReportRetrievalTestCase(ReportTestCase): self.experiment_analyzer1.fullname(), ], "content": {}, - "analyzer": self.report.analyzer.fullname() + "analyzer": None } self.client.login(username=self.jackdoe.username, password=self.password) @@ -1770,8 +1768,8 @@ class EditableReportAddExperimentsTestCase(ReportTestCase): self.client.login(username=self.johndoe.username, password=self.password) response = self.client.post(self.url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue(len(response.content) > 0) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue('content' not in response) def test_logged_in_user__one_experiment__two_analyzers(self): expected_answer = { @@ -1790,8 +1788,8 @@ class EditableReportAddExperimentsTestCase(ReportTestCase): self.client.login(username=self.johndoe.username, password=self.password) response = self.client.post(self.url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(json.loads(response.content), expected_answer) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue('content' not in response) def test_logged_in_user__compatible_experiment_addition(self): data = { @@ -1813,6 +1811,7 @@ class EditableReportAddExperimentsTestCase(ReportTestCase): response = self.client.post(self.url, data, format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue('content' not in response) def test_logged_in_user__incompatible_experiment_addition(self): @@ -1840,8 +1839,8 @@ class EditableReportAddExperimentsTestCase(ReportTestCase): } response = self.client.post(self.url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(json.loads(response.content), expected_answer) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertTrue('content' not in response) def test_with_unknown_experiments(self): diff --git a/beat/web/reports/views.py b/beat/web/reports/views.py index 6d17fb7fbaa859e581573c7db6554f4696723a16..0a778fea71f2d522d8e806deeba0575bc139df71 100644 --- a/beat/web/reports/views.py +++ b/beat/web/reports/views.py @@ -25,7 +25,7 @@ # # ############################################################################### -from django.shortcuts import render_to_response +from django.shortcuts import render_to_response, redirect from django.shortcuts import get_object_or_404 from django.template import RequestContext, Context from django.conf import settings @@ -33,6 +33,7 @@ from django.views.generic import TemplateView from django.contrib.auth.models import User from django.db.models import Q from django.db.models.functions import Coalesce +from django.http import Http404 from ..ui.templatetags.markup import restructuredtext @@ -40,21 +41,45 @@ from .models import Report import simplejson as json +# Permissions for viewing a report is complicated: +# - 'E' indicates permissions to view the editable version of the report (usually accessed by author and report name) +# - 'V' indicates permissions to view the view-only version of the report (usually accessed by report number) +# - 'R' indicates that the user will be redirected from the editable version to the view-only version (but not given a 404) +# | *Report State* X *User Type* | Editable | Locked | Published | +# | -------------------------------------: | :------: | :----: | :-------: | +# | Report Author | E, V | R, V | R, V | +# | BEAT User | V | V | V | +# | Anonymous | | V | V | +# | Public (Published) Reports List Reader | | | V | + +def show_actionbar(request, report): + correct_status = report.status == 'E' or report.status == 'L' + correct_user = request.user == report.author + is_admin = request.user.is_staff + + return (correct_status and correct_user) or is_admin + #------------------------------------------------ def by_number(request, number): - # get the query from the DB obj = get_object_or_404(Report, number=int(number)) + isAnon = request.user.is_anonymous() + + if obj.status == 'E' and isAnon: + # return 404 + raise Http404('No %s matches the given query.' % Report._meta.object_name) + return render_to_response('reports/report.html', { 'report_number' : number, 'owner': False, 'report': obj, 'USE_HTTPS_GRAVATAR': settings.USE_HTTPS_GRAVATAR, + 'show_actionbar': show_actionbar(request, obj) }, context_instance=RequestContext(request)) @@ -63,22 +88,37 @@ def by_number(request, number): def for_author(request, author_name, report_name): - # get the query from the DB obj = get_object_or_404(Report, author__username = author_name, name = report_name) - - return render_to_response('reports/report.html', - { - 'author' : author_name, - 'report_name' : report_name, - 'owner' : (request.user == obj.author), - 'report' : obj, - 'USE_HTTPS_GRAVATAR': settings.USE_HTTPS_GRAVATAR, - }, - context_instance=RequestContext(request)) + isAuthor = request.user.username == obj.author.username + isEditable = obj.status == 'E' + isPublished = obj.status == 'P' + isLocked = obj.status == 'L' + + # if its the author and its locked, redirect to numbered url + # same if its published + if isPublished or (isAuthor and isLocked): + return redirect(obj) + + + # only valid when the author is accessing it and its editable + if isEditable and isAuthor: + return render_to_response('reports/report.html', + { + 'author' : author_name, + 'report_name' : report_name, + 'owner' : (request.user == obj.author), + 'report' : obj, + 'USE_HTTPS_GRAVATAR': settings.USE_HTTPS_GRAVATAR, + 'show_actionbar': show_actionbar(request, obj) + }, + context_instance=RequestContext(request)) + + # return 404 + raise Http404('No %s matches the given query.' % Report._meta.object_name) #------------------------------------------------ diff --git a/beat/web/ui/static/ui/js/reportdialog.js b/beat/web/ui/static/ui/js/reportdialog.js index c7efcf3461aad12012bf074e03bac034e96132ba..9ec8ca5716a6a7b708f1b1960e2193631658679e 100644 --- a/beat/web/ui/static/ui/js/reportdialog.js +++ b/beat/web/ui/static/ui/js/reportdialog.js @@ -1,21 +1,21 @@ /* * 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/. */ @@ -120,439 +120,3 @@ beat.ui.report.add_experiments = function(dialog_id, received_data, experiments_ // Display the dialog $('#' + dialog_id).dialog("open"); } - -beat.ui.report.lock_report = function(dialog_id, scope) -{ - var num_click = 0; - // Create the dialog - $('#' + dialog_id).dialog({ - autoOpen: false, - resizable: false, - width: Math.min($(window).width() * 0.6, 700), - position: { my: "center", at: "center", of: window }, - modal: true, - closeOnEscape: true, - buttons: [ - { - id: 'button-' + dialog_id + '-ok', - text: 'Ok', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - - //$('#' + dialog_id).dialog("close"); - if(num_click == 0) - { - num_click++; - - scope.reportFactory.lockReport(scope.user, scope.report_id, scope.url_prefix) - .success(function (reportData) - { - $(".explanation_text").hide(); - $("#report_lock .warnings").show(); - $("#report_lock .errors").hide(); - $("#button-report_lock-cancel").hide(); - }) - .error(function (error) - { - scope.status = 'Unable to lock report data: ' + error; - $(".explanation_text").hide(); - $("#report_lock .warnings").hide(); - $("#report_lock .errors").show(); - if(error.detail != undefined) - scope.status = 'Unable to lock report data: ' + error.detail; - $("#report_lock .errors .errorslist").append(scope.status); - $("#button-report_lock-cancel").hide(); - }); - } - else - { - $('#' + dialog_id).dialog("close"); - $(".explanation_text").show(); - $("#report_lock .warnings").hide(); - $("#report_lock .errors").hide(); - $("#report_lock .errors .errorslist").empty(); - num_click = 0; - location.reload(); - } - - } - }, - { - id: 'button-' + dialog_id + '-cancel', - text: 'Cancel', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - $('#' + dialog_id).dialog("close"); - } - }, - ] - }); - - // Display the dialog - $('#' + dialog_id).dialog("open"); -} - -beat.ui.report.publish_report = function(dialog_id, scope, algorithms_list) -{ - var num_click = 0; - // Create the dialog - $('#' + dialog_id).dialog({ - autoOpen: false, - resizable: false, - width: Math.min($(window).width() * 0.6, 700), - position: { my: "center", at: "center", of: window }, - modal: true, - closeOnEscape: true, - buttons: [ - { - id: 'button-' + dialog_id + '-ok', - text: 'Ok', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - - //$('#' + dialog_id).dialog("close"); - if(num_click == 0) - { - num_click++; - - if(algorithms_list.length > 0) - { - var data = { - visible_algorithms: [], - }; - - var inputs = $('#' + dialog_id + ' div.algorithms ul.algorithmslist input'); - - for (var i = 0; i < inputs.length; ++i) - { - if (!inputs[i].checked) - data.visible_algorithms.push(inputs[i].value); - } - - var send_data = JSON.stringify(data); - - scope.reportFactory.publishReport(scope.url_prefix, scope.user, scope.report_id, send_data) - .success(function (reportData) - { - $(".explanation_text").hide(); - $("#report_publish .warnings").show(); - $("#report_publish .errors").hide(); - $("#button-report_publish-cancel").hide(); - }) - .error(function (error) - { - scope.status = 'Unable to publish report data: ' + error; - $(".explanation_text").hide(); - $("#report_publish .warnings").hide(); - $("#report_publish .errors").show(); - if(error.detail != undefined) - scope.status = 'Unable to publish report data: ' + error.detail; - $("#report_publish .errors .errorslist").append(scope.status); - $("#button-report_publish-cancel").hide(); - }); - - } - else - { - scope.reportFactory.publishReport(scope.url_prefix, scope.user, scope.report_id) - .success(function (reportData) - { - $(".explanation_text").hide(); - $("#report_publish .warnings").show(); - $("#report_publish .errors").hide(); - $("#button-report_publish-cancel").hide(); - }) - .error(function (error) - { - scope.status = 'Unable to publish report data: ' + error; - $(".explanation_text").hide(); - $("#report_publish .warnings").hide(); - $("#report_publish .errors").show(); - if(error.detail != undefined) - scope.status = 'Unable to publish report data: ' + error.detail; - $("#report_publish .errors .errorslist").append(scope.status); - $("#button-report_publish-cancel").hide(); - }); - } - - - } - else - { - $('#' + dialog_id).dialog("close"); - $(".explanation_text").show(); - $("#report_publish .warnings").hide(); - $("#report_publish .errors").hide(); - $("#report_publish .errors .errorslist").empty(); - num_click = 0; - location.reload(); - } - } - }, - { - id: 'button-' + dialog_id + '-cancel', - text: 'Cancel', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - $('#' + dialog_id).dialog("close"); - } - }, - ] - }); - - if(algorithms_list.length > 0 && num_click == 0) - { - // Initialise the dialog content - $('#' + dialog_id + ' div.explanation_text .algorithms ul.algorithmslist').empty(); - - - // Populate the list of algorithms - parent = $('#' + dialog_id + ' div.explanation_text .algorithms ul.algorithmslist')[0]; - - for (var i = 0; i < algorithms_list.length; ++i) - { - var li = document.createElement('li'); - - var input = document.createElement('input'); - input.type = 'checkbox'; - input.className = 'algorithm'; - input.value = algorithms_list[i]; - input.checked = true; - - li.appendChild(input); - - var a = document.createElement('a'); - a.href = scope.url_prefix + '/algorithms/' + algorithms_list[i] + '/'; - a.target = '_blank'; - a.textContent = algorithms_list[i]; - - li.appendChild(a); - - parent.appendChild(li); - } - - $('#' + dialog_id + ' div.explanation_text .algorithms').show(); - - - } - - // Display the dialog - $('#' + dialog_id).dialog("open"); -} - -beat.ui.report.remove_experiment = function(dialog_id, experiment_id, scope) -{ - var num_click = 0; - // Create the dialog - $('#' + dialog_id).dialog({ - autoOpen: false, - resizable: false, - width: Math.min($(window).width() * 0.6, 700), - position: { my: "center", at: "center", of: window }, - modal: true, - closeOnEscape: true, - buttons: [ - { - id: 'button-' + dialog_id + '-ok', - text: 'Ok', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - - //$('#' + dialog_id).dialog("close"); - if(num_click == 0) - { - num_click++; - - var data_to_send = {}; - data_to_send['experiments'] = [experiment_id]; - scope.reportFactory.removeExperiment(scope.user, scope.report_id, data_to_send, scope.url_prefix) - .success(function (reportData) - { - $(".explanation_text").hide(); - $("#report_remove_experiment .warnings").show(); - $("#report_remove_experiment .errors").hide(); - $("#button-report_remove_experiment-cancel").hide(); - }) - .error(function (error) - { - scope.status = 'Unable to remove experiment from report: ' + error; - $(".explanation_text").hide(); - $("#report_remove_experiment .warnings").hide(); - $("#report_remove_experiment .errors").show(); - if(error.detail != undefined) - scope.status = 'Unable to remove experiment from report: ' + error.detail; - $("#report_remove_experiment .errors .errorslist").append(scope.status); - $("#button-report_remove_experiment-cancel").hide(); - }); - } - else - { - $('#' + dialog_id).dialog("close"); - $(".explanation_text").show(); - $("#report_remove_experiment .warnings").hide(); - $("#report_remove_experiment .errors").hide(); - $("#report_remove_experiment .errors .errorslist").empty(); - num_click = 0; - location.reload(); - } - - } - }, - { - id: 'button-' + dialog_id + '-cancel', - text: 'Cancel', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - $('#' + dialog_id).dialog("close"); - } - }, - ] - }); - - // Display the dialog - $('#' + dialog_id).dialog("open"); -} - -beat.ui.report.remove_item = function(dialog_id, elementToRemove, scope) -{ - var num_click = 0; - // Create the dialog - $('#' + dialog_id).dialog({ - autoOpen: false, - resizable: false, - width: Math.min($(window).width() * 0.6, 700), - position: { my: "center", at: "center", of: window }, - modal: true, - closeOnEscape: true, - buttons: [ - { - id: 'button-' + dialog_id + '-ok', - text: 'Ok', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - - //$('#' + dialog_id).dialog("close"); - if(num_click == 0) - { - num_click++; - - if(elementToRemove.id.split("_")[0] == 'table') - { - delete scope.tables_details[elementToRemove.id]; - } - else - { - delete scope.plots_details[elementToRemove.id]; - } - - var index = scope.report.orderedcontent.indexOf(elementToRemove.id); - if(index > -1) - { - scope.report.orderedcontent.splice(index, 1); - } - - delete scope.report.content[elementToRemove.id]; - - - $("#" + elementToRemove.id).remove(); - - $('#' + dialog_id).dialog("close"); - } - } - }, - { - id: 'button-' + dialog_id + '-cancel', - text: 'Cancel', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - $('#' + dialog_id).dialog("close"); - } - }, - ] - }); - - // Display the dialog - $('#' + dialog_id).dialog("open"); -} - -beat.ui.report.report_saved = function(dialog_id, scope) -{ - var num_click = 0; - // Create the dialog - $('#' + dialog_id).dialog({ - autoOpen: false, - resizable: false, - width: Math.min($(window).width() * 0.6, 700), - position: { my: "center", at: "center", of: window }, - modal: true, - closeOnEscape: true, - buttons: [ - { - id: 'button-' + dialog_id + '-ok', - text: 'Ok', - click: function() { - var data = {}; - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - xhr.setRequestHeader('X-CSRFToken', $.cookie('csrftoken')); - } - }); - - //$('#' + dialog_id).dialog("close"); - if(num_click == 0) - { - num_click++; - $(".explanation_text").hide(); - $('#' + dialog_id).dialog("close"); - $(".explanation_text").show(); - num_click = 0; - location.reload(); - } - } - }, - ] - }); - - // Display the dialog - $('#' + dialog_id).dialog("open"); -} diff --git a/beat/web/utils/management/commands/install.py b/beat/web/utils/management/commands/install.py index c863d605ade40598a0af0333e2f14327abb7005d..7201b86eddc7a1b13c4339a5ce0a8bab86f817ca 100755 --- a/beat/web/utils/management/commands/install.py +++ b/beat/web/utils/management/commands/install.py @@ -736,14 +736,10 @@ def upload_plotter(prefix, name, data): with open(short_desc_file_location) as short_desc_data_file: short_desc = short_desc_data_file.readline().split("\n")[0] - if plotter.dataformat.name != "scatter": - plotterparameter = PlotterParameter.objects.create(name=plotter.dataformat.name, - author=author, plotter=plotter, data=simplejson.dumps(plotterparameter_data, - indent=4), short_description=short_desc, sharing = Shareable.PUBLIC) - else: - plotterparameter = PlotterParameter.objects.create(name=plotter.dataformat.name, - author=author, plotter=plotter, data=simplejson.dumps(plotterparameter_data, - indent=4), short_description=short_desc) + plotterparameter = PlotterParameter.objects.create(name=plotter.dataformat.name, + author=author, plotter=plotter, data=simplejson.dumps(plotterparameter_data, + indent=4), short_description=short_desc, sharing = Shareable.PUBLIC) + plotterparameter.save() logger.info("Add plotterparameter `%s' ", plotterparameter) diff --git a/buildout.cfg b/buildout.cfg index 74cab04d2fe2b25194ac1c2837c6221d7cb4d50c..fa09ef7eb197b9136577b295c1a278c2ea4c4692 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -23,7 +23,7 @@ django-activity-stream = >= 0.6.0 [sysegg] recipe = syseggrecipe -;force-sysegg = true +force-sysegg = true eggs = alabaster babel backports.shutil-get-terminal-size @@ -75,6 +75,7 @@ eggs = alabaster six snowballstemmer Sphinx + sphinxcontrib-ansi sphinxcontrib-httpdomain sphinxcontrib-mscgen sphinxcontrib-programoutput @@ -96,7 +97,7 @@ recipe = bob.buildout:scripts [docker_images] recipe = collective.recipe.cmd cmds = ./src/beat.core/buildout_pull_images.sh -uninstall_cmds = +uninstall_cmds = on_install = true on_update = true @@ -116,8 +117,8 @@ eggs = ${buildout:eggs} [node] recipe = gp.recipe.node -npms = bower -scripts = bower +npms = bower protractor +scripts = bower protractor webdriver-manager [bower] recipe = bowerrecipe @@ -137,11 +138,13 @@ packages = jquery#~1.11.3 spectrum#~1.7.1 https://github.com/joshaven/string_score.git#~0.1.22 chosen-bootstrap#~1.1.0 - angularjs#~1.4.5 + angularjs#~1.6.4 angular-ui-router#~0.2.15 chartjs#~1.0.2 underscore#~1.8.3 datatables#~1.10.10 + angular-ui-sortable#~0.14 + angular-ui-codemirror executable = ${buildout:bin-directory}/bower base-directory = beat/web downloads = static diff --git a/doc/admin/installation.rst b/doc/admin/installation.rst index 87d4598d623cbe9576dd728b7254812db3d04a33..3bf7ce19ccc065b9eca9aae3733ecde312f4861b 100644 --- a/doc/admin/installation.rst +++ b/doc/admin/installation.rst @@ -195,19 +195,16 @@ End-to-End Testing Setup ===== -Currently, testing the BEAT web platform with Protractor requires additional setup after successfully setting up the project locally: +There are two system dependencies to run Selenium: -- Install Protractor +- Java 8 must be available in your PATH +- If you want to run the testing in a GNOME environment, you need `GConf <https://projects.gnome.org/gconf/>`_ - .. code:: bash - - ./parts/buildout-node/node-*/bin/npm i -g protractor - -- Download/update the webdriver-manager's dependencies (Selenium & more) +Download/update Protractor's dependencies into the local repository (Selenium & more): .. code:: bash - ./parts/buildout-node/node-*/bin/webdriver-manager update + ./bin/webdriver-manager update Running tests with the provided script ====================================== @@ -217,9 +214,9 @@ The ``protractor.sh`` script is a one-liner to run Protractor tests. It handles - It is being ran in the top directory of the ``beat.web`` repository - The repository has already ran ``./bin/buildout`` successfully and with default development configuration - Protractor's ``.conf`` file is ``./protractor-conf.js`` -- Default (no) additional arguments passed to ``webdriver-manager`` or Django ``runserver`` +- No additional arguments need to be passed to ``webdriver-manager`` or Django ``runserver`` - Django uses ``./django.sql3`` as the database -- If ``./django.sql3`` does not exist, the default database generated by ``./bin/django install`` is sufficient for testing +- If ``./template.django.sql3`` does not exist, the default database generated by ``./bin/django install`` is sufficient for testing the basic tests. However, some tests will fail and it is suggested to provide a database with experiments that have been ran successfully. Manual test running =================== @@ -235,7 +232,7 @@ _____________________________ .. code:: bash - ./parts/buildout-node/node-*/bin/webdriver-manager start + ./bin/webdriver-manager start .. important:: @@ -245,7 +242,7 @@ _____________________________ .. code:: bash - ./parts/buildout-node/node-*/bin/protractor protractor-conf.js + ./bin/protractor protractor-conf.js - If you started your webdriver server as a background process, you can kill all webdriver processes @@ -291,6 +288,27 @@ For example, to add the test file ``example-spec.js``: 'example-spec.js' ], +Overriding Protractor's browser choices +======================================= + +In ``protractor-conf.js``, add a ``multiCapabilities`` option in the following format: + +.. code:: javascript + + multiCapabilities: [ + { + browserName: '<browser name 1>' + }, + { + browserName: '<browser name 2>' + }, + ... + ] + +.. note:: + + You may need to download your browsers' WebDrivers separately - see `the official Selenium docs <https://seleniumhq.github.io/docs/wd.html#quick_reference>`_. + Writing Protractor tests ======================== diff --git a/protractor-conf.js b/protractor-conf.js index 00bf9926fe894fb31d4e4a27af2f3ea9dfb5934e..b775b755b9dd6fadfa6f49e7e6991e5e596b7824 100644 --- a/protractor-conf.js +++ b/protractor-conf.js @@ -7,5 +7,18 @@ exports.config = { ], allScriptsTimeout: 60000, + // to override the default browser that Protractor uses: + // give a list of browsers to use (firefox, chrome, safari, etc.) + // e.g.: + //multiCapabilities: [{ + // browserName: 'firefox' + //}, + //{ + // browserName: 'chrome' + //}], + + // disable some selenium~browser notification than can steal focus + notify: false, + //resultJsonOutputFile: './protractor-test-results.json' } diff --git a/protractor.sh b/protractor.sh index 87ee1bc03530f34c944da88a0c0a32b9922960ff..81a00e11991d9f63d9f9cef80c5e38dcc0fd7866 100755 --- a/protractor.sh +++ b/protractor.sh @@ -15,6 +15,9 @@ # speed up testing. ############################################################################### +# clean up any type of exit +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT + # make sure the beat web server and web manager arent running already if [ $(ps aux | grep -c 'django runserver') -gt 1 ] then @@ -50,9 +53,9 @@ fi # run the web server beat_cmd='./bin/django runserver' # spin up web manager -webdriver_cmd='./parts/buildout-node/node-*/bin/webdriver-manager start' +webdriver_cmd='./bin/webdriver-manager start' # run tests -protractor_cmd='./parts/buildout-node/node-*/bin/protractor ./protractor-conf.js' +protractor_cmd='./bin/protractor ./protractor-conf.js' # start bg processes echo 'Output from the BEAT web server &' \ @@ -78,6 +81,3 @@ then echo 'restoring old database...' mv old.django.sql3 django.sql3 fi - -# clean up any type of exit -trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT