diff --git a/beat/web/accounts/static/accounts/js/dialogs.js b/beat/web/accounts/static/accounts/js/dialogs.js
index 2e65ddcafc38b49acc4dedc80fe87c2a38c57b35..1d14883c8f93bcb8b071a2ed9dce9512bf466556 100644
--- a/beat/web/accounts/static/accounts/js/dialogs.js
+++ b/beat/web/accounts/static/accounts/js/dialogs.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/.
 */
diff --git a/beat/web/algorithms/static/algorithms/js/editor.js b/beat/web/algorithms/static/algorithms/js/editor.js
index 33654c6dc1473e7f8574fb8655f492e6f199454c..c06bc00b8ce2bccabca38aeffe108641f8f1f4bf 100644
--- a/beat/web/algorithms/static/algorithms/js/editor.js
+++ b/beat/web/algorithms/static/algorithms/js/editor.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/.
 */
diff --git a/beat/web/algorithms/static/algorithms/js/models.js b/beat/web/algorithms/static/algorithms/js/models.js
index 1b12b6416f797e692d5b64a6bd205c183239f045..ded7a76310c1092ea1db3f9da20a51787ef70ee3 100644
--- a/beat/web/algorithms/static/algorithms/js/models.js
+++ b/beat/web/algorithms/static/algorithms/js/models.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/.
 */
diff --git a/beat/web/attestations/static/attestations/js/dialogs.js b/beat/web/attestations/static/attestations/js/dialogs.js
index 51e46781bbf2a42ef22ab9312a3c2d9fe726aa87..e9e918bbe8fec9fc260a35ebb75707aa4710fd3f 100644
--- a/beat/web/attestations/static/attestations/js/dialogs.js
+++ b/beat/web/attestations/static/attestations/js/dialogs.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/.
 */
diff --git a/beat/web/backend/static/backend/js/models.js b/beat/web/backend/static/backend/js/models.js
index 33ef15c7411938b9598826eb5d399cd95e86d4c1..6f2ed43d716c470410ef62e22fd4b553d7777840 100644
--- a/beat/web/backend/static/backend/js/models.js
+++ b/beat/web/backend/static/backend/js/models.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/.
 */
diff --git a/beat/web/databases/static/databases/js/models.js b/beat/web/databases/static/databases/js/models.js
index adb2c016a5ef8e55418006d2e3e3953d9351a5d3..c11024808aed5a9a0281495a46697776df1d06f2 100644
--- a/beat/web/databases/static/databases/js/models.js
+++ b/beat/web/databases/static/databases/js/models.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/.
 */
diff --git a/beat/web/databases/static/databases/js/utils.js b/beat/web/databases/static/databases/js/utils.js
index 6d6a6a7ee98002908c42ef13446c1af77be577de..463786d7c30a3e4d638349c9d1a1d14b3d8db769 100644
--- a/beat/web/databases/static/databases/js/utils.js
+++ b/beat/web/databases/static/databases/js/utils.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/.
 */
diff --git a/beat/web/dataformats/static/dataformats/js/models.js b/beat/web/dataformats/static/dataformats/js/models.js
index afa6de56fc144ff699e2f0702054d6ee936eccf3..cd3e0054a556bd41c9dc42fdb621f972d6a6d52a 100644
--- a/beat/web/dataformats/static/dataformats/js/models.js
+++ b/beat/web/dataformats/static/dataformats/js/models.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/.
 */
diff --git a/beat/web/experiments/static/experiments/js/dialogs.js b/beat/web/experiments/static/experiments/js/dialogs.js
index 67c2b709250a0a32774553dd06a66e9fc16570d9..e87121f51b97784824865ba9fe9f0b43b5927596 100644
--- a/beat/web/experiments/static/experiments/js/dialogs.js
+++ b/beat/web/experiments/static/experiments/js/dialogs.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/.
 */
diff --git a/beat/web/experiments/static/experiments/js/utils.js b/beat/web/experiments/static/experiments/js/utils.js
index eb79e93af5de7894a730bcef95414b32cdf387d6..eb56f56fb53b2774a3e7cb41e06fb261c702e60f 100644
--- a/beat/web/experiments/static/experiments/js/utils.js
+++ b/beat/web/experiments/static/experiments/js/utils.js
@@ -54,884 +54,884 @@ if (beat.experiments.utils === undefined) beat.experiments.utils = {};
 // }
 //------------------------------------------------------------------------------
 beat.experiments.utils.displayPlot = function(
-	prefix,
-	container,
-	value,
-	available_plotters,
-	available_plotter_parameter,
-	replace_container_content,
-	callback
+    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');
-
-				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');
-
-		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);
-	}
+    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');
+
+                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');
+
+        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
+    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);
-	}
+    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';
-	}
+    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 = [];
+    var block_inputs_list = [];
 
-	for (var i = 0; i < block.nbInputs(); ++i) {
-		var input = block.inputs[i];
+    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,
-		});
-	}
+        block_inputs_list.push({
+            name: input.name,
+            dataformat: input.connections[0].output.dataformat,
+            channel: input.channel,
+        });
+    }
 
-	return beat.experiments.utils.getEntryPointsSignature(block_inputs_list);
+    return beat.experiments.utils.getEntryPointsSignature(block_inputs_list);
 };
 
 //-----------------------------------------------
 
 beat.experiments.utils.getBlockOutputSignature = function(block) {
-	var block_outputs_list = [];
+    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,
-		});
-	}
+        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);
+    return beat.experiments.utils.getEntryPointsSignature(block_outputs_list);
 };
 
 //-----------------------------------------------
 
 beat.experiments.utils.getEntryPointsSignature = function(entry_points) {
-	var signature = {};
+    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;
-	}
+        if (signature[entry.channel][entry.dataformat] !== undefined)
+            signature[entry.channel][entry.dataformat] += 1;
+        else signature[entry.channel][entry.dataformat] = 1;
+    }
 
-	return signature;
+    return signature;
 };
 
 //-----------------------------------------------
 
 beat.experiments.utils.analyzeCompatibility = function(
-	block_inputs_signature,
-	block_outputs_signature,
-	algorithm_inputs_signature,
-	algorithm_outputs_signature,
-	dataformats_list
+    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;
+    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
+    fixed,
+    extendable,
+    dataformats_list
 ) {
-	// Clone the signatures to have a modifiable version
-	fixed = JSON.parse(JSON.stringify(fixed));
-	extendable = JSON.parse(JSON.stringify(extendable));
+    // 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);
+    // 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 (fixed_names.length == 1 && fixed_names[0] == 'null') break;
 
-		if (extendable_names.length == 1 && extendable_names[0] == 'null') break;
+        if (extendable_names.length == 1 && extendable_names[0] == 'null') break;
 
-		for (var k = 0; k < extendable_names.length; ++k) {
-			var dataformat_name = extendable_names[k];
+        for (var k = 0; k < extendable_names.length; ++k) {
+            var dataformat_name = extendable_names[k];
 
-			if (dataformat_name == 'null') continue;
+            if (dataformat_name == 'null') continue;
 
-			if (fixed_names.indexOf(dataformat_name) >= 0) {
-				var nb = Math.min(fixed[dataformat_name], extendable[dataformat_name]);
+            if (fixed_names.indexOf(dataformat_name) >= 0) {
+                var nb = Math.min(fixed[dataformat_name], extendable[dataformat_name]);
 
-				fixed[dataformat_name] -= nb;
-				extendable[dataformat_name] -= nb;
+                fixed[dataformat_name] -= nb;
+                extendable[dataformat_name] -= nb;
 
-				if (fixed[dataformat_name] == 0) fixed[dataformat_name] = undefined;
+                if (fixed[dataformat_name] == 0) fixed[dataformat_name] = undefined;
 
-				if (extendable[dataformat_name] == 0)
-					extendable[dataformat_name] = undefined;
-			}
+                if (extendable[dataformat_name] == 0)
+                    extendable[dataformat_name] = undefined;
+            }
 
-			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 (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];
 
-					extendable[dataformat_name] = undefined;
-				} else {
-					return false;
-				}
-			}
+                    extendable[dataformat_name] = undefined;
+                } else {
+                    return false;
+                }
+            }
 
-			fixed = JSON.parse(JSON.stringify(fixed));
-			extendable = JSON.parse(JSON.stringify(extendable));
-		}
-	}
+            fixed = JSON.parse(JSON.stringify(fixed));
+            extendable = JSON.parse(JSON.stringify(extendable));
+        }
+    }
 
-	var fixed_names = Object.keys(fixed);
-	var extendable_names = Object.keys(extendable);
+    var fixed_names = Object.keys(fixed);
+    var extendable_names = Object.keys(extendable);
 
-	var fixed_nulls = 0;
-	var fixed_not_nulls = 0;
+    var fixed_nulls = 0;
+    var fixed_not_nulls = 0;
 
-	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]];
-	}
+    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 extended_nulls = 0;
-	var extended_not_nulls = 0;
+    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]];
-	}
+    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;
+    return fixed_nulls == extended_not_nulls && fixed_not_nulls == extended_nulls;
 };
 
 /******************************** CLASS: SmartDatasetList *******************************/
@@ -940,41 +940,41 @@ beat.experiments.utils.compareSignatures = function(
 // Constructor
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartDatasetList = function() {
-	// Attributes
-	this.databases = {};
+    // Attributes
+    this.databases = {};
 };
 
 //----------------------------------------------------------------------------------------
 // Add an entry into the list
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartDatasetList.prototype.add = function(
-	database,
-	protocol,
-	block,
-	dataset,
-	compatibility_infos
+    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
-	);
+    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
+    );
 };
 
 //----------------------------------------------------------------------------------------
@@ -982,426 +982,426 @@ beat.experiments.utils.SmartDatasetList.prototype.add = function(
 // database/protocol
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartDatasetList.prototype.finish = function(
-	nb_dataset_blocks
+    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;
+    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);
+    return Object.keys(this.databases);
 };
 
 //----------------------------------------------------------------------------------------
 // Returns the name of all the protocols of a specific database
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartDatasetList.prototype.protocolNames = function(
-	database
+    database
 ) {
-	if (this.databases[database] === undefined) return [];
+    if (this.databases[database] === undefined) return [];
 
-	return Object.keys(this.databases[database].protocols);
+    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
+    database,
+    name
 ) {
-	if (this.databases[database] === undefined) return null;
+    if (this.databases[database] === undefined) return null;
 
-	if (this.databases[database].protocols[name] === undefined) return null;
+    if (this.databases[database].protocols[name] === undefined) return null;
 
-	return this.databases[database].protocols[name];
+    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
+    database_name,
+    protocol_name,
+    block_name,
+    dataset,
+    usable
 ) {
-	var database = this.databases[database_name];
-	if (database === undefined) return;
+    var database = this.databases[database_name];
+    if (database === undefined) return;
 
-	var protocol = database.protocols[protocol_name];
-	if (protocol === undefined) return;
+    var protocol = database.protocols[protocol_name];
+    if (protocol === undefined) return;
 
-	var block = protocol.blocks[block_name];
-	if (block === undefined) return;
+    var block = protocol.blocks[block_name];
+    if (block === undefined) return;
 
-	if (block.set != dataset) return;
+    if (block.set != dataset) return;
 
-	if (usable != block.usable) {
-		block.usable = usable;
+    if (usable != block.usable) {
+        block.usable = usable;
 
-		if (usable) {
-			protocol.usable = null;
-			database.usable = null;
-		} else {
-			protocol.usable = false;
-			database.usable = null;
-		}
-	}
+        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
+    database_name,
+    protocol_name
 ) {
-	var database = this.databases[database_name];
-	if (database === undefined) return false;
+    var database = this.databases[database_name];
+    if (database === undefined) return false;
 
-	var protocol = database.protocols[protocol_name];
-	if (protocol === undefined) return false;
+    var protocol = database.protocols[protocol_name];
+    if (protocol === undefined) return false;
 
-	if (protocol.usable === null) {
-		protocol.usable = true;
+    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;
-			}
-		}
-	}
+        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;
+    return protocol.usable;
 };
 
 //----------------------------------------------------------------------------------------
 // Indicates if a database is usable (given the current configuration)
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartDatasetList.prototype.isDatabaseUsable = function(
-	database_name
+    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;
+    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
+    block_names,
+    protocol,
+    set_to_remove
 ) {
-	for (var i = 0; i < block_names.length; ++i) {
-		var block_name = block_names[i];
+    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);
+        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);
 
-				if (protocol.blocks[block_name].length == 0) return false;
+                if (protocol.blocks[block_name].length == 0) return false;
 
-				break;
-			}
-		}
-	}
+                break;
+            }
+        }
+    }
 
-	return true;
+    return true;
 };
 
 //----------------------------------------------------------------------------------------
 // Process all the dataset blocks for which only one dataset is compatible
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartDatasetList.prototype._processSingleMatches = function(
-	block_names,
-	protocol
+    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;
+    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
+    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;
+    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
+    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;
+    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,
+    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 = 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 *********************************/
@@ -1410,227 +1410,227 @@ beat.experiments.utils.SmartDatasetList.prototype._processBestMatches = function
 // Constructor
 //----------------------------------------------------------------------------------------
 beat.experiments.utils.SmartMapping = function(
-	possibilities,
-	block_inputs_signature,
-	block_outputs_signature,
-	algorithm_inputs_signature,
-	algorithm_outputs_signature
+    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;
+    // 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;
+    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();
+    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;
+    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();
+    return this.iteration_entries.pop();
 };
 
 //---------------------------------------------------------
 
 beat.experiments.utils.SmartMapping.prototype._removePossibility = function(
-	channel_names,
-	group_to_remove
+    channel_names,
+    group_to_remove
 ) {
-	for (var i = 0; i < channel_names.length; ++i) {
-		var channel = channel_names[i];
+    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);
+        for (var j = 0; j < this.possibilities[channel].length; ++j) {
+            if (this.possibilities[channel][j] == group_to_remove) {
+                this.possibilities[channel].splice(j, 1);
 
-				if (this.possibilities[channel].length == 0) return false;
+                if (this.possibilities[channel].length == 0) return false;
 
-				break;
-			}
-		}
-	}
+                break;
+            }
+        }
+    }
 
-	return true;
+    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;
+    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;
 };
 
 /**
@@ -1649,54 +1649,54 @@ beat.experiments.utils.SmartMapping.prototype._automaticMapping = function() {
  *   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();
+    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();
 };
 
 /**
@@ -1714,50 +1714,50 @@ beat.experiments.utils.modal_attest = function(name, url, redirect) {
  *   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();
+    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();
 };
 
 /**
@@ -1772,212 +1772,212 @@ beat.experiments.utils.modal_cancel = function(name, url, redirect) {
  *
  **/
 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;
-	});
+    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;
+    });
 };
 
 /**
@@ -1991,112 +1991,112 @@ beat.experiments.utils.modal_add_to_report = function(names, report_list_url) {
  *
  **/
 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();
-					},
-				},
-			],
-		});
-	});
+    $.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();
+                    },
+                },
+            ],
+        });
+    });
 };
 
 /**
@@ -2112,99 +2112,99 @@ beat.experiments.utils.modal_new_experiment = function(toolchain_list_url) {
  *
  **/
 beat.experiments.utils.modal_rename = function(
-	userid,
-	current_name,
-	list_url,
-	update_url
+    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;
-						});
-					},
-				},
-			],
-		});
-	});
+    $.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;
+                        });
+                    },
+                },
+            ],
+        });
+    });
 };
 
 /**
@@ -2222,21 +2222,21 @@ beat.experiments.utils.modal_rename = function(
  *     where the current block state is stored.
  */
 beat.experiments.utils.update_viewer = function(
-	viewer,
-	objects,
-	dt_block_name,
-	dt_block_status
+    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);
+    //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);
 };
 
 /**
@@ -2265,74 +2265,74 @@ beat.experiments.utils.update_viewer = function(
  *     representations and the toolchain viewer.
  */
 beat.experiments.utils.update_blocks = function(
-	url,
-	st,
-	dt,
-	unveil,
-	objects,
-	dt_block_name,
-	viewer,
-	interval
+    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);
+    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/static/plotters/app/app.config.js b/beat/web/plotters/static/plotters/app/app.config.js
index d3915a49737bb76e62bda9798fdc56b61e249557..b69fecfc898d374c6724d0ddeb86775156141989 100644
--- a/beat/web/plotters/static/plotters/app/app.config.js
+++ b/beat/web/plotters/static/plotters/app/app.config.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/.
 */
diff --git a/beat/web/plotters/static/plotters/app/app.js b/beat/web/plotters/static/plotters/app/app.js
index 58a518fb940735e52ab3b423dc682ec7c19661c0..1ee6f5086a2de4a3c083e54cb7b64ae58fa9974c 100644
--- a/beat/web/plotters/static/plotters/app/app.js
+++ b/beat/web/plotters/static/plotters/app/app.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/.
 */
diff --git a/beat/web/plotters/static/plotters/app/controllers/plotterparameterController.js b/beat/web/plotters/static/plotters/app/controllers/plotterparameterController.js
index 462a73a7f61bfecb0586829ecfe9de16ca455fba..3bd323dce134d899a3a3b09936a63cf306ef7e7c 100644
--- a/beat/web/plotters/static/plotters/app/controllers/plotterparameterController.js
+++ b/beat/web/plotters/static/plotters/app/controllers/plotterparameterController.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/.
 */
@@ -101,7 +101,7 @@ app.controller('plotterparameterController',['$scope', 'plotterFactory', 'plotte
                             return false;
                       }
                     });
-                    
+
                     $.each($scope.plotters.selected.declaration.parameters, function( key, value ) {
                         //push all keys in array
                         $scope.textdata.push(key);
diff --git a/beat/web/plotters/static/plotters/app/directives/plotterparameterItemView.js b/beat/web/plotters/static/plotters/app/directives/plotterparameterItemView.js
index 3afd5c7b30678a2203d06fa69eb6a5254f42c1a1..d2bea19b5737f773ff24605c1a3340a125d3b3b3 100644
--- a/beat/web/plotters/static/plotters/app/directives/plotterparameterItemView.js
+++ b/beat/web/plotters/static/plotters/app/directives/plotterparameterItemView.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/.
 */
@@ -635,7 +635,7 @@ app.directive("buttonplusminus", function()
 //Directive used to handle save plotterparameter click
 app.directive("plotterparameteritems", function($compile)
 {
-	return function(scope, element, attrs)
+    return function(scope, element, attrs)
     {
         scope.$on("addParametersElement", function(event)
         {
@@ -670,7 +670,7 @@ app.directive("plotterparameteritems", function($compile)
 //Directive used to handle dynamic testing on graph display
 app.directive("testplotterparameters", function($compile)
 {
-	return function(scope, element, attrs)
+    return function(scope, element, attrs)
     {
         element.bind("click", function()
         {
diff --git a/beat/web/plotters/static/plotters/app/factories/plotterFactory.js b/beat/web/plotters/static/plotters/app/factories/plotterFactory.js
index 9babc79f3031c1d6fd58fd1405fefce1dce3ed33..f87fc671258ea5fb2da7d44f83325e6beaaf5d49 100644
--- a/beat/web/plotters/static/plotters/app/factories/plotterFactory.js
+++ b/beat/web/plotters/static/plotters/app/factories/plotterFactory.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/.
 */
diff --git a/beat/web/plotters/static/plotters/app/factories/plotterparameterFactory.js b/beat/web/plotters/static/plotters/app/factories/plotterparameterFactory.js
index d6292c515f27646969a63c3f2448dcca1fa9d8b1..2dd9480de3ba0b8357859f471e91bbfb7cf512fd 100644
--- a/beat/web/plotters/static/plotters/app/factories/plotterparameterFactory.js
+++ b/beat/web/plotters/static/plotters/app/factories/plotterparameterFactory.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/.
 */
diff --git a/beat/web/plotters/static/plotters/js/dialogs.js b/beat/web/plotters/static/plotters/js/dialogs.js
index 59fa6bc647b617d4f31b740d1933faf044f483db..f74d7dc0ec7cfd5986139eed0706a68c24f6e7f4 100644
--- a/beat/web/plotters/static/plotters/js/dialogs.js
+++ b/beat/web/plotters/static/plotters/js/dialogs.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/.
 */
diff --git a/beat/web/plotters/static/plotters/js/new_plotterparameter_dialog.js b/beat/web/plotters/static/plotters/js/new_plotterparameter_dialog.js
index b527b723c544f5ea21edc9f46ceb53edfa05b970..c74462730c66e2e990115c8aebede0b074485bfd 100644
--- a/beat/web/plotters/static/plotters/js/new_plotterparameter_dialog.js
+++ b/beat/web/plotters/static/plotters/js/new_plotterparameter_dialog.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/.
 */
diff --git a/beat/web/reports/static/reports/app/app.config.js b/beat/web/reports/static/reports/app/app.config.js
index 0896cf9b7006508d69f8198af583c1126751c86f..124cae4ab1d0bd603c354abaf95841ab223b24fd 100644
--- a/beat/web/reports/static/reports/app/app.config.js
+++ b/beat/web/reports/static/reports/app/app.config.js
@@ -20,13 +20,13 @@
  * with the BEAT platform. If not, see http://www.gnu.org/licenses/.
  */
 angular.module('reportApp').config(function configureStartEndSymbol($interpolateProvider) {
-	$interpolateProvider.startSymbol('{$').endSymbol('$}');
+    $interpolateProvider.startSymbol('{$').endSymbol('$}');
 }
 );
 
 angular.module('reportApp').config(function configHttp($httpProvider) {
-	$httpProvider.defaults.xsrfCookieName = 'csrftoken';
-	$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
-	$httpProvider.defaults.withCredentials = true;
+    $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 663256161a1b9cfeaeda43da27c3ffb0befa8836..effd85f405f1cf5942139b1bc2ea098dc540e5bf 100644
--- a/beat/web/reports/static/reports/app/app.js
+++ b/beat/web/reports/static/reports/app/app.js
@@ -23,13 +23,13 @@ angular.module('reportApp', ['ui.router', 'angular.filter', 'ui.sortable', 'ui.c
 
 
 angular.module('reportApp').config(function ($stateProvider, $urlRouterProvider){
-	$urlRouterProvider
-	.otherwise('/');
+    $urlRouterProvider
+    .otherwise('/');
 
-	$stateProvider
-	.state('report', {
-		url: '/',
-		views: {
-		}
-	});
+    $stateProvider
+    .state('report', {
+        url: '/',
+        views: {
+        }
+    });
 });
diff --git a/beat/web/reports/static/reports/app/controllers/groupsController.js b/beat/web/reports/static/reports/app/controllers/groupsController.js
index 0c9c91eb5452a3d1399a69e0506d9cf8d43653ea..67f6fc41475a40a60e982ee1db14599ca2945afa 100644
--- a/beat/web/reports/static/reports/app/controllers/groupsController.js
+++ b/beat/web/reports/static/reports/app/controllers/groupsController.js
@@ -22,37 +22,37 @@
 
 /*
  * GroupsController
- * 	provides access to the groups data to Django templates,
- * 	used for handling the removal of experiments from the report
+ *  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;
+    let vm = this;
 
-	vm.expNamesToRemove = [];
-	vm.toggleExpName = (expName) => {
-		let idx = vm.expNamesToRemove.indexOf(expName);
-		if(idx > -1){
-			vm.expNamesToRemove.splice(idx, 1);
-		} else {
-			vm.expNamesToRemove.push(expName);
-		}
-	};
+    vm.expNamesToRemove = [];
+    vm.toggleExpName = (expName) => {
+        let idx = vm.expNamesToRemove.indexOf(expName);
+        if(idx > -1){
+            vm.expNamesToRemove.splice(idx, 1);
+        } else {
+            vm.expNamesToRemove.push(expName);
+        }
+    };
 
-	vm.removeExperiments = () => {
-		if(vm.expNamesToRemove.length === 0){
-			return;
-		}
+    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())
-		;
-	};
+        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 64b783d7bd56d685468883c979fbeafe2dc4f29d..e6c611838380c48b7edca5925cf295fa7997da2b 100644
--- a/beat/web/reports/static/reports/app/controllers/reportController.js
+++ b/beat/web/reports/static/reports/app/controllers/reportController.js
@@ -21,7 +21,7 @@
  */
 
 /* reportController
- * 	bootstraps the reports angular code by requiring the ReportService.
- * 	NOTE: DONT add any code to this controller.
+ *  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/directives/bootstrapModal.js b/beat/web/reports/static/reports/app/directives/bootstrapModal.js
index 3a60c59e9f31fc0a32279295071eaea48e93b39d..05d1ca3790f8a7f715b979989ea15ac39c9e5191 100644
--- a/beat/web/reports/static/reports/app/directives/bootstrapModal.js
+++ b/beat/web/reports/static/reports/app/directives/bootstrapModal.js
@@ -23,47 +23,47 @@
 /*
  * bootstrapModal
  * Desc:
- * 	represents a modal from Bootstrap 3
+ *  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: `
+    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">&times;</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 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">&times;</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
index 74733f12af922559ff6bd66723151aa0d67f8da3..78058d1b2b79513b4187fb70b77a9ac9664991a8 100644
--- a/beat/web/reports/static/reports/app/directives/downloadLink.js
+++ b/beat/web/reports/static/reports/app/directives/downloadLink.js
@@ -23,53 +23,53 @@
 /*
  * downloadLink
  * Desc:
- * 	A button to download the content of the report item in the
- * 	chosen file format (PNG, JPEG, PDF)
+ *  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'
-			];
+    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`);
+            // 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: `
+                    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>
+    <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/directives/dragHandle.js b/beat/web/reports/static/reports/app/directives/dragHandle.js
index 4eb0fce3646b9fc1caaccfb8a4814dfb6f2e8a74..a954ce0d20998bc99200a9706ba06ffa9b12ed07 100644
--- a/beat/web/reports/static/reports/app/directives/dragHandle.js
+++ b/beat/web/reports/static/reports/app/directives/dragHandle.js
@@ -23,24 +23,24 @@
 /*
  * dragHandle
  * Desc:
- * 	displays the drag handle button, and adds the specified element class
- * 	to the parent el
+ *  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: `
+    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>
+    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
index dcefe7f83a0fedd4b9110c930596efdfe8f8e5bf..9df0546557110cdf9377a6e592dbf52598eb67ca 100644
--- a/beat/web/reports/static/reports/app/directives/edit/addGroupMenu.js
+++ b/beat/web/reports/static/reports/app/directives/edit/addGroupMenu.js
@@ -23,48 +23,48 @@
 /*
  * groupAddGroupMenu
  * Desc:
- * 	the menu & validating for creating a new group
+ *  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;
-			};
+    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;
-				}
+            // 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: `
+                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>
+    <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
index a2c9a11be423a5925c2da1f3d01d63e09eefa2a6..d9ea7e3e6af9b039ca47d3072ab7ef0739837e0f 100644
--- a/beat/web/reports/static/reports/app/directives/edit/addItemsMenu.js
+++ b/beat/web/reports/static/reports/app/directives/edit/addItemsMenu.js
@@ -23,129 +23,129 @@
 /*
  * groupAddItemsMenu
  * Desc:
- * 	the button group for adding report items (plot, table, text) to a group
+ *  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 {
+        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;
-			};
+                return nextId;
+            };
 
-			scope.plottables = ExperimentsService.plottables;
+            scope.plottables = ExperimentsService.plottables;
 
-			// helper func for adding a table
-			scope.addNewTable = () => {
-				const id = getNextItemId('table');
+            // 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())
-				;
+                // 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);
+                const defaultFields = Array.from(defaultFieldsSet);
 
-				// tables have an arr of selected fields
-				// and a float precision
-				let content = {
-					itemName: `Table`,
-					fields: defaultFields,
-					precision: 10
-				};
+                // tables have an arr of selected fields
+                // and a float precision
+                let content = {
+                    itemName: `Table`,
+                    fields: defaultFields,
+                    precision: 10
+                };
 
-				scope.group.addReportItem(id, content);
-			};
+                scope.group.addReportItem(id, content);
+            };
 
-			// helper func for adding a plot
-			scope.addNewPlot = (plot) => {
-				let id = getNextItemId('plot');
+            // 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: ''
-				};
+                // 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;
+                content.savedPlotter = PlotService.getPlotter(content).name;
+                content.savedConfig = PlotService.getPlotterConfig(content).name;
 
-				scope.group.addReportItem(id, 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: ''
-				};
+            // 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: `
+                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>
+    <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
+    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>
+    <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
index e49a45f8106792afa554c66a00dbe007b21d2d71..e621ce683958626e92c4544d9e46b7dad454a93f 100644
--- a/beat/web/reports/static/reports/app/directives/edit/tableFieldSelector.js
+++ b/beat/web/reports/static/reports/app/directives/edit/tableFieldSelector.js
@@ -23,177 +23,177 @@
 /*
  * GroupTableFieldSelector
  * Desc:
- * 	Handles the choosing of table columns
+ *  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()();
-			};
+    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], [])
-				;
+            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));
+                // converting to and from a Set is a simple way of
+                // removing dups
+                const arr = Array.from(new Set(fieldArr));
 
-				return arr;
-			};
+                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}`;
+            // 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)
-				;
+                // 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;
-			};
+                // 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]);
+            // 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))
+                const sorted = Array.from(new Set(groupNames))
+                .sort((a, b) => groupNames.indexOf(a) - groupNames.indexOf(b))
 
-				return sorted;
-			};
+                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.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;
+            // 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;
+            scope.shouldShowField = (fName, gName) => {
+                const isUnique = scope.isUniqueTableable(fName);
+                const isInGroup = scope.groupName(fName) == gName;
 
-				return isUnique && 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: `
+            // 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>
+    {{ 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>
+    <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/static/reports/app/directives/editableLabel.js b/beat/web/reports/static/reports/app/directives/editableLabel.js
index 01f9eecd3ef2664dba66a86995ad93f1a916b858..dbc5b9ccd01fb50eed671533acc9eb29760a4a97 100644
--- a/beat/web/reports/static/reports/app/directives/editableLabel.js
+++ b/beat/web/reports/static/reports/app/directives/editableLabel.js
@@ -23,30 +23,30 @@
 /*
  * editableLabel
  * Desc:
- * 	represents an editable label (group name, report item name, etc.)
+ *  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: `
+    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 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>
 `
-	};
+    };
 }]);
 
diff --git a/beat/web/reports/static/reports/app/directives/error.js b/beat/web/reports/static/reports/app/directives/error.js
index a8759663eef6136d0f7bc6580a96766d7be2d0dc..1a1116e150d34eafccb52a545e3376e5d07ce8e1 100644
--- a/beat/web/reports/static/reports/app/directives/error.js
+++ b/beat/web/reports/static/reports/app/directives/error.js
@@ -23,66 +23,66 @@
 /*
  * 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.
+ *  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: ''
-			};
+    return {
+        scope: {
+        },
+        restrict: 'E',
+        link: function(scope){
+            scope.currError = {
+                message: '',
+                error: ''
+            };
 
-			const errors = [];
+            const errors = [];
 
-			const processError = () => {
-				const modalOpen = ($("#errorReportModal").data('bs.modal') || {}).isShown;
-				if(modalOpen || errors.length === 0){
-					return;
-				}
+            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();
+                // 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();
-			};
+                // pop up the modal
+                $('#errorReportModal').modal();
+            };
 
-			$timeout(() => {
-				$('#errorReportModal').on('hidden.bs.modal', () => {
-					// finished processing the last error
-					// process the next one
-					processError();
-				});
-			}, 0);
+            $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: `
+            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>
+    <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
index 7ff2edbfbb3706554bf365bbcef3bcb17fd7e67d..9b96493927af12f866efc432564e2daadea13625 100644
--- a/beat/web/reports/static/reports/app/directives/experimentsTable.js
+++ b/beat/web/reports/static/reports/app/directives/experimentsTable.js
@@ -23,116 +23,116 @@
 /*
  * experimentsTable
  * Desc:
- * 	displays the report's experiments table
- * 	- Experiment statuses
- * 	- Name
- * 	- Databases/Protocols
- * 	- Analyzer
+ *  displays the report's experiments table
+ *  - Experiment statuses
+ *  - Name
+ *  - Databases/Protocols
+ *  - Analyzer
  *
- * 	also lets the user remove experiments from the report.
+ *  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;
+    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.expNames = ExperimentsService.experimentNames;
+            scope.exps = ExperimentsService.experiments;
 
-			scope.groups = GroupsService.groups;
+            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}`)));
-			};
+            // 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: `
+            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>
-								&nbsp;
-							</span>
-						</td>
-						<td>{{ getAnalyzerFromExpName(expName) }}</td>
-					</tr>
-				</tbody>
-			</table>
+    <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>
+                                &nbsp;
+                            </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 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
index f32d9a5271abcba1d23fe006a55a0ed0e1b934b9..27ae4d1896269620ee4490e95caa78e27e59a9fc 100644
--- a/beat/web/reports/static/reports/app/directives/layout.js
+++ b/beat/web/reports/static/reports/app/directives/layout.js
@@ -23,44 +23,44 @@
 /*
  * groupsLayout
  * Desc:
- * 	controls the layout of the reports content,
- * 	generating group panels using the GroupsService data,
- * 	and holding the menu for adding a group
+ *  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;
+    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: `
+            // 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 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
index ead9538a1da8b32f5c2e248866a38bb3c0104fc2..73b7f687c961e04996f1f8e8b405cd608f0f2cea 100644
--- a/beat/web/reports/static/reports/app/directives/lock.js
+++ b/beat/web/reports/static/reports/app/directives/lock.js
@@ -23,37 +23,37 @@
 /*
  * reportLock
  * Desc:
- * 	Displays a modal for locking the current report.
+ *  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: `
+    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>
+    <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>
+    </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
index e971fe661eafad4686000287745cbda4d403c313..7f4f843a2d127e647be203784e16be0b1dd7400c 100644
--- a/beat/web/reports/static/reports/app/directives/panelContainer.js
+++ b/beat/web/reports/static/reports/app/directives/panelContainer.js
@@ -23,46 +23,46 @@
 /*
  * panelContainer
  * Desc:
- * 	displays a Bootstrap panel with the specified header & body content
+ *  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: `
+    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>
+    <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>
+    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
index 9f6319b036b2e8e4c739cc3502f38410a8d73bed..67e54aac7fea7d6d4fa57fe4237afd5d719fb28e 100644
--- a/beat/web/reports/static/reports/app/directives/panelContent.js
+++ b/beat/web/reports/static/reports/app/directives/panelContent.js
@@ -23,61 +23,61 @@
 /*
  * 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
+ *  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: `
+    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>
+    <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
index 32326886e59ba01b32c1195c75158dc1dca8290c..5ed6d3e2dfdbf9bb207dcfecfa3c3bef53003a5e 100644
--- a/beat/web/reports/static/reports/app/directives/panelExperiments.js
+++ b/beat/web/reports/static/reports/app/directives/panelExperiments.js
@@ -23,132 +23,132 @@
 /*
  * 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.
+ *  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`;
+    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()];
+            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)
-				;
-			};
+            // 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;
-				}
+            // 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}`)));
-			};
+                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: `
+            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>
+    <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>
-							&nbsp;
-						</span>
-					</td>
-				</tr>
-			</tbody>
-		</table>
-	</div>
+    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>
+                            &nbsp;
+                        </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
index 358862dd6011b54b7979a23511a80f8491d41a08..28df1fdd08b30633950e01e978f69deae9e06fcc 100644
--- a/beat/web/reports/static/reports/app/directives/panelItems.js
+++ b/beat/web/reports/static/reports/app/directives/panelItems.js
@@ -23,48 +23,48 @@
 /*
  * groupPanelItems
  * Desc:
- * 	displays the panel of report items of the group,
- * 	using the item container adaptor
+ *  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: `
+    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 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
index 4c7655b1ba4429d745673cf24f0112a195eca054..36039d8308359bf267c0e52df6f93ac1a245d191 100644
--- a/beat/web/reports/static/reports/app/directives/plotItem.js
+++ b/beat/web/reports/static/reports/app/directives/plotItem.js
@@ -23,132 +23,132 @@
 /*
  * groupPlotItem
  * Desc:
- * 	displays a plot report item (basically a container for the plots code to insert into)
+ *  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}`;
+    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`;
+            // 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;
-			};
+            // 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);
-			});
+            // 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;
+            let plotTimer;
 
-			const updatePlot = () => {
-				clearTimeout(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);
-					}
-				};
+                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);
-			};
+                plotTimer = setTimeout(queueUpdate, 1000);
+            };
 
-			// if the user selected different plotter or config, rerender
-			scope.$watch(
-				() => `${scope.content.savedPlotter}|${scope.content.savedConfig}`,
-				updatePlot
-			);
+            // 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.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: `
+            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>
+    <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
index 96d295aa9b18f1146223dbd9fd9690555531d945..fe68cb35a868895f47e0fc6bef8b3adf1d037ac1 100644
--- a/beat/web/reports/static/reports/app/directives/publish.js
+++ b/beat/web/reports/static/reports/app/directives/publish.js
@@ -23,94 +23,94 @@
 /*
  * reportPublish
  * Desc:
- * 	Displays a modal for publishing the current report.
- * 	It also lets the user select which algorithms will be made
- * 	Visible or Public.
+ *  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 = {};
+    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]);
+            // 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);
-				})
-				;
-			};
+                    // 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);
+            $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);
+            // 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
-				});
+                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: `
+                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>
+    <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/save.js b/beat/web/reports/static/reports/app/directives/save.js
index 60d7c576b5fe3ab2c57f87367bdc8c2a9a769525..ebaadea9b70b50d7493da917a81b22f2f738932b 100644
--- a/beat/web/reports/static/reports/app/directives/save.js
+++ b/beat/web/reports/static/reports/app/directives/save.js
@@ -23,38 +23,38 @@
 /*
  * reportSave
  * Desc:
- * 	saves the current report
+ *  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){
+    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()
-					}
-				};
+            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.`);
-				});
-			};
+                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);
-		},
-	};
+            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
index e157b69f6039b476303bbedc5baa28ea4c27317b..036c1678d5727ca3cb4f2db75aa8fba4eb9d3834 100644
--- a/beat/web/reports/static/reports/app/directives/tableItem.js
+++ b/beat/web/reports/static/reports/app/directives/tableItem.js
@@ -23,271 +23,271 @@
 /*
  * tableItem
  * Desc:
- * 	displays a table report item and lets the user
- * 	manage this table's selected cols and float precision
+ *  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`;
+    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);
+            // 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);
+            // 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
+            // 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 newCols = scope.chosenCols
+                .filter(c => !scope.fields.includes(c));
 
-				const rmCols = scope.fields
-				.filter(f => !scope.chosenCols.includes(f) && f !== 'Experiment');
+                const rmCols = scope.fields
+                .filter(f => !scope.chosenCols.includes(f) && f !== 'Experiment');
 
-				rmCols.forEach(rf => scope.fields.splice(scope.fields.indexOf(rf), 1));
+                rmCols.forEach(rf => scope.fields.splice(scope.fields.indexOf(rf), 1));
 
-				newCols.forEach(nf => scope.fields.push(nf));
-			};
+                newCols.forEach(nf => scope.fields.push(nf));
+            };
 
-			// toggle val for viewing CSV
-			scope.isViewingCSV = { val: false };
-			scope.toggleViewingCSV = () => {
-				scope.isViewingCSV.val = !scope.isViewingCSV.val;
-			};
+            // toggle val for viewing CSV
+            scope.isViewingCSV = { val: false };
+            scope.toggleViewingCSV = () => {
+                scope.isViewingCSV.val = !scope.isViewingCSV.val;
+            };
 
-			// aliases
-			scope.fields = scope.content.fields;
+            // 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');
-			}
+            // 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 || {};
+            // 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';
-				}
+            // 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;
-				}
+                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;
+                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;
+                const name = scope.tableables[alias] ?
+                    alias : expName;
 
-				let fVal = scope.tableables[name] ? scope.tableables[name][field] : undefined;
-				let val;
+                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;
-					}
+                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;
-					}
-				}
+                    let type = scope.getFieldType(field);
+                    if(type && type.startsWith('float')){
+                        val = tmp.toFixed(parseInt(scope.content.precision));
+                    } else {
+                        val = tmp;
+                    }
+                }
 
-				return val;
-			};
+                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;
-				}
-			};
+            // 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))
-				;
+            // 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 str = '';
 
-				let fieldsStr = fields
-				.map(f => `${f}(${scope.getFieldType(f)})`)
-				.join(',');
+                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');
+                let expsStrs = exps
+                .map(e => fields.map(f => `${scope.getFieldVal(e, f)}`).join(','))
+                .join('\n');
 
-				str = `${fieldsStr}\n${expsStrs}`;
+                str = `${fieldsStr}\n${expsStrs}`;
 
-				return str;
-			};
+                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);
-				}
+            // 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;
-			};
+                // 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.sortableOptions = {
+                items: `th:not(:first-child)`
+            };
 
-			scope.isViewmode = UrlService.isViewmode;
-		},
-		template: `
+            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>
+    <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
index 72a2c9967bb523a1fdefbad820dc022c0748a2b5..e1879d5126a89f6825121e1671be2722a029dcab 100644
--- a/beat/web/reports/static/reports/app/directives/textItem.js
+++ b/beat/web/reports/static/reports/app/directives/textItem.js
@@ -23,130 +23,130 @@
 /*
  * 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
+ *  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}`;
+    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;
+            scope.isViewmode = UrlService.isViewmode;
 
-			// codemirror options
-			scope.uicmOptions = {
-				mode: 'rst',
-				readOnly: scope.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)}`;
+            // 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;
-				});
-			};
+                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;
-			};
+            // 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: `
+            // 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>
+    <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 6ef4f0b32436f78da2d77a8a02909ce5f88134b6..630ce188b65e4d658421b4f1f3692414b91e30eb 100644
--- a/beat/web/reports/static/reports/app/factories/experimentFactory.js
+++ b/beat/web/reports/static/reports/app/factories/experimentFactory.js
@@ -21,29 +21,29 @@
  */
 //This factory retrieves data from the REST API and associates it with the $scope
 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;
-			});
-		},
+    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 232d70245363a5950baef16039f5f47ec3b2302f..705093f8d2ac8a32494706abeb5b350cd7770995 100644
--- a/beat/web/reports/static/reports/app/factories/plotterFactory.js
+++ b/beat/web/reports/static/reports/app/factories/plotterFactory.js
@@ -21,23 +21,23 @@
  */
 //This factory retrieves data from the REST API and associates it with the $scope
 angular.module('reportApp').factory('plotterFactory', ['$http', function($http){
-	let plotterFactory = {};
+    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 7c06a21430dc2f0bc97b98c32dea6ed08159d58a..1177ca93a9f1652c532ce92de643b544d70d198d 100644
--- a/beat/web/reports/static/reports/app/factories/reportFactory.js
+++ b/beat/web/reports/static/reports/app/factories/reportFactory.js
@@ -21,96 +21,96 @@
  */
 //This factory retrieves data from the REST API and associates it with the $scope
 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;
+    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/static/reports/app/services/errorService.js b/beat/web/reports/static/reports/app/services/errorService.js
index 562c8a3261e40dd61b42d84fb0ecc1dfdb88f1bf..cecadebc88ca0c0497ccaeabd67ed21cb7454670 100644
--- a/beat/web/reports/static/reports/app/services/errorService.js
+++ b/beat/web/reports/static/reports/app/services/errorService.js
@@ -23,36 +23,36 @@
 /*
  * 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.
+ *  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 = {
-	};
+    const es = {
+    };
 
-	class ReportError {
-		constructor (errorObj, message) {
-			this._error = errorObj;
-			this._message = message || '';
-		}
+    class ReportError {
+        constructor (errorObj, message) {
+            this._error = errorObj;
+            this._message = message || '';
+        }
 
-		get error () {
-			return this._error;
-		}
+        get error () {
+            return this._error;
+        }
 
-		get message () {
-			return this._message;
-		}
-	}
+        get message () {
+            return this._message;
+        }
+    }
 
-	es.logError = (error, message) => {
-		const newErr = new ReportError(error, message);
+    es.logError = (error, message) => {
+        const newErr = new ReportError(error, message);
 
-		$rootScope.$broadcast('user:error', newErr);
-	};
+        $rootScope.$broadcast('user:error', newErr);
+    };
 
-	return es;
+    return es;
 }]);
diff --git a/beat/web/reports/static/reports/app/services/experimentsService.js b/beat/web/reports/static/reports/app/services/experimentsService.js
index 8a9818da1b22e160c70c5b2ac9c01fcf3b3af7d4..87083a14477d8e5ea779bf2054d3d70a1edfc594 100644
--- a/beat/web/reports/static/reports/app/services/experimentsService.js
+++ b/beat/web/reports/static/reports/app/services/experimentsService.js
@@ -23,211 +23,211 @@
 /*
  * ExperimentsService
  * Desc:
- * 	Manages the experiments data, including storing it and generating
- * 	different views of the data
+ *  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);
-		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)){
-					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);
-				}
-			});
-
-			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
-	};
+    // 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);
+        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)){
+                    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);
+                }
+            });
+
+            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
index 29d6f6da8bb533630b5d29917d725f9fe49b1d89..bb77b6951a9219ceb0939a43ef70b5a24c4d6ac5 100644
--- a/beat/web/reports/static/reports/app/services/groupsService.js
+++ b/beat/web/reports/static/reports/app/services/groupsService.js
@@ -23,237 +23,237 @@
 /*
  * GroupsService
  * Desc:
- * 	The main datastore for the reports app, holding the tree of
- * 	relationships between groups, experiments, aliases, and report items
+ *  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));
-		});
-	};
-
-	return groupsServiceInstance;
+    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));
+        });
+    };
+
+    return groupsServiceInstance;
 }]);
diff --git a/beat/web/reports/static/reports/app/services/plotService.js b/beat/web/reports/static/reports/app/services/plotService.js
index 4a11fa0fc4a30ec6d53d695e2c473aa50e1ca1d1..e5b576d015c6212f3444c07d93dd4495795c58c4 100644
--- a/beat/web/reports/static/reports/app/services/plotService.js
+++ b/beat/web/reports/static/reports/app/services/plotService.js
@@ -23,198 +23,198 @@
 /*
  * PlotService
  * Desc:
- * 	Manages the plots in the report,
- * 	including:
- * 	- Adding new/saved
- * 	- Configuring
- * 	- Deleting
- * 	- Rendering
+ *  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);
-
-		// 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);
-
-		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;
-
-	return ps;
+    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);
+
+        // 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);
+
+        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;
+
+    return ps;
 }]);
diff --git a/beat/web/reports/static/reports/app/services/reportService.js b/beat/web/reports/static/reports/app/services/reportService.js
index ad68469fe0eaeb5ae31d6b571842bbc2640deff1..ff9a97f588ee5c9fe4bb9a76d7cf0758c39ee578 100644
--- a/beat/web/reports/static/reports/app/services/reportService.js
+++ b/beat/web/reports/static/reports/app/services/reportService.js
@@ -23,118 +23,118 @@
 /*
  * ReportService
  * Desc:
- * 	Consumes the "report" object from the API and digests it into helper
- * 	funcs and report-wide info. Basically an adaptor & bootstrap-er.
+ *  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) => {
-
-		// 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);
-
-		// 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.`));
-
-	return rs;
+    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) => {
+
+        // 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);
+
+        // 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.`));
+
+    return rs;
 }]);
diff --git a/beat/web/reports/static/reports/app/services/urlService.js b/beat/web/reports/static/reports/app/services/urlService.js
index f42d6af73be62de0ce0559909d5c5f0ac3578fda..861f52a65de39978167dcf88ea6ad543ba268fd6 100644
--- a/beat/web/reports/static/reports/app/services/urlService.js
+++ b/beat/web/reports/static/reports/app/services/urlService.js
@@ -23,78 +23,78 @@
 /*
  * UrlService
  * Desc:
- * 	Helper functionality to generate URLs for the reports app
+ *  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/';
+    // 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;
+    // 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(/#\/?$/, '');
+    // 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;
-		}
-	};
+        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 getPrefix = () => prefix;
 
-	const getApiSegment = () => apiSegment;
-	const getNameSegment = () => reportByName;
-	const getNumberSegment = () => reportByNumber;
+    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 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 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 getCompileRstUrl = () => `${getApiUrl(`${reportByName || reportByNumber}rst/`)}`;
+    const getRemoveExperimentUrl = () => `${getApiUrl(`${reportByName}remove/`)}`;
+    const getByNamePath = () => reportByName;
+    const getByNumberPath = () => reportByNumber;
+    const getExperimentListPath = () => experimentPath();
 
-	const isViewmode = () => reportByNumber ? true : false;
+    const isViewmode = () => reportByNumber ? true : false;
 
-	extractUsingCurrentUrl();
+    extractUsingCurrentUrl();
 
-	return {
-		getExperimentUrl,
-		getBlockUrl,
-		getDatabaseUrl,
-		getCompileRstUrl,
-		getRemoveExperimentUrl,
-		getByNamePath,
-		getByNumberPath,
-		getApiSegment,
-		getNameSegment,
-		getNumberSegment,
-		getPrefix,
-		getExperimentListPath,
-		isViewmode
-	};
+    return {
+        getExperimentUrl,
+        getBlockUrl,
+        getDatabaseUrl,
+        getCompileRstUrl,
+        getRemoveExperimentUrl,
+        getByNamePath,
+        getByNumberPath,
+        getApiSegment,
+        getNameSegment,
+        getNumberSegment,
+        getPrefix,
+        getExperimentListPath,
+        isViewmode
+    };
 }]);
diff --git a/beat/web/reports/static/reports/js/base_64_encoder_decoder.js b/beat/web/reports/static/reports/js/base_64_encoder_decoder.js
index 6d794c27d0f0ac96fdc2cf1165104e5e622046a3..ccf3564f365fc47eb8da9bdd08280c8329e7f7e8 100644
--- a/beat/web/reports/static/reports/js/base_64_encoder_decoder.js
+++ b/beat/web/reports/static/reports/js/base_64_encoder_decoder.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/.
 */
diff --git a/beat/web/reports/static/reports/js/download.js b/beat/web/reports/static/reports/js/download.js
index d2f3931926c3897266c68787126eee921abad1ce..14d1c8d8cd5fdf0ab30c8722b56d91734c15af80 100644
--- a/beat/web/reports/static/reports/js/download.js
+++ b/beat/web/reports/static/reports/js/download.js
@@ -1,137 +1,137 @@
-//download.js v4.0, by dandavis; 2008-2015. [CCBY2] see http://danml.com/download.html for tests/usage
-// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
-// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
-// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
-// v4 adds AMD/UMD, commonJS, and plain browser support
-// https://github.com/rndme/download
-
-(function (root, factory) {
-	if (typeof define === 'function' && define.amd) {
-		// AMD. Register as an anonymous module.
-		define([], factory);
-	} else if (typeof exports === 'object') {
-		// Node. Does not work with strict CommonJS, but
-		// only CommonJS-like environments that support module.exports,
-		// like Node.
-		module.exports = factory();
-	} else {
-		// Browser globals (root is window)
-		root.download = factory();
-  }
-}(this, function () {
-
-	return function download(data, strFileName, strMimeType) {
-
-		var self = window, // this script is only for browsers anyway...
-			u = "application/octet-stream", // this default mime also triggers iframe downloads
-			m = strMimeType || u,
-			x = data,
-			D = document,
-			a = D.createElement("a"),
-			z = function(a){return String(a);},
-			B = (self.Blob || self.MozBlob || self.WebKitBlob || z);
-			B=B.call ? B.bind(self) : Blob ;
-			var fn = strFileName || "download",
-			blob,
-			fr;
-
-
-		if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
-			x=[x, m];
-			m=x[0];
-			x=x[1];
-		}
-
-
-
-
-		//go ahead and download dataURLs right away
-		if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){
-			return navigator.msSaveBlob ?  // IE10 can't do a[download], only Blobs:
-				navigator.msSaveBlob(d2b(x), fn) :
-				saver(x) ; // everyone else can save dataURLs un-processed
-		}//end if dataURL passed?
-
-		blob = x instanceof B ?
-			x :
-			new B([x], {type: m}) ;
-
-
-		function d2b(u) {
-			var p= u.split(/[:;,]/),
-			t= p[1],
-			dec= p[2] == "base64" ? atob : decodeURIComponent,
-			bin= dec(p.pop()),
-			mx= bin.length,
-			i= 0,
-			uia= new Uint8Array(mx);
-
-			for(i;i<mx;++i) uia[i]= bin.charCodeAt(i);
-
-			return new B([uia], {type: t});
-		 }
-
-		function saver(url, winMode){
-
-			if ('download' in a) { //html5 A[download]
-				a.href = url;
-				a.setAttribute("download", fn);
-				a.innerHTML = "downloading...";
-				D.body.appendChild(a);
-				setTimeout(function() {
-					a.click();
-					D.body.removeChild(a);
-					if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(a.href);}, 250 );}
-				}, 66);
-				return true;
-			}
-
-			if(typeof safari !=="undefined" ){ // handle non-a[download] safari as best we can:
-				url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);
-				if(!window.open(url)){ // popup blocked, offer direct download:
-					if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
-				}
-				return true;
-			}
-
-			//do iframe dataURL download (old ch+FF):
-			var f = D.createElement("iframe");
-			D.body.appendChild(f);
-
-			if(!winMode){ // force a mime that will download:
-				url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);
-			}
-			f.src=url;
-			setTimeout(function(){ D.body.removeChild(f); }, 333);
-
-		}//end saver
-
-
-
-
-		if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
-			return navigator.msSaveBlob(blob, fn);
-		}
-
-		if(self.URL){ // simple fast and modern way using Blob and URL:
-			saver(self.URL.createObjectURL(blob), true);
-		}else{
-			// handle non-Blob()+non-URL browsers:
-			if(typeof blob === "string" || blob.constructor===z ){
-				try{
-					return saver( "data:" +  m   + ";base64,"  +  self.btoa(blob)  );
-				}catch(y){
-					return saver( "data:" +  m   + "," + encodeURIComponent(blob)  );
-				}
-			}
-
-			// Blob but not URL:
-			fr=new FileReader();
-			fr.onload=function(e){
-				saver(this.result);
-			};
-			fr.readAsDataURL(blob);
-		}
-		return true;
-	}; /* end download() */
-}));
\ No newline at end of file
+//download.js v4.0, by dandavis; 2008-2015. [CCBY2] see http://danml.com/download.html for tests/usage
+// v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
+// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
+// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
+// v4 adds AMD/UMD, commonJS, and plain browser support
+// https://github.com/rndme/download
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define([], factory);
+    } else if (typeof exports === 'object') {
+        // Node. Does not work with strict CommonJS, but
+        // only CommonJS-like environments that support module.exports,
+        // like Node.
+        module.exports = factory();
+    } else {
+        // Browser globals (root is window)
+        root.download = factory();
+  }
+}(this, function () {
+
+    return function download(data, strFileName, strMimeType) {
+
+        var self = window, // this script is only for browsers anyway...
+            u = "application/octet-stream", // this default mime also triggers iframe downloads
+            m = strMimeType || u,
+            x = data,
+            D = document,
+            a = D.createElement("a"),
+            z = function(a){return String(a);},
+            B = (self.Blob || self.MozBlob || self.WebKitBlob || z);
+            B=B.call ? B.bind(self) : Blob ;
+            var fn = strFileName || "download",
+            blob,
+            fr;
+
+
+        if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
+            x=[x, m];
+            m=x[0];
+            x=x[1];
+        }
+
+
+
+
+        //go ahead and download dataURLs right away
+        if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){
+            return navigator.msSaveBlob ?  // IE10 can't do a[download], only Blobs:
+                navigator.msSaveBlob(d2b(x), fn) :
+                saver(x) ; // everyone else can save dataURLs un-processed
+        }//end if dataURL passed?
+
+        blob = x instanceof B ?
+            x :
+            new B([x], {type: m}) ;
+
+
+        function d2b(u) {
+            var p= u.split(/[:;,]/),
+            t= p[1],
+            dec= p[2] == "base64" ? atob : decodeURIComponent,
+            bin= dec(p.pop()),
+            mx= bin.length,
+            i= 0,
+            uia= new Uint8Array(mx);
+
+            for(i;i<mx;++i) uia[i]= bin.charCodeAt(i);
+
+            return new B([uia], {type: t});
+         }
+
+        function saver(url, winMode){
+
+            if ('download' in a) { //html5 A[download]
+                a.href = url;
+                a.setAttribute("download", fn);
+                a.innerHTML = "downloading...";
+                D.body.appendChild(a);
+                setTimeout(function() {
+                    a.click();
+                    D.body.removeChild(a);
+                    if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(a.href);}, 250 );}
+                }, 66);
+                return true;
+            }
+
+            if(typeof safari !=="undefined" ){ // handle non-a[download] safari as best we can:
+                url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);
+                if(!window.open(url)){ // popup blocked, offer direct download:
+                    if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
+                }
+                return true;
+            }
+
+            //do iframe dataURL download (old ch+FF):
+            var f = D.createElement("iframe");
+            D.body.appendChild(f);
+
+            if(!winMode){ // force a mime that will download:
+                url="data:"+url.replace(/^data:([\w\/\-\+]+)/, u);
+            }
+            f.src=url;
+            setTimeout(function(){ D.body.removeChild(f); }, 333);
+
+        }//end saver
+
+
+
+
+        if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
+            return navigator.msSaveBlob(blob, fn);
+        }
+
+        if(self.URL){ // simple fast and modern way using Blob and URL:
+            saver(self.URL.createObjectURL(blob), true);
+        }else{
+            // handle non-Blob()+non-URL browsers:
+            if(typeof blob === "string" || blob.constructor===z ){
+                try{
+                    return saver( "data:" +  m   + ";base64,"  +  self.btoa(blob)  );
+                }catch(y){
+                    return saver( "data:" +  m   + "," + encodeURIComponent(blob)  );
+                }
+            }
+
+            // Blob but not URL:
+            fr=new FileReader();
+            fr.onload=function(e){
+                saver(this.result);
+            };
+            fr.readAsDataURL(blob);
+        }
+        return true;
+    }; /* end download() */
+}));
diff --git a/beat/web/reports/static/reports/js/new_report_dialog.js b/beat/web/reports/static/reports/js/new_report_dialog.js
index fc6f90cdb3188f388faaeecc6fe9e81cf5768e09..71843c41c36f2ed8bddf7d34f87b7571bff605e2 100644
--- a/beat/web/reports/static/reports/js/new_report_dialog.js
+++ b/beat/web/reports/static/reports/js/new_report_dialog.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/.
 */
diff --git a/beat/web/reports/static/reports/test/report-spec.js b/beat/web/reports/static/reports/test/report-spec.js
index aee6bb81907e3951d82b4df3246366f3b7dc2b5c..0667e35168afdaed257a1cefd22ee7dcf7707653 100644
--- a/beat/web/reports/static/reports/test/report-spec.js
+++ b/beat/web/reports/static/reports/test/report-spec.js
@@ -1,956 +1,956 @@
 // 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
-	afterEach(function() {
-		let util = require('util');
-		browser.manage().logs().get('browser').then(function(logs) {
-			// 'failed to load resource' is a familiar log error
-			// when running adblockers. Don't fail on those.
-			let failingLogs = logs
-			.filter(l => !(/net::ERR_FAILED/.test(l.message)));
-
-			expect(failingLogs.length).toEqual(0);
-
-			if (failingLogs.length > 0) {
-				console.log(`logs: ${util.inspect(failingLogs)}`);
-			}
-		});
-	});
-
-	// /reports
-	describe('home', function(){
-		beforeEach(function(){
-			browser.get('http://localhost:8000/reports/user');
-		});
-
-		it('should load', function(){
-			expect(browser.getTitle()).toEqual("BEAT - user's Reports");
-		});
-	});
-
-	// /reports/user
-	describe('home for the test user', function(){
-
-		// go to user's reports page before each test
-		beforeAll(function(){
-			browser.get('http://localhost:8000/reports/user/');
-		});
-
-		// before adding a report, there shouldn't be any
-		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
-		it('should create a new report called "test"', function(){
-			let newReportButton = browser.findElement(by.partialLinkText('New'));
-			newReportButton.click();
-
-			// wait for dialog box to pop up
-			browser.sleep(1000);
-
-			let reportNameInput = browser.findElement(by.css('.has-error input'));
-			reportNameInput.sendKeys('test');
-
-			let submitButton = element(by.buttonText('Create'));
-			expect(submitButton.isPresent()).toBeTruthy();
-			submitButton.click();
-
-			// wait for page to refresh
-			browser.sleep(1000);
-
-			let newReportLink = browser.findElement(by.linkText('user/test'));
-			newReportLink.click();
-			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]}`)
-				})
-				;
-			});
-		});
-	});
+    // 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
+    afterEach(function() {
+        let util = require('util');
+        browser.manage().logs().get('browser').then(function(logs) {
+            // 'failed to load resource' is a familiar log error
+            // when running adblockers. Don't fail on those.
+            let failingLogs = logs
+            .filter(l => !(/net::ERR_FAILED/.test(l.message)));
+
+            expect(failingLogs.length).toEqual(0);
+
+            if (failingLogs.length > 0) {
+                console.log(`logs: ${util.inspect(failingLogs)}`);
+            }
+        });
+    });
+
+    // /reports
+    describe('home', function(){
+        beforeEach(function(){
+            browser.get('http://localhost:8000/reports/user');
+        });
+
+        it('should load', function(){
+            expect(browser.getTitle()).toEqual("BEAT - user's Reports");
+        });
+    });
+
+    // /reports/user
+    describe('home for the test user', function(){
+
+        // go to user's reports page before each test
+        beforeAll(function(){
+            browser.get('http://localhost:8000/reports/user/');
+        });
+
+        // before adding a report, there shouldn't be any
+        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
+        it('should create a new report called "test"', function(){
+            let newReportButton = browser.findElement(by.partialLinkText('New'));
+            newReportButton.click();
+
+            // wait for dialog box to pop up
+            browser.sleep(1000);
+
+            let reportNameInput = browser.findElement(by.css('.has-error input'));
+            reportNameInput.sendKeys('test');
+
+            let submitButton = element(by.buttonText('Create'));
+            expect(submitButton.isPresent()).toBeTruthy();
+            submitButton.click();
+
+            // wait for page to refresh
+            browser.sleep(1000);
+
+            let newReportLink = browser.findElement(by.linkText('user/test'));
+            newReportLink.click();
+            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/static/reports/test/test-spec.js b/beat/web/reports/static/reports/test/test-spec.js
index b22411eb98ba6136b0d2bfca49c25800196e8858..6f1a392c1de26ad1a9c47ae64451c0b9c85720f1 100644
--- a/beat/web/reports/static/reports/test/test-spec.js
+++ b/beat/web/reports/static/reports/test/test-spec.js
@@ -1,18 +1,18 @@
 // 'describe' blocks can hold other 'describe' blocks and test blocks
 describe('BEAT platform', function() {
-	/*
-	 * the BEAT platform does not use Angular in a way that
-	 * Protractor can automatically reason about,
-	 * so disable special Angular features
-	 */
-	browser.ignoreSynchronization = true;
-	// 'it' blocks are individual tests
-	it('should have the page title of "BEAT"', function() {
-		// 'browser' is a global object representing the browser
-		// assumes the BEAT web server is running locally on port 8000
-		browser.get('http://localhost:8000');
+    /*
+     * the BEAT platform does not use Angular in a way that
+     * Protractor can automatically reason about,
+     * so disable special Angular features
+     */
+    browser.ignoreSynchronization = true;
+    // 'it' blocks are individual tests
+    it('should have the page title of "BEAT"', function() {
+        // 'browser' is a global object representing the browser
+        // assumes the BEAT web server is running locally on port 8000
+        browser.get('http://localhost:8000');
 
-		// simple test to sanity check Protractor
-		expect(browser.getTitle()).toEqual('BEAT');
-	});
+        // simple test to sanity check Protractor
+        expect(browser.getTitle()).toEqual('BEAT');
+    });
 });
diff --git a/beat/web/toolchains/static/toolchains/js/common.js b/beat/web/toolchains/static/toolchains/js/common.js
index 276503c470f169c852b15a25972695f2f8bd8727..15817d48e172b4c68506cbf952cf2c82dc5e0d72 100644
--- a/beat/web/toolchains/static/toolchains/js/common.js
+++ b/beat/web/toolchains/static/toolchains/js/common.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/.
 */
diff --git a/beat/web/toolchains/static/toolchains/js/dialogs.js b/beat/web/toolchains/static/toolchains/js/dialogs.js
index 32e5b61c6a0973e0e90935561b525d076d33869b..814aa761af7236d935a2ed95a133271500538984 100644
--- a/beat/web/toolchains/static/toolchains/js/dialogs.js
+++ b/beat/web/toolchains/static/toolchains/js/dialogs.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/.
 */
diff --git a/beat/web/toolchains/static/toolchains/js/editor.js b/beat/web/toolchains/static/toolchains/js/editor.js
index 2a90b5c3971129cc5ac19d10dbc1cb9ebe87aaab..88056781b4b5f86c0a8ba3be39e23a08fca8aeb6 100644
--- a/beat/web/toolchains/static/toolchains/js/editor.js
+++ b/beat/web/toolchains/static/toolchains/js/editor.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/.
 */
@@ -4306,7 +4306,7 @@ beat.toolchains.editor.ToolchainView.prototype._onMouseWheel = function(event)
     var previous_zoom = this.zoom;
     var must_redraw = false;
 
-    // With Ctrl key: control the zoom 
+    // With Ctrl key: control the zoom
     if (event.ctrlKey)
     {
         // Scroll up
@@ -4320,7 +4320,7 @@ beat.toolchains.editor.ToolchainView.prototype._onMouseWheel = function(event)
         must_redraw = (previous_zoom != this.zoom);
     }
 
-    // Without modifier key: translate the view 
+    // Without modifier key: translate the view
     else
     {
         var mouse_position = this._getMousePosition(event);
@@ -4378,7 +4378,7 @@ beat.toolchains.editor.ToolchainView.prototype._updateSelectionOrActionDataPosit
             }
         }
     }
-    
+
     // If we are currently importing a toolchain, correctly update the position
     // of all the blocks and connections
     else if (this.current_action == beat.toolchains.editor.ACTION_TOOLCHAIN_IMPORTATION)
diff --git a/beat/web/toolchains/static/toolchains/js/models.js b/beat/web/toolchains/static/toolchains/js/models.js
index ee21ca6902e2b819ef35f6bb808d970c4655c250..aa03273d1dab8164869b35d3af16709a92a9923f 100644
--- a/beat/web/toolchains/static/toolchains/js/models.js
+++ b/beat/web/toolchains/static/toolchains/js/models.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/.
 */
diff --git a/beat/web/toolchains/static/toolchains/js/utils.js b/beat/web/toolchains/static/toolchains/js/utils.js
index 654735cef8b92e760f15979b6a610eaabaa353ee..96fd6380c787f7621c57b4ed643e69f262eb7d4d 100644
--- a/beat/web/toolchains/static/toolchains/js/utils.js
+++ b/beat/web/toolchains/static/toolchains/js/utils.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/.
 */
diff --git a/beat/web/toolchains/static/toolchains/js/viewer.js b/beat/web/toolchains/static/toolchains/js/viewer.js
index f3736f024698647fd7a6353b5c7e16773995f6ae..ac886c0f97b9744d58e34be489ca5e1fab81bb82 100644
--- a/beat/web/toolchains/static/toolchains/js/viewer.js
+++ b/beat/web/toolchains/static/toolchains/js/viewer.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/.
 */
@@ -661,7 +661,7 @@ beat.toolchains.viewer.ToolchainViewer.prototype._onMouseWheel = function(event)
     var previous_zoom = this.zoom;
     var must_redraw = false;
 
-    // With Ctrl key: control the zoom 
+    // With Ctrl key: control the zoom
     if (event.ctrlKey)
     {
         // Scroll up
@@ -681,7 +681,7 @@ beat.toolchains.viewer.ToolchainViewer.prototype._onMouseWheel = function(event)
         must_redraw = (previous_zoom != this.zoom);
     }
 
-    // Without modifier key: translate the view 
+    // Without modifier key: translate the view
     else
     {
         var mouse_position = this._getMousePosition(event);
diff --git a/beat/web/ui/static/ui/js/history.js b/beat/web/ui/static/ui/js/history.js
index 028d94ca26b67c9558f2f3220191b64a12b226f8..c204a79c7f1842521625145422865cd0794e4177 100644
--- a/beat/web/ui/static/ui/js/history.js
+++ b/beat/web/ui/static/ui/js/history.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/.
 */
diff --git a/beat/web/ui/static/ui/js/listselector.js b/beat/web/ui/static/ui/js/listselector.js
index 83ca04104f290967fb5b111bb8d210a5d6ff428e..f77e644ba96e2b28aca2e12f11044cbe1551dc4b 100644
--- a/beat/web/ui/static/ui/js/listselector.js
+++ b/beat/web/ui/static/ui/js/listselector.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/.
 */
diff --git a/beat/web/ui/static/ui/js/multipleselector.js b/beat/web/ui/static/ui/js/multipleselector.js
index 4bc4fe54164fb60460062f1f67a5a2a374713a27..1911cf8300893907e776b24ae1b04451b98d204c 100644
--- a/beat/web/ui/static/ui/js/multipleselector.js
+++ b/beat/web/ui/static/ui/js/multipleselector.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/.
 */
diff --git a/beat/web/ui/static/ui/js/multipleselector_for_report.js b/beat/web/ui/static/ui/js/multipleselector_for_report.js
index 495bf2c86d3cd986ffbd3fab247e0c1a3dd9b253..564cc9626247487ff3ab9b9526c39de45ad44acd 100644
--- a/beat/web/ui/static/ui/js/multipleselector_for_report.js
+++ b/beat/web/ui/static/ui/js/multipleselector_for_report.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/.
 */
diff --git a/beat/web/ui/static/ui/js/plotterparameterdialog.js b/beat/web/ui/static/ui/js/plotterparameterdialog.js
index 1d9aba3c045dc0cfaa487acf2a228e17c6f906ac..f0a85c784873f5ef8e63bfcab85d6052833fc3d8 100644
--- a/beat/web/ui/static/ui/js/plotterparameterdialog.js
+++ b/beat/web/ui/static/ui/js/plotterparameterdialog.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/.
 */
diff --git a/beat/web/ui/static/ui/js/save_as_dialog.js b/beat/web/ui/static/ui/js/save_as_dialog.js
index fbb9f6c79e4c5613e291ce8e33de2a44fd86141c..d9b8c910cee6122e52cac8613c8f6e97c1a7c101 100644
--- a/beat/web/ui/static/ui/js/save_as_dialog.js
+++ b/beat/web/ui/static/ui/js/save_as_dialog.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/.
 */
diff --git a/beat/web/ui/static/ui/js/smartselector.js b/beat/web/ui/static/ui/js/smartselector.js
index bfadcb77eae29c71674ea5993aeb415569bea72c..f936011718100316d2430fa953537967e32206e5 100644
--- a/beat/web/ui/static/ui/js/smartselector.js
+++ b/beat/web/ui/static/ui/js/smartselector.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/.
 */
@@ -311,7 +311,7 @@ beat.ui.SmartSelector.prototype.display = function(entries, left, top)
         if (left + width >= window_width)
             left = window_width - width - 4;
     }
-        
+
     if (top === undefined)
     {
         top = (window_height - height) / 2;
diff --git a/buildout.cfg b/buildout.cfg
index d3794a0bc842a63735fb2768fc74c24b8444c80f..a186c864111e8d4abf302756625a22394a2d8bd2 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -144,7 +144,7 @@ packages = jquery#~1.11.3
            underscore#~1.8.3
            datatables#~1.10.10
            angular-ui-sortable#~0.14
-	   angular-ui-codemirror
+           angular-ui-codemirror
 executable = ${buildout:bin-directory}/bower
 base-directory = beat/web
 downloads = static