Merge from stable

This commit is contained in:
Clayton Turner
2014-02-10 16:49:43 -05:00
893 changed files with 45786 additions and 40419 deletions
+1
View File
@@ -60,6 +60,7 @@ shed_tool_data_table_conf.xml
job_conf.xml
data_manager_conf.xml
shed_data_manager_conf.xml
object_store_conf.xml
config/*
static/welcome.html.*
static/welcome.html
+2
View File
@@ -4,3 +4,5 @@ a4113cc1cb5eaa68091c9a73375f00555b66dd11 release_2013.01.13
2cc8d10988e03257dc7b97f8bb332c7df745d1dd security_2013.04.08
524f246ca85395082719ae7a6ff72260d7ad5612 release_2013.06.03
1ae95b3aa98d1ccf15b243ac3ce6a895eb7efc53 release_2013.08.12
26f58e05aa1068761660681583821e21e6cbf7ab release_2013.11.04
5e605ed6069fe4c5ca9875e95e91b2713499e8ca release_2014.02.10
-28
View File
@@ -4,28 +4,6 @@ cd `dirname $0`
: ${HOSTTYPE:=`uname -m`}
# link to HYPHY is arch-dependent
case "$OSTYPE" in
linux-gnu)
kernel=`uname -r | cut -f1,2 -d.`
HYPHY="/galaxy/software/linux$kernel-$HOSTTYPE/hyphy"
;;
darwin*)
this_minor=`uname -r | awk -F. '{print ($1-4)}'`
machine=`machine`
for minor in `jot - 3 $this_minor 1`; do
HYPHY="/galaxy/software/macosx10.$minor-$machine/hyphy"
[ -d "$HYPHY" ] && break
done
[ ! -d "$HYPHY" ] && unset HYPHY
;;
solaris2.10)
# For the psu-production builder which is Solaris, but jobs run on a
# Linux cluster
HYPHY="/galaxy/software/linux2.6-x86_64/hyphy"
;;
esac
LINKS="
/galaxy/data/location/add_scores.loc
/galaxy/data/location/all_fasta.loc
@@ -121,12 +99,6 @@ case $type in
ln -sf $link tool-data
done
if [ -d "$HYPHY" ]; then
echo "Linking $HYPHY"
rm -f tool-data/HYPHY
ln -sf $HYPHY tool-data/HYPHY
fi
if [ -d "$JARS" ]; then
echo "Linking $JARS"
rm -f tool-data/shared/jars
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- these relative paths can contain common templates importable by all visualization plugins -->
<paths>
<path>common/templates</path>
</paths>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="Charts">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">tabular.Tabular</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<template>charts.mako</template>
</visualization>
@@ -0,0 +1,9 @@
43 55 63 99 28 85 20 31 47 14
1 63 94 83 94 74 7 73 51 7
8 34 2 94 87 21 72 51 3 84
5 50 40 30 59 43 19 77 30 80
22 55 69 43 77 72 12 54 53 56
36 34 81 28 59 64 54 29 57 69
57 90 93 32 45 98 40 16 52 83
6 17 39 86 10 11 38 69 19 50
67 51 47 22 58 27 65 49 92 96
@@ -0,0 +1,92 @@
// dependencies
define(['library/portlet', 'library/ui', 'library/utils',
'views/charts', 'views/viewport', 'views/chart', 'views/group',
'models/config', 'models/datasets', 'models/chart', 'models/charts', 'models/group', 'models/types'],
function( Portlet, Ui, Utils,
ChartsView, ViewportView, ChartView, GroupView,
Config, Datasets, Chart, Charts, Group, Types
) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(options)
{
// link options
this.options = options;
// link galaxy
this.modal = parent.Galaxy.modal;
// create configuration model
this.config = new Config();
// create chart models
this.types = new Types();
this.chart = new Chart();
this.charts = new Charts();
this.group = new Group();
// create dataset handler
this.datasets = new Datasets(this);
// create views
this.charts_view = new ChartsView(this);
this.group_view = new GroupView(this);
this.chart_view = new ChartView(this);
this.viewport_view = new ViewportView(this);
// append view port to charts viewer
this.charts_view.append(this.viewport_view.$el);
// create portlet
if (!this.options.config.widget) {
this.portlet = new Portlet({icon : 'fa-bar-chart-o', label : 'Charts'});
} else {
this.portlet = $('<div></div>');
}
// append views
this.portlet.append(this.charts_view.$el);
this.portlet.append(this.group_view.$el);
this.portlet.append(this.chart_view.$el);
// set element
if (!this.options.config.widget) {
this.setElement(this.portlet.$el);
} else {
this.setElement(this.portlet);
}
// hide views
this.group_view.$el.hide();
this.charts_view.$el.hide();
// events
var self = this;
this.config.on('change:current_view', function() {
self._showCurrent();
});
},
// current view
_showCurrent: function() {
},
// execute command
execute: function(options) {
},
// unload
onunload: function() {
},
// log
log: function(location, message) {
console.log(location + ' ' + message);
}
});
});
@@ -0,0 +1,33 @@
// dependencies
define([], function() {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
},
// render
refresh : function(data)
{
// add graph to screen
var self = this;
nv.addGraph(function() {
self.d3_chart = nv.models.multiBarChart();
self.d3_chart.xAxis.tickFormat(d3.format('.2f'))
self.d3_chart.yAxis.tickFormat(d3.format('.1f'))
d3.select(self.options.svg_id)
.datum(data)
.call(self.d3_chart);
nv.utils.windowResize(self.d3_chart.update);
});
}
});
});
@@ -0,0 +1,33 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
this.chart = options.chart;
},
// render
refresh : function(data)
{
// add graph to screen
var self = this;
nv.addGraph(function() {
self.d3_chart = nv.models.multiBarHorizontalChart();
self.d3_chart.xAxis.tickFormat(function() { return ''; });
d3.select(self.options.svg_id)
.datum(data)
.call(self.d3_chart);
nv.utils.windowResize(self.d3_chart.update);
});
}
});
});
@@ -0,0 +1,39 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
this.chart = options.chart;
},
// render
refresh : function(data)
{
// add graph to screen
var self = this;
nv.addGraph(function() {
self.chart_3d = nv.models.lineChart();
self.chart_3d.xAxis
.tickFormat(d3.format(',f'));
self.chart_3d.yAxis
.tickFormat(d3.format(',.2f'));
d3.select(self.options.svg_id)
.datum(data)
.call(self.chart_3d);
nv.utils.windowResize(self.chart_3d.update);
return self.chart_3d;
});
}
});
});
@@ -0,0 +1,37 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
this.chart = options.chart;
},
// render
refresh : function(data)
{
// add graph to screen
var self = this;
nv.addGraph(function() {
self.chart_3d = nv.models.lineWithFocusChart();
self.chart_3d.xAxis
.tickFormat(d3.format(',f'));
self.chart_3d.yAxis
.tickFormat(d3.format(',.2f'));
d3.select(self.options.svg_id)
.datum(data)
.call(self.chart_3d);
nv.utils.windowResize(self.chart_3d.update);
});
}
});
});
@@ -0,0 +1,51 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
this.chart = options.chart;
},
// render
refresh : function(data)
{
// loop through data groups
for (var key in data) {
// get group
var group = data[key];
// format chart data
var pie_data = [];
for (var key in group.values) {
var value = group.values[key];
pie_data.push ({
key : value.x,
y : value.y
});
}
// add graph to screen
var self = this;
nv.addGraph(function() {
self.chart_3d = nv.models.pieChart()
.donut(true)
.showLegend(false);
d3.select(self.options.svg_id)
.datum(pie_data)
.call(self.chart_3d);
nv.utils.windowResize(self.chart_3d.update);
return self.chart_3d;
});
}
}
});
});
@@ -0,0 +1,37 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
this.chart = options.chart;
},
// render
refresh : function(data)
{
// add graph to screen
var self = this;
nv.addGraph(function() {
self.d3_chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range());
self.d3_chart.xAxis.tickFormat(d3.format('.02f'))
self.d3_chart.yAxis.tickFormat(d3.format('.02f'))
d3.select(self.options.svg_id)
.datum(data)
.call(self.d3_chart);
nv.utils.windowResize(self.d3_chart.update);
});
}
});
});
@@ -0,0 +1,59 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
this.app = app;
this.options = options;
this.chart = options.chart;
},
// render
refresh : function(data)
{
// add graph to screen
var self = this;
nv.addGraph(function() {
// check data
var valid = true;
var length = 0;
for (var key in data) {
// evalute length
if (length == 0) {
length = data[key].values.length;
} else {
if (length != data[key].values.length) {
valid = false;
break;
}
}
}
if (!valid) {
return;
}
// make plot
self.d3_chart = nv.models.stackedAreaChart()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
})
.clipEdge(true);
self.d3_chart.xAxis.tickFormat(function() { return ''; });
d3.select(self.options.svg_id)
.datum(data)
.call(self.d3_chart);
nv.utils.windowResize(self.d3_chart.update);
});
}
});
});
@@ -0,0 +1,171 @@
// dependencies
define(['library/utils'], function(Utils) {
// return
return Backbone.View.extend(
{
// visibility
visible: false,
// defaults options
optionsDefault: {
label : '',
icon : 'fa-tasks',
buttons : null,
body : null,
height : null,
operations : null,
placement : 'bottom',
overflow : 'auto'
},
// content
$content : null,
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(this.options));
// link content
this.$content = this.$el.find('#content');
// set content height
if (this.options.height) {
this.$el.find('#body').css('height', this.options.height);
this.$el.find('#content').css('overflow', this.options.overflow);
}
// append buttons
this.$buttons = $(this.el).find('#buttons');
if (this.options.buttons) {
// link functions
var self = this;
$.each(this.options.buttons, function(name, item) {
item.$el.prop('id', name);
self.$buttons.append(item.$el);
});
} else {
this.$buttons.remove();
}
// append operations
this.$operations = $(this.el).find('#operations');
if (this.options.operations) {
// link functions
var self = this;
$.each(this.options.operations, function(name, item) {
item.$el.prop('id', name);
self.$operations.append(item.$el);
});
}
// add body
if(this.options.body) {
this.append(this.options.body);
}
},
// append
append: function($el) {
this.$content.append(Utils.wrap($el));
},
// content
content: function() {
return this.$content;
},
// hide modal
show: function(){
// fade in
this.$el.fadeIn('fast');
// set flag
this.visible = true;
},
// hide modal
hide: function(){
// fade out
this.$el.fadeOut('fast');
// set flag
this.visible = false;
},
// enable buttons
enableButton: function(id) {
this.$buttons.find('#' + id).prop('disabled', false);
},
// disable buttons
disableButton: function(id) {
this.$buttons.find('#' + id).prop('disabled', true);
},
// enable operation
hideOperation: function(id) {
this.$operations.find('#' + id).hide();
},
// disable operation
showOperation: function(id) {
this.$operations.find('#' + id).show();
},
// set operation
setOperation: function(id, callback) {
var $el = this.$operations.find('#' + id);
$el.off('click');
$el.on('click', callback);
},
// label
label: function(new_label) {
var $el = this.$el.find('#label');
if (new_label) {
$el.html(new_label);
}
return $el.html();
},
// fill regular modal template
template: function(options) {
var tmpl = '<div class="toolForm">';
if (options.label || options.icon) {
tmpl += '<div id="title" class="toolFormTitle" style="overflow:hidden;">' +
'<div id="operations" style="float: right;"></div>' +
'<div style="overflow: hidden">';
if (options.icon)
tmpl += '<i style="padding-top: 3px; float: left; font-size: 1.2em" class="icon fa ' + options.icon + '">&nbsp;</i>';
tmpl += '<div id="label" style="padding-top: 2px; float: left;">' + options.label + '</div>';
tmpl += '</div>' +
'</div>';
}
tmpl += '<div id="body" class="toolFormBody">';
if (options.placement == 'top') {
tmpl += '<div id="buttons" class="buttons" style="height: 50px; padding: 10px;"></div>';
}
tmpl += '<div id="content" class="content" style="height: inherit; padding: 10px;"></div>';
if (options.placement == 'bottom') {
tmpl += '<div id="buttons" class="buttons" style="height: 50px; padding: 10px;"></div>';
}
tmpl += '</div>' +
'</div>';
return tmpl;
}
});
});
@@ -0,0 +1,188 @@
// dependencies
define(['library/utils'], function(Utils) {
// return
return Backbone.View.extend(
{
// current row
row: null,
// count rows
row_count: 0,
// defaults options
optionsDefault: {
content : 'No content available.',
onchange : null,
ondblclick : null,
onconfirm : null
},
// events
events : {
'click' : 'onclick',
'dblclick' : 'ondblclick'
},
// first
first: true,
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(options));
// initialize row
this.row = $('<tr></tr>');
},
// add header cell
addHeader: function($el) {
var wrapper = $('<th></th>');
wrapper.append($el);
this.row.append(wrapper);
},
// header
appendHeader: function() {
// append header row
$(this.el).find('thead').append(this.row);
// row
this.row = $('<tr></tr>');
},
// add row cell
add: function($el) {
var wrapper = $('<td></td>');
wrapper.append($el);
this.row.append(wrapper);
},
// append
append: function(id) {
this.commit(id);
},
// prepend
prepend: function(id) {
this.commit(id, true);
},
// commit
commit: function(id, prepend) {
// add
this.row.attr('id', id);
// add row
if (prepend) {
$(this.el).find('tbody').prepend(this.row);
} else {
$(this.el).find('tbody').append(this.row);
}
// row
this.row = $('<tr></tr>');
// row count
this.row_count++;
this.refresh();
},
// remove
remove: function(id) {
$(this.el).find('#' + id).remove();
this.row_count--;
this.refresh();
},
// remove
removeAll: function() {
$(this.el).find('tbody').html('');
this.row_count = 0;
this.refresh();
},
// value
value: function(new_value) {
// get current id/value
this.before = this.$el.find('.current').attr('id');
// check if new_value is defined
if (new_value !== undefined) {
this.$el.find('tr').removeClass('current');
if (new_value) {
this.$el.find('#' + new_value).addClass('current');
}
}
// get current id/value
var after = this.$el.find('.current').attr('id');
if(after === undefined) {
return null;
} else {
// fire onchange
if (after != this.before && this.options.onchange) {
this.options.onchange(new_value);
}
// return current value
return after;
}
},
// confirm new value
confirm: function(new_value) {
this.value(new_value);
},
// onclick
onclick: function(e) {
// get values
var old_value = this.value();
var new_value = $(e.target).closest('tr').attr('id');
// check equality
if (new_value && old_value != new_value) {
if (this.options.onconfirm) {
this.options.onconfirm(new_value);
} else {
this.confirm(new_value);
}
}
},
// onclick
ondblclick: function(e) {
var value = this.value();
if (value && this.options.ondblclick) {
this.options.ondblclick(value);
}
},
// refresh
refresh: function() {
if (this.row_count == 0) {
this.$el.find('tmessage').show();
} else {
this.$el.find('tmessage').hide();
}
},
// load html template
template: function(options)
{
return '<div>' +
'<table class="grid">' +
'<thead></thead>' +
'<tbody style="cursor: pointer;"></tbody>' +
'</table>' +
'<tmessage>' + options.content + '</tmessage>' +
'<div>';
}
});
});
@@ -0,0 +1,472 @@
// dependencies
define(['library/utils', 'library/ui.select'], function(Utils, Select) {
// plugin
var Label = Backbone.View.extend(
{
// options
optionsDefault: {
label : ''
},
// initialize
initialize : function(options) {
// get options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(this.options));
},
// template
template: function(options) {
return '<label><b>' + options.label + '</b></label>';
},
// value
value: function() {
return options.label;
}
});
// plugin
var Button = Backbone.View.extend(
{
// options
optionsDefault: {
id : null,
label : '',
float : 'right',
cls : 'btn-default',
type : 'submit',
icon : ''
},
// initialize
initialize : function(options) {
// get options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this._template(this.options));
// add event
$(this.el).on('click', options.onclick);
},
// element
_template: function(options) {
var str = '<button id="' + options.id + '" type="' + options.type + '" style="margin-right: 5px; float: ' + options.float + ';" type="button" class="btn ' + options.cls + '">';
if (options.icon) {
str += '<i class="icon fa ' + options.icon + '"></i>&nbsp;' ;
}
str += options.label +
'</button>';
return str;
}
});
// plugin
var Anchor = Backbone.View.extend(
{
// options
optionsDefault: {
label : ''
},
// initialize
initialize : function(options) {
// get options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this._template(this.options));
// add event
$(this.el).on('click', options.onclick);
},
// element
_template: function(options) {
return '<div><a href="javascript:void(0)">' + options.label + '</a></div>';
}
});
// plugin
var Message = Backbone.View.extend(
{
// options
optionsDefault: {
message : '',
status : 'info',
persistent : false
},
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement('<div></div>');
},
// update
update : function(options) {
// get options
this.options = Utils.merge(options, this.optionsDefault);
// show message
if (options.message != '') {
this.$el.html(this.template(this.options));
this.$el.fadeIn();
// check if message is persistent
if (!options.persistent) {
// set timer
var self = this;
window.setTimeout(function() {
if (self.$el.is(':visible')) {
self.$el.fadeOut();
} else {
self.$el.hide();
}
}, 3000);
}
} else {
this.$el.fadeOut();
}
},
// element
template: function(options) {
return '<div class="alert alert-' + options.status + '" style="padding: 2px 2px 2px 10px;">' + options.message + '</div>';
}
});
// plugin
var Searchbox = Backbone.View.extend(
{
// options
optionsDefault: {
onclick : null,
searchword : ''
},
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(this.options));
// add click event
var self = this;
if (this.options.onclick) {
this.$el.on('submit', function(e) {
var search_field = self.$el.find('#search');
self.options.onclick(search_field.val());
});
}
},
// element
template: function(options) {
return '<div class="search">' +
'<form onsubmit="return false;">' +
'<input id="search" class="form-control input-sm" type="text" name="search" placeholder="Search..." value="' + options.searchword + '">' +
'<button type="submit" class="btn search-btn">' +
'<i class="fa fa-search"></i>' +
'</button>' +
'</form>' +
'</div>';
}
});
// plugin
var Title = Backbone.View.extend(
{
// options
optionsDefault: {
label : 'Unlabeled',
body : null
},
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(this.options));
if (this.options.body) {
this.$el.find('.body').append(this.options.body);
}
},
// element
template: function(options) {
return '<div id="title" class="title">' +
options.label + ':' +
'</div>';
}
});
// tab
var ButtonMenu = Backbone.View.extend(
{
// main options
optionsDefault:
{
id : '',
title : '',
target : '',
href : '',
onunload : null,
onclick : null,
visible : true,
icon : null,
tag : ''
},
// optional sub menu
$menu: null,
// initialize
initialize: function (options)
{
// get options
this.options = Utils.merge(options, this.optionsDefault);
// add template for tab
this.setElement($(this._template(this.options)));
// find root
var $root = $(this.el).find('.root');
// link head
var self = this;
$root.on('click', function(e)
{
// prevent default
e.preventDefault();
// add click event
if(self.options.onclick) {
self.options.onclick();
}
});
// visiblity
if (!this.options.visible)
this.hide();
},
// show
show: function()
{
$(this.el).show();
},
// hide
hide: function()
{
$(this.el).hide();
},
// add menu item
addMenu: function (options)
{
// menu option defaults
var menuOptions = {
title : '',
target : '',
href : '',
onclick : null,
divider : false,
icon : null
}
// get options
menuOptions = Utils.merge(options, menuOptions);
// check if submenu element is available
if (!this.$menu)
{
// insert submenu element into root
$(this.el).append(this._templateMenu());
// update element link
this.$menu = $(this.el).find('.menu');
}
// create
var $item = $(this._templateMenuItem(menuOptions));
// add events
$item.on('click', function(e)
{
// prevent default
e.preventDefault();
// add click event
if(menuOptions.onclick) {
menuOptions.onclick();
}
});
// append menu
this.$menu.append($item);
// append divider
if (menuOptions.divider)
this.$menu.append($(this._templateDivider()));
},
// fill template header
_templateMenuItem: function (options)
{
var tmpl = '<li>' +
'<a href="' + options.href + '" target="' + options.target + '">';
if (options.icon)
tmpl += '<i class="fa ' + options.icon + '"></i>';
tmpl += ' ' + options.title +
'</a>' +
'</li>';
return tmpl;
},
// fill template header
_templateMenu: function ()
{
return '<ul class="menu dropdown-menu pull-right" role="menu"></ul>';
},
_templateDivider: function()
{
return '<li class="divider"></li>';
},
// fill template
_template: function (options)
{
// start template
var tmpl = '<div id="' + options.id + '" class="button-menu btn-group">' +
'<button type="button" class="root btn btn-default dropdown-toggle" data-toggle="dropdown">';
if (options.icon)
tmpl += '<i class="fa ' + options.icon + '"></i>';
'</button>' +
'</div>';
// return template
return tmpl;
}
});
// plugin
var ButtonIcon = Backbone.View.extend(
{
// options
optionsDefault: {
id : null,
label : '',
float : 'right',
cls : 'icon-btn',
type : 'submit',
icon : '',
tooltip : ''
},
// initialize
initialize : function(options) {
// get options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(this.options));
// add event
$(this.el).on('click', options.onclick);
// add tooltip
$(this.el).tooltip({title: options.tooltip, placement: 'bottom'});
},
// element
template: function(options) {
var str = '<a id="' + options.id + '" style="margin-right: 5px; float: ' + options.float + ';" class="' + options.cls + '">';
if (options.icon) {
str += '<i class="icon fa ' + options.icon + '"></i>';
}
str += options.label +
'</a>';
return str;
}
});
// plugin
var Input = Backbone.View.extend(
{
// options
optionsDefault: {
value : '',
type : 'text',
placeholder : '',
disabled : false,
visible : true
},
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this.template(this.options));
// disable input field
if (this.options.disabled) {
this.$el.prop('disabled', true);
}
// hide input field
if (!this.options.visible) {
this.$el.hide();
}
},
// value
value : function (new_val) {
if (new_val !== undefined) {
this.$el.val(new_val);
}
return this.$el.val();
},
// element
template: function(options) {
return '<input id="' + options.id + '" type="' + options.type + '" value="' + options.value + '" placeholder="' + options.placeholder + '" class="form-control">';
}
});
// return
return {
Label : Label,
Button : Button,
ButtonIcon : ButtonIcon,
Input : Input,
Anchor : Anchor,
Message : Message,
Searchbox : Searchbox,
Title : Title,
Select : Select,
ButtonMenu : ButtonMenu
}
});
@@ -0,0 +1,165 @@
// dependencies
define(['library/utils'], function(Utils) {
// plugin
return Backbone.View.extend(
{
// options
optionsDefault: {
id : '',
cls : '',
empty : 'No data available'
},
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement(this._template(this.options));
// add change event
var self = this;
if (this.options.onchange) {
this.$el.on('change', function() { self.options.onchange(self.value()); });
}
// refresh
this._refresh();
},
// value
value : function (new_value) {
// get current id/value
var before = this.$el.val();
// check if new_value is defined
if (new_value !== undefined) {
this.$el.val(new_value);
}
// get current id/value
var after = this.$el.val();
if(after === undefined) {
return null;
} else {
// fire onchange
if ((after != before && this.options.onchange) || this.$el.find('option').length == 1) {
this.options.onchange(after);
}
// return current value
return after;
}
},
// label
label : function () {
return this.$el.find('option:selected').text();
},
// disabled
disabled: function() {
return this.$el.is(':disabled');
},
// enable
enable: function() {
this.$el.prop('disabled', false);
},
// disable
disable: function() {
this.$el.prop('disabled', true);
},
// add
add: function(options) {
// add options
$(this.el).append(this._templateOption(options));
// refresh
this._refresh();
},
// remove
remove: function(value) {
// remove option
$(this.el).find('option[value=' + value + ']').remove();
$(this.el).trigger('change');
// refresh
this._refresh();
},
// render
update: function(options) {
// selected
var selected = this.$el.val();
// remove all options
$(this.el).find('option').remove();
// add new options
for (var key in options.data) {
$(this.el).append(this._templateOption(options.data[key]));
}
// add selected value
if (this._exists(selected))
$(this.el).val(selected);
// refresh
this._refresh();
},
// refresh
_refresh: function() {
// remove placeholder
$(this.el).find('option[value=null]').remove();
// count remaining entries
var remaining = $(this.el).find('option').length;
if (remaining == 0) {
// append placeholder
$(this.el).append(this._templateOption({value : 'null', label : this.options.empty}));
this.disable();
} else {
this.enable();
}
},
// exists
_exists: function(value) {
// check if selected value exists
return 0 != $(this.el).find('option[value=' + value + ']').length;
},
// option
_templateOption: function(options) {
return '<option value="' + options.value + '">' + options.label + '</option>';
},
// element
_template: function(options) {
var tmpl = '<select id="' + options.id + '" class="select ' + options.cls + ' ' + options.id + '">';
for (key in options.data) {
// options
var item = options.data[key];
// identify selected value
var tag = '';
if (item.value == options.selected || item.value == '') {
tag = 'selected';
}
// add template string
tmpl += '<option value="' + item.value + '" ' + tag + '>' + item.label + '</option>';
}
tmpl += '</select>';
return tmpl;
}
});
});
@@ -0,0 +1,130 @@
// dependencies
define([], function() {
// get
function get (url, success, error) {
request('GET', url, {}, success, error);
};
// generic function to send json to url
function request (method, url, data, success, error) {
// encode data into url
if (method == 'GET' || method == 'DELETE') {
url = url + "?" + $.param(data);
}
// prepare request
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('Cache-Control', 'no-cache');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onloadend = function() {
var status = xhr.status;
if (status == 200) {
try {
response = jQuery.parseJSON(xhr.responseText);
} catch (e) {
response = xhr.responseText;
}
success && success(response);
} else {
error && error(status);
}
};
// make request
if (method == 'GET' || method == 'DELETE') {
xhr.send();
} else {
xhr.send(JSON.stringify(data));
}
};
// merge
function merge (options, optionsDefault) {
if (options)
return _.defaults(options, optionsDefault);
else
return optionsDefault;
};
// get css value
function cssGetAttribute (classnames, name) {
if (classnames.length == 0) {
console.log('cssGetAttribute() : Requires a list of classnames');
}
// place dummy element
var el = $('<div class="' + classnames[0] + '"></div>');
var traverse_el = el;
for (var i in classnames) {
traverse_el.append('<div class="' + classnames[i] + '"></div>');
traverse_el = traverse_el.find('.' + classnames[i]);
}
// required append
el.appendTo(':eq(0)');
// get value
var value = el.css(name);
// remove element
el.remove();
// return css value
return value;
};
// load css
function cssLoadFile (url) {
// check if css is already available
if (!$('link[href^="' + url + '"]').length)
$('<link href="' + galaxy_config.root + url + '" rel="stylesheet">').appendTo('head');
};
// unique ide
function uuid(){
return (new Date().getTime()).toString(36);
};
// wrap
function wrap($el) {
var wrapper = $('<p></p>');
wrapper.append($el);
return wrapper;
};
// time
function time() {
// get date object
var d = new Date();
// format items
var hours = (d.getHours() < 10 ? "0" : "") + d.getHours();
var minutes = (d.getMinutes() < 10 ? "0" : "") + d.getMinutes()
// format final stamp
var datetime = d.getDate() + "/"
+ (d.getMonth() + 1) + "/"
+ d.getFullYear() + ", "
+ hours + ":"
+ minutes;
return datetime;
};
// return
return {
cssLoadFile : cssLoadFile,
cssGetAttribute : cssGetAttribute,
get : get,
merge : merge,
request: request,
uuid: uuid,
wrap : wrap,
time : time
};
});
@@ -0,0 +1,43 @@
// dependencies
define(['models/groups'], function(Groups) {
// model
return Backbone.Model.extend(
{
// defaults
defaults : {
id : null,
title : '',
type : '',
date : null
},
// initialize
initialize: function(options)
{
this.groups = new Groups();
},
// reset
reset: function()
{
this.clear({silent: true}).set(this.defaults);
this.groups.reset();
this.trigger('reset', this);
},
// copy
copy: function(new_chart) {
// copy chart/groups
var current = this;
current.set(new_chart.attributes);
current.groups.reset();
new_chart.groups.each(function(group) {
current.groups.add(group.clone());
});
current.trigger('change', current);
}
});
});
@@ -0,0 +1,10 @@
// dependencies
define(['models/chart'], function(Chart) {
// collection
return Backbone.Collection.extend(
{
model: Chart
});
});
@@ -0,0 +1,16 @@
// dependencies
define([], function() {
// model
return Backbone.Model.extend(
{
// options
defaults : {
query_limit : 20,
query_pace : 1000,
query_max : 5
}
});
});
@@ -0,0 +1,125 @@
// dependencies
define(['library/utils'], function(Utils) {
// widget
return Backbone.Model.extend(
{
// options
optionsDefault : {
limit : 500,
pace : 1000,
max : 2
},
// list
list: {},
// initialize
initialize: function(app, options)
{
// link app
this.app = app;
// configure options
this.options = Utils.merge(options, this.optionsDefault);
},
// get
get: function(options, callback) {
// check if dataset already exists
var dataset = this.list[options.id];
if (dataset) {
if (dataset.values) {
if (dataset.values[options.column]) {
if (callback) {
callback(dataset);
}
return dataset;
}
}
} else {
this.list[options.id] = dataset = this.app.options.dataset;
}
// initiate fetch cycle
if (options.column !== undefined) {
this._fetch(options, callback);
} else {
if (callback) {
callback(dataset);
}
return dataset;
}
// return
return dataset;
},
// size
length: function() {
return this.list.length;
},
// request data
_fetch : function(options, callback) {
// get dataset
var dataset = this.list[options.id];
// initialize values for datasets
if (!dataset.values) {
dataset.values = {};
}
if (!dataset.values[options.column]) {
dataset.values[options.column] = [];
}
// initialize page number
if (options.page === undefined) {
options.page = 0;
} else {
options.page++;
}
// check max page
if (options.page > this.options.max) {
return;
}
// config limits
var limit = this.options.limit;
var offset = this.options.limit * options.page;
// make request
var self = this;
Utils.request('GET', config.root + 'api/datasets/' + options.id, {
data_type : 'raw_data',
provider : 'dataset-column',
limit : limit,
offset : offset,
indeces : options.column
}, function(response) {
// verify if all data has been fetched
if (response.data.length > 0) {
// add values to dataset
for (var key in response.data){
var value = response.data[key][0];
if (value !== undefined) {
dataset.values[options.column].push(value);
} else {
return dataset;
}
}
// update charts
if (callback) {
callback(dataset);
}
// next call
setTimeout(function() { self._fetch(options, callback); }, self.options.pace);
}
});
}
});
});
@@ -0,0 +1,15 @@
// dependencies
define([], function() {
// model
return Backbone.Model.extend(
{
// reset
reset: function()
{
this.clear({silent: true}).set(this.defaults);
this.trigger('reset', this);
}
});
});
@@ -0,0 +1,10 @@
// dependencies
define(['models/group'], function(Group) {
// collection
return Backbone.Collection.extend(
{
model: Group
});
});
@@ -0,0 +1,71 @@
// dependencies
define([], function() {
// widget
return Backbone.Model.extend(
{
// types
defaults: {
'bardiagram' : {
title : 'Bar diagram',
data : {
y : {
title : 'Values for y-axis'
}
}
},
'horizontal' : {
title : 'Bar diagram (horizontal)',
data : {
y : {
title : 'Values for y-axis'
}
}
},
'line' : {
title : 'Line chart',
data : {
y : {
title : 'Values for y-axis'
}
}
},
'linewithfocus' : {
title : 'Line chart (with focus)',
data : {
y : {
title : 'Values for y-axis'
}
}
},
'piechart' : {
title : 'Pie chart',
data : {
y : {
title : 'Values for y-axis'
}
}
},
'scatterplot' : {
title : 'Scatter plot',
data : {
x : {
title : 'Values for x-axis'
},
y : {
title : 'Values for y-axis'
}
}
},
'stackedarea' : {
title : 'Stacked area',
data : {
y : {
title : 'Values for y-axis'
}
}
}
}
});
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,201 @@
// dependencies
define(['library/portlet', 'library/table', 'library/ui', 'library/utils', 'models/chart', 'views/groups'],
function(Portlet, Table, Ui, Utils, Chart, GroupsView) {
// widget
return Backbone.View.extend(
{
// defaults options
optionsDefault: {
header : true,
content : 'No content available.'
},
// initialize
initialize: function(app, options)
{
// link application
this.app = app;
// get current chart object
this.chart = this.app.chart;
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// main elements
this.message = new Ui.Message();
this.title = new Ui.Input({placeholder: 'Chart title'});
this.dataset = new Ui.Input({value : app.options.dataset.id, disabled: true, visible: false});
// configure dataset
this.groups_view = new GroupsView(this.app);
// table
var self = this;
this.table = new Table({
header : false,
onconfirm : function(type) {
if (self.chart.groups.length > 0) {
// show modal
self.app.modal.show({
title : 'Switching the chart type?',
body : 'You configured data sources. If you switch chart types your configurations will be removed.',
buttons : {
'Cancel' : function() {
// hide modal
self.app.modal.hide();
},
'Continue' : function() {
// hide modal
self.app.modal.hide();
// confirm
self.table.confirm(type);
}
}
});
} else {
// confirm
self.table.confirm(type);
}
},
onchange : function(type) {
// update chart type
self.chart.set({type: type});
// reset groups
self.chart.groups.reset();
},
content: 'No chart types available'
});
// add types
var types_n = 0;
var types = app.types.attributes;
for (var id in types){
var chart_type = types[id];
this.table.add (++types_n + '.');
this.table.add (chart_type.title);
this.table.append(id);
}
// add table to portlet
var self = this;
this.portlet = new Portlet({
icon : 'fa-edit',
label : 'Create a new chart:',
operations : {
'save' : new Ui.ButtonIcon({
icon : 'fa-save',
tooltip : 'Save Chart',
onclick : function() {
self._saveChart();
}
}),
'back' : new Ui.ButtonIcon({
icon : 'fa-caret-left',
tooltip : 'Return',
onclick : function() {
self.$el.hide();
self.app.charts_view.$el.show();
}
})
}
});
this.portlet.append(this.message.$el);
this.portlet.append(this.dataset.$el);
this.portlet.append((new Ui.Label({ label : 'Provide a chart title:'})).$el);
this.portlet.append(this.title.$el);
this.portlet.append((new Ui.Label({ label : 'Select a chart type:'})).$el);
this.portlet.append(this.table.$el);
this.portlet.append(this.groups_view.$el);
// elements
this.setElement(this.portlet.$el);
// hide back button on startup
this.portlet.hideOperation('back');
// model events
var self = this;
this.chart.on('change:title', function(chart) {
self.title.value(chart.get('title'));
});
this.chart.on('change:type', function(chart) {
self.table.value(chart.get('type'));
});
this.chart.on('reset', function(chart) {
self._resetChart();
});
// collection events
this.app.charts.on('add', function(chart) {
self.portlet.showOperation('back');
});
this.app.charts.on('remove', function(chart) {
if (self.app.charts.length == 1) {
self.portlet.hideOperation('back');
}
});
this.app.charts.on('reset', function(chart) {
self.portlet.hideOperation('back');
});
// reset
this._resetChart();
},
// reset
_resetChart: function() {
this.chart.set('id', Utils.uuid());
this.chart.set('dataset_id', this.app.options.dataset.id);
this.chart.set('type', 'bardiagram');
this.chart.set('title', 'Chart title');
},
// create chart
_saveChart: function() {
// update chart data
this.chart.set({
type : this.table.value(),
title : this.title.value(),
dataset_id : this.dataset.value(),
date : Utils.time()
});
// validate
if (!this.chart.get('title')) {
this.message.update({message : 'Please enter a title for your chart.'});
return;
}
if (!this.chart.get('type')) {
this.message.update({message : 'Please select a chart type.'});
return;
}
if (this.chart.groups.length == 0) {
this.message.update({message : 'Please configure at least one data source.'});
return;
}
// create/get chart
var current = this.app.charts.get(this.chart.id);
if (!current) {
current = this.chart.clone();
this.app.charts.add(current);
}
// update chart model
current.copy(this.chart);
// hide
this.$el.hide();
// update main
this.app.charts_view.$el.show();
}
});
});
@@ -0,0 +1,133 @@
// dependencies
define(['library/portlet', 'library/table', 'library/ui', 'library/utils'], function(Portlet, Table, Ui, Utils) {
// widget
return Backbone.View.extend(
{
// initialize
initialize: function(app, options)
{
// link app
this.app = app;
// table
this.table = new Table({
content : 'Add charts to this table.',
ondblclick : function(chart_id) {
// get chart
var chart = self.app.charts.get(chart_id);
self.app.chart.copy(chart);
// show edit
self.$el.hide();
// update model and show create
self.app.chart_view.$el.show();
},
onchange : function(chart_id) {
self.app.viewport_view.show(chart_id);
}
});
// add table to portlet
var self = this;
this.portlet = new Portlet({
icon : 'fa-list',
label : 'List of created charts:',
height : 100,
operations : {
'new' : new Ui.ButtonIcon({
icon : 'fa-plus',
tooltip: 'Create',
onclick: function() {
self.$el.hide();
self.app.chart.reset();
self.app.chart_view.$el.show();
}
}),
'delete' : new Ui.ButtonIcon({
icon : 'fa-minus',
tooltip: 'Delete',
onclick: function() {
// check if element has been selected
var chart_id = self.table.value();
if (!chart_id) {
return;
}
// title
var chart = self.app.charts.get(chart_id);
// show modal
self.app.modal.show({
title : 'Are you sure?',
body : 'The selected chart "' + chart.get('title') + '" will be irreversibly deleted.',
buttons : {
'Cancel' : function() {self.app.modal.hide();},
'Delete' : function() {
// hide modal
self.app.modal.hide();
// remove chart
self.app.charts.remove(chart_id);
}
}
});
}
}),
}
});
this.portlet.append(this.table.$el);
// append to main
if (!this.app.options.config.widget) {
this.$el.append(this.portlet.$el);
}
// events
var self = this;
this.app.charts.on('add', function(chart) {
// add
self._addChart(chart);
});
this.app.charts.on('remove', function(chart) {
// remove
self._removeChart(chart);
});
this.app.charts.on('change', function(chart) {
// replace
self._removeChart(chart);
self._addChart(chart);
});
},
// append
append : function($el) {
this.$el.append(Utils.wrap(''));
this.$el.append($el);
},
// add
_addChart: function(chart) {
// add title to table
this.table.add(chart.get('title'));
// get chart type
var type = this.app.types.get(chart.get('type'));
this.table.add(type.title);
// add additional columns
this.table.add('Last change: ' + chart.get('date'));
this.table.prepend(chart.get('id'));
this.table.value(chart.get('id'));
},
// remove
_removeChart: function(chart) {
// remove from to table
this.table.remove(chart.id);
}
});
});
@@ -0,0 +1,219 @@
// dependencies
define(['library/portlet', 'library/table', 'library/ui', 'library/utils'],
function(Portlet, Table, Ui, Utils) {
// chart config
return Backbone.View.extend(
{
// columns
columns: [],
// initialize
initialize: function(app, options) {
// link app
this.app = app;
// get current chart object
this.chart = this.app.chart;
this.group = this.app.group;
// ui elements
this.message = new Ui.Message();
this.label = new Ui.Input({placeholder: 'Group label'});
this.table = new Table({content: 'No data column.'});
// add table to portlet
var self = this;
this.portlet = new Portlet({
icon : 'fa-edit',
label : 'Define group properties:',
operations : {
'save' : new Ui.ButtonIcon({
icon : 'fa-save',
tooltip : 'Save',
onclick : function() {
// save/add group
self._saveGroup();
}
}),
'back' : new Ui.ButtonIcon({
icon : 'fa-caret-left',
tooltip : 'Return',
onclick : function() {
self.$el.hide();
self.app.chart_view.$el.show();
}
})
}
});
this.portlet.append(this.message.$el);
this.portlet.append(this.label.$el);
this.portlet.append(this.table.$el);
// add element
this.setElement(this.portlet.$el);
// change
var self = this;
this.chart.on('change:dataset_id', function() {
self._refreshDataset();
});
this.chart.on('change:type', function() {
self._refreshType();
});
this.group.on('change:label', function() {
self._refreshLabel();
});
this.group.on('change', function() {
self._refreshGroup();
});
this.group.on('reset', function() {
self._resetGroup();
});
},
// show
show: function() {
this.$el.show();
},
// reset
_resetGroup: function() {
this.group.set('id', Utils.uuid());
this.group.set('label', 'Group label');
},
// update dataset
_refreshDataset: function() {
// identify datasets
var dataset_id = this.chart.get('dataset_id');
// check if dataset is available
if (!dataset_id) {
return;
}
// get dataset
var dataset = this.app.datasets.get({id : dataset_id});
// check
if (!dataset) {
this.app.log('Config::render()', 'Failed to retrieve dataset.');
return;
}
// configure columns
this.columns = [];
var meta = dataset.metadata_column_types;
for (var key in meta){
this.columns.push({
'label' : key + ' [' + meta[key] + ']',
'value' : key
});
}
// update select fields
for (var key in this.list) {
this.list[key].update(this.columns);
}
},
// update
_refreshType: function() {
// configure chart type
var self = this;
var chart_type = this.chart.get('type');
if (chart_type) {
var chart_settings = this.app.types.get(chart_type);
// table
this.table.removeAll();
this.list = {};
for (var id in chart_settings.data) {
// create select field
var data_def = chart_settings.data[id];
var select = new Ui.Select({
id : 'select_' + id,
gid : id,
data : this.columns,
onchange : function(value) {
self.group.set(this.gid, value);
}
});
// add row to table
this.table.add(data_def.title);
this.table.add(select.$el);
this.table.append(id);
// add select field to list
this.list[id] = select;
}
}
},
// update
_refreshGroup: function() {
// update select fields
for (var id in this.list) {
var col = this.group.get(id);
if (col === undefined) {
col = 0;
}
this.list[id].value(col);
}
},
// update label
_refreshLabel: function() {
var label_text = this.group.get('label');
if (label_text === undefined) {
label_text = '';
}
this.label.value(label_text);
},
// create group
_saveGroup: function() {
// get current chart
var chart = this.chart;
// update group object
var group = this.group;
for (var key in this.list) {
group.set(key, this.list[key].value());
}
// add label
group.set({
dataset_id : this.chart.get('dataset_id'),
label : this.label.value(),
date : Utils.time()
});
// validate
if (!group.get('label')) {
this.message.update({message : 'Please enter a label for your group.'});
return;
}
// get groups of current chart
var groups = this.chart.groups;
// create/update group
var group_update = groups.get(group.id);
if (group_update) {
group_update.set(group.attributes);
} else {
groups.add(group.clone());
}
// hide
this.$el.hide();
// update main
this.app.chart_view.$el.show();
}
});
});
@@ -0,0 +1,154 @@
// dependencies
define(['library/portlet', 'library/table', 'library/ui', 'library/utils'], function(Portlet, Table, Ui, Utils) {
// chart config
return Backbone.View.extend(
{
// initialize
initialize: function(app, options) {
// link app
this.app = app;
// get current chart object
this.chart = this.app.chart;
// table
this.table = new Table({
content : 'Add data columns to this table.',
ondblclick : function(group_id) {
// get group
var group = self.app.chart.groups.get(group_id);
// show edit
self.app.chart_view.$el.hide();
self.app.group_view.show();
self.app.group_view.group.set(group.attributes);
}
});
// add table to portlet
var self = this;
this.portlet = new Portlet({
icon : '',
label : 'Select data columns:',
height : 100,
operations : {
'new' : new Ui.ButtonIcon({
icon : 'fa-plus',
tooltip: 'Create',
onclick: function() {
self.app.group.reset();
self.app.chart_view.$el.hide();
self.app.group_view.show();
}
}),
'edit' : new Ui.ButtonIcon({
icon : 'fa-pencil',
tooltip: 'Edit',
onclick: function() {
// check if element has been selected
var group_id = self.table.value();
if (!group_id) {
return;
}
// get group
var group = self.app.chart.groups.get(group_id);
// show edit
self.app.chart_view.$el.hide();
self.app.group_view.show();
self.app.group_view.group.set(group.attributes);
}
}),
'delete' : new Ui.ButtonIcon({
icon : 'fa-minus',
tooltip: 'Delete',
onclick: function() {
// check if element has been selected
var id = self.table.value();
if (!id) {
return;
}
// remove group from chart
self.chart.groups.remove(id);
}
})
}
});
this.portlet.append(this.table.$el);
// add element
this.setElement(this.portlet.$el);
// change
var self = this;
this.chart.on('change', function() {
self._refresh();
});
this.chart.on('reset', function() {
self._removeGroupAll();
});
this.chart.on('change:type', function() {
self.app.group_view.trigger('change');
});
this.chart.groups.on('add', function(group) {
self._addGroup(group);
});
this.chart.groups.on('remove', function(group) {
self._removeGroup(group);
});
this.chart.groups.on('reset', function(group) {
self._removeGroupAll();
});
this.chart.groups.on('change', function(group) {
self._removeGroup(group);
self._addGroup(group);
});
},
// refresh
_refresh: function() {
this._removeGroupAll();
var self = this;
var groups = this.chart.groups;
if (groups) {
groups.each(function(group) { self._addGroup(group); });
}
},
// add
_addGroup: function(group) {
// make custom info string
var info = '[';
var chart_type = this.chart.get('type');
if (chart_type) {
var chart_settings = this.app.types.get(chart_type);
for (var key in chart_settings.data) {
info += key + '=' + group.get(key) + ', '
}
}
info = info.substring(0, info.length - 2) + ']';
// add to table
this.table.add(group.get('label'));
this.table.add(info);
this.table.add('Last changed: ' + group.get('date'));
this.table.prepend(group.id);
this.table.value(group.id);
},
// remove
_removeGroup: function(group) {
// remove from to table
this.table.remove(group.id);
},
// data config reset
_removeGroupAll: function() {
// reset options table
this.table.removeAll();
}
});
});
@@ -0,0 +1,231 @@
// dependencies
define(['library/portlet', 'library/ui', 'library/utils'],
function(Portlet, Ui, Utils) {
// widget
return Backbone.View.extend(
{
// list
list: {},
// options
optionsDefault :
{
height : 300
},
// initialize
initialize: function(app, options)
{
// link app
this.app = app;
// link options
this.options = Utils.merge(options, this.optionsDefault);
// add table to portlet
this.portlet = new Portlet({
label : '',
icon : 'fa-signal',
height : this.options.height,
overflow : 'hidden',
operations : {
'edit' : new Ui.ButtonIcon({
icon : 'fa-gear',
tooltip : 'Configure'
})
}
});
// set this element
this.setElement(this.portlet.$el);
// events
var self = this;
// add
this.app.charts.on('add', function(chart) {
self._addChart(chart);
});
// remove
this.app.charts.on('remove', function(chart) {
self._removeChart(chart.id);
});
// replace
this.app.charts.on('change', function(chart) {
self._removeChart(chart.id);
self._addChart(chart);
});
},
// show
show: function(chart_id) {
// hide all
this.$el.find('svg').hide();
// identify selected item from list
var item = this.list[chart_id];
if (item) {
// show selected chart
this.$el.find(item.svg_id).show();
// get chart
var chart = self.app.charts.get(chart_id);
// update portlet
this.portlet.label(chart.get('title'));
this.portlet.setOperation('edit', function() {
// get chart
self.app.chart.copy(chart);
// show edit
self.app.charts_view.$el.hide();
self.app.chart_view.$el.show();
});
// this trigger d3 update events
$(window).trigger('resize');
}
},
// add
_addChart: function(chart) {
// link this
var self = this;
// backup chart details
var chart_id = chart.id;
// make sure that svg does not exist already
this._removeChart(chart_id);
// create id
var svg_id = '#svg_' + chart_id;
// create element
var chart_el = $(this._template({id: svg_id, height : this.options.height}));
// add to portlet
this.portlet.append(chart_el);
// backup id
this.list[chart_id] = {
svg_id : svg_id
}
// identify chart type
var chart_type = chart.get('type');
var chart_settings = this.app.types.get(chart_type);
// create chart view
var self = this;
require(['charts/' + chart_type], function(ChartView) {
// create chart
var view = new ChartView(self.app, {svg_id : svg_id, chart : chart});
// reset chart data
var chart_data = [];
// request data
var chart_index = 0;
chart.groups.each(function(group) {
// add group data
chart_data.push({
key : (chart_index + 1) + ':' + group.get('label'),
values : []
});
// get data
for (var key in chart_settings.data) {
// configure request
var data_options = {
view : view,
data : chart_data,
index : chart_index,
dataset_id : group.get('dataset_id'),
column : group.get(key),
column_key : key
}
// make request
self._data(data_options);
}
// count
chart_index++;
});
// show
self.show(chart_id);
});
},
// data
_data: function(options) {
// gather objects
var view = options.view;
var data = options.data;
// gather parameters
var index = options.index;
var column = options.column;
var dataset_id = options.dataset_id;
var column_key = options.column_key;
var group_data = data[options.index];
// send request
self.app.datasets.get({id : dataset_id, column: column}, function(dataset) {
// select column
var values = dataset.values[column];
var n_values = values.length;
// read column values
var column_values = [];
for (var i = 0; i < n_values; i++) {
var value = values[i];
if(!isNaN(value)) {
column_values.push(value);
} else {
column_values.push(0)
}
}
// write column values
for (var i = 0; i < n_values; i++) {
// make sure dictionary exists
if (group_data.values[i] === undefined) {
group_data.values[i] = {
x : i,
y : 0
}
}
// write data
group_data.values[i][column_key] = column_values[i];
}
// refresh view
view.refresh(data);
});
},
// remove
_removeChart: function(id) {
var item = this.list[id];
if (item) {
d3.select(item.svg_id).remove();
$(item.svg_id).remove();
}
},
// template
_template: function(options) {
return '<svg id="' + options.id.substr(1) + '"style="height: ' + options.height + 'px;"></svg>';
}
});
});
@@ -0,0 +1,78 @@
<%
scripts = [ 'jquery/jquery.js',
'bootstrap.js',
'require.js',
'underscore.js',
'backbone/backbone.js',
'd3.js']
%>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>${hda.name} | ${visualization_name}</title>
%for v in scripts:
<script type="text/javascript" charset="utf-8" src="/static/scripts/libs/${v}" ></script>
%endfor
## css
<link type="text/css" rel="Stylesheet" media="screen" href="/static/style/base.css">
## install nv.d3 module
<script type="text/javascript" charset="utf-8" src="/plugins/visualizations/charts/static/plugins/nv.d3.js" ></script>
<link type="text/css" rel="Stylesheet" media="screen" href="/plugins/visualizations/charts/static/plugins/nv.d3.css">
</head>
<body>
<script type="text/javascript">
// get configuration
var config = {
root : '/'
};
// console protection
window.console = window.console || {
log : function(){},
debug : function(){},
info : function(){},
warn : function(){},
error : function(){},
assert : function(){}
};
// configure require
require.config({
baseUrl: config.root + "plugins/visualizations/charts/static/",
shim: {
"libs/underscore": { exports: "_" },
"libs/backbone/backbone": { exports: "Backbone" }
}
});
// application
var app = null;
$(function() {
// request application script
require(['app'], function(App) {
// load options
var options = {
config : ${h.to_json_string( config )},
dataset : ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )}
}
// create application
app = new App(options);
// add to body
$('body').append(app.$el);
});
});
</script>
</body>
</html>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="circster">
<visualization name="Circster" disabled="true">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
@@ -24,5 +24,5 @@
<!-- template_root and template are currently ignored for the 'built-in' visualizations -->
<template_root>webapps/galaxy/visualization</template_root>
<template>circster.mako</template>
<render_location>_top</render_location>
<render_target>_top</render_target>
</visualization>
@@ -0,0 +1,8 @@
Common templates for visualization plugins
==========================================
Placing Mako templates in this directory will allow them to be properly
inherited and imported in plugin directories. E.g. if you have a template file
in this directory named 'config_utils.mako', you can import it in your plugin
templates using:
<%namespace name="config_utils" file="config_utils.mako" />
@@ -0,0 +1,46 @@
<%def name="add_config_defaults( defaults )">
## overwrite default_config_dict with config (if any) then assign to config
<%
for key, default in defaults.items():
if key not in config or config[ key ] is None:
config[ key ] = default
%>
</%def>
<%def name="config_form( config_dict )">
## render form for everything in possible config
</%def>
<%def name="link_to_change_config( link_contents, new_settings, target='' )">
<%
# assumes there's a config var
url_for_args = {
'controller' : 'visualization',
'action' : 'render',
'visualization_name' : visualization_name,
'title' : title
}
url_for_args.update( config )
url_for_args.update( new_settings )
if visualization_id:
url_for_args[ 'id' ] = visualization_id
%>
<a href="${h.url_for( **url_for_args )}" target="${target}">${link_contents}</a>
</%def>
<%def name="save_button( text='Save' )">
<%
# still a GET
url_for_args = {
'controller' : 'visualization',
'action' : 'saved',
'type' : visualization_name,
'title' : title,
'config' : h.to_json_string( config )
}
# save to existing visualization
if visualization_id:
url_for_args[ 'id' ] = visualization_id
%>
<form action="${h.url_for( **url_for_args )}" method="post"><input type="submit" value="${text}" /></form>
</%def>
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="phyloviz">
<visualization name="Phyloviz">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Newick</test>
<test type="isinstance" test_attr="datatype" result_type="datatype">data.Nexus</test>
<test type="isinstance" test_attr="datatype" result_type="datatype">xml.Phyloxml</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
@@ -16,5 +17,5 @@
<!-- template_root and template are currently ignored for the 'built-in' visualizations -->
<template_root>webapps/galaxy/visualization</template_root>
<template>phyloviz.mako</template>
<render_location>_top</render_location>
<render_target>_top</render_target>
</visualization>
@@ -6,9 +6,10 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON( 'package.json' ),
handlebars: {
// compile all hb templates into a single file in the build dir
compile: {
options: {
namespace: 'Templates',
namespace: 'scatterplot',
processName : function( filepath ){
return filepath.match( /\w*\.handlebars/ )[0].replace( '.handlebars', '' );
}
@@ -20,6 +21,7 @@ module.exports = function(grunt) {
},
concat: {
// concat the template file and any js files in the src dir into a single file in the build dir
options: {
separator: ';\n'
},
@@ -31,16 +33,19 @@ module.exports = function(grunt) {
},
uglify: {
// uglify the concat single file directly into the static dir
options: {
//mangle : false,
//beautify : true
},
dist: {
src : 'build/scatterplot-concat.js',
// uglify directly into static dir
dest: 'static/scatterplot.js'
dest: 'static/scatterplot-edit.js'
}
},
watch: {
// watch for changes in the src dir
files: [ 'src/**.js', 'src/handlebars/*.handlebars' ],
tasks: [ 'default' ]
}
@@ -52,5 +57,6 @@ module.exports = function(grunt) {
grunt.loadNpmTasks( 'grunt-contrib-watch' );
grunt.registerTask( 'default', [ 'handlebars', 'concat', 'uglify' ]);
grunt.registerTask( 'watch', [ 'handlebars', 'concat', 'uglify', 'watch' ]);
// you can run grunt watch directly:
// grunt watch
};
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="scatterplot">
<visualization name="Scatterplot">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
@@ -19,6 +19,6 @@
"grunt-contrib-handlebars": "~0.5.10",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-uglify": "~0.2.2",
"grunt-contrib-watch": "~0.5.1"
"grunt-contrib-watch": "~0.5.3"
}
}
@@ -1,56 +0,0 @@
<p class="help-text">
Use the following controls to how the chart is displayed.
The slide controls can be moved by the mouse or, if the 'handle' is in focus, your keyboard's arrow keys.
Move the focus between controls by using the tab or shift+tab keys on your keyboard.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
<div id="datapointSize" class="form-input numeric-slider-input">
<label for="datapointSize">Size of data point: </label>
<div class="slider-output">{{datapointSize}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
Size of the graphic representation of each data point
</p>
</div>
<div id="animDuration" class="form-input checkbox-input">
<label for="animate-chart">Animate chart transitions?: </label>
<input type="checkbox" id="animate-chart"
class="checkbox control"{{#if animDuration}} checked="true"{{/if}} />
<p class="form-help help-text-small">
Uncheck this to disable the animations used on the chart
</p>
</div>
<div id="width" class="form-input numeric-slider-input">
<label for="width">Chart width: </label>
<div class="slider-output">{{width}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
(not including chart margins and axes)
</p>
</div>
<div id="height" class="form-input numeric-slider-input">
<label for="height">Chart height: </label>
<div class="slider-output">{{height}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
(not including chart margins and axes)
</p>
</div>
<div id="X-axis-label"class="text-input form-input">
<label for="X-axis-label">Re-label the X axis: </label>
<input type="text" name="X-axis-label" id="X-axis-label" value="{{xLabel}}" />
<p class="form-help help-text-small"></p>
</div>
<div id="Y-axis-label" class="text-input form-input">
<label for="Y-axis-label">Re-label the Y axis: </label>
<input type="text" name="Y-axis-label" id="Y-axis-label" value="{{yLabel}}" />
<p class="form-help help-text-small"></p>
</div>
<input id="render-button" type="button" value="Draw" />
@@ -1 +0,0 @@
<svg width="{{width}}" height="{{height}}"></svg>

Before

Width:  |  Height:  |  Size: 49 B

@@ -0,0 +1,47 @@
<p class="help-text">
Use the following controls to how the chart is displayed.
The slide controls can be moved by the mouse or, if the 'handle' is in focus, your keyboard's arrow keys.
Move the focus between controls by using the tab or shift+tab keys on your keyboard.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
<div data-config-key="datapointSize" class="form-input numeric-slider-input">
<label for="datapointSize">Size of data point: </label>
<div class="slider-output">{{datapointSize}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
Size of the graphic representation of each data point
</p>
</div>
<div data-config-key="width" class="form-input numeric-slider-input">
<label for="width">Chart width: </label>
<div class="slider-output">{{width}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
(not including chart margins and axes)
</p>
</div>
<div data-config-key="height" class="form-input numeric-slider-input">
<label for="height">Chart height: </label>
<div class="slider-output">{{height}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
(not including chart margins and axes)
</p>
</div>
<div data-config-key="X-axis-label"class="text-input form-input">
<label for="X-axis-label">Re-label the X axis: </label>
<input type="text" name="X-axis-label" id="X-axis-label" value="{{x.label}}" />
<p class="form-help help-text-small"></p>
</div>
<div data-config-key="Y-axis-label" class="text-input form-input">
<label for="Y-axis-label">Re-label the Y axis: </label>
<input type="text" name="Y-axis-label" id="Y-axis-label" value="{{y.label}}" />
<p class="form-help help-text-small"></p>
</div>
<button class="render-button btn btn-primary active">Draw</button>
@@ -1,56 +0,0 @@
<p class="help-text">
Use the following controls to change the data used by the chart.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
{{! column selector containers }}
<div class="column-select">
<label for="X-select">Data column for X: </label>
<select name="X" id="X-select">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="column-select">
<label for="Y-select">Data column for Y: </label>
<select name="Y" id="Y-select">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! optional id column }}
<div id="include-id">
<label for="include-id-checkbox">Include a third column as data point IDs?</label>
<input type="checkbox" name="include-id" id="include-id-checkbox" />
<p class="help-text-small">
These will be displayed (along with the x and y values) when you hover over
a data point.
</p>
</div>
<div class="column-select" style="display: none">
<label for="ID-select">Data column for IDs: </label>
<select name="ID" id="ID-select">
{{#each allColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! if we're using generic column selection names ('column 1') - allow the user to use the first line }}
<div id="first-line-header" style="display: none;">
<p>Possible headers: {{ possibleHeaders }}
</p>
<label for="first-line-header-checkbox">Use the above as column headers?</label>
<input type="checkbox" name="include-id" id="first-line-header-checkbox"
{{#if usePossibleHeaders }}checked="true"{{/if}}/>
<p class="help-text-small">
It looks like Galaxy couldn't get proper column headers for this data.
Would you like to use the column headers above as column names to select columns?
</p>
</div>
<input id="render-button" type="button" value="Draw" />
<div class="clear"></div>
@@ -0,0 +1,55 @@
<p class="help-text">
Use the following controls to change the data used by the chart.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
{{! column selector containers }}
<div class="column-select">
<label>Data column for X: </label>
<select name="xColumn">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="column-select">
<label>Data column for Y: </label>
<select name="yColumn">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! optional id column }}
<div id="include-id">
<label for="include-id-checkbox">Include a third column as data point IDs?</label>
<input type="checkbox" name="include-id" id="include-id-checkbox" />
<p class="help-text-small">
These will be displayed (along with the x and y values) when you hover over
a data point.
</p>
</div>
<div class="column-select" style="display: none">
<label for="ID-select">Data column for IDs: </label>
<select name="idColumn">
{{#each allColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! if we're using generic column selection names ('column 1') - allow the user to use the first line }}
<div id="first-line-header" style="display: none;">
<p>Possible headers: {{ possibleHeaders }}
</p>
<label for="first-line-header-checkbox">Use the above as column headers?</label>
<input type="checkbox" name="include-id" id="first-line-header-checkbox"
{{#if usePossibleHeaders }}checked="true"{{/if}}/>
<p class="help-text-small">
It looks like Galaxy couldn't get proper column headers for this data.
Would you like to use the column headers above as column names to select columns?
</p>
</div>
<button class="render-button btn btn-primary active">Draw</button>
@@ -0,0 +1,42 @@
<div class="scatterplot-editor tabbable tabs-left">
{{! tab buttons/headers using Bootstrap }}
<ul class="nav nav-tabs">
{{! start with the data controls as the displayed tab }}
<li class="active">
<a title="Use this tab to change which data are used"
href="#data-control" data-toggle="tab">Data Controls</a>
</li>
<li>
<a title="Use this tab to change how the chart is drawn"
href="#chart-control" data-toggle="tab" >Chart Controls</a>
</li>
{{! chart starts as disabled since there's no info yet }}
<li class="disabled">
<a title="This tab will display the chart"
href="#chart-display" data-toggle="tab">Chart</a>
</li>
{{! ... }}
<li class="file-controls">
<!-- <button class="copy-btn btn btn-default"
title="Save this as a new visualization">Save to new</button>-->
<button class="save-btn btn btn-default">Save</button>
</li>
</ul>
{{! data form, chart config form, chart all get their own tab }}
<div class="tab-content">
{{! ---------------------------- tab for data settings form }}
<div id="data-control" class="scatterplot-config-control tab-pane active">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for chart graphics control form }}
<div id="chart-control" class="scatterplot-config-control tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for actual chart }}
<div id="chart-display" class="scatterplot-display tab-pane"></div>
</div>{{! end .tab-content }}
</div>{{! end .chart-control }}
@@ -1,46 +0,0 @@
{{! main layout }}
<div class="scatterplot-container chart-container tabbable tabs-left">
{{! tab buttons/headers using Bootstrap }}
<ul class="nav nav-tabs">
{{! start with the data controls as the displayed tab }}
<li class="active"><a href="#data-control" data-toggle="tab" class="tooltip"
title="Use this tab to change which data are used">Data Controls</a></li>
<li><a href="#chart-control" data-toggle="tab" class="tooltip"
title="Use this tab to change how the chart is drawn">Chart Controls</a></li>
<li><a href="#stats-display" data-toggle="tab" class="tooltip"
title="This tab will display overall statistics for your data">Statistics</a></li>
<li><a href="#chart-display" data-toggle="tab" class="tooltip"
title="This tab will display the chart">Chart</a>
{{! loading indicator - initially hidden }}
<div id="loading-indicator" style="display: none;">
<img class="loading-img" src="{{loadingIndicatorImagePath}}" />
<span class="loading-message">{{message}}</span>
</div>
</li>
</ul>
{{! data form, chart config form, stats, and chart all get their own tab }}
<div class="tab-content">
{{! ---------------------------- tab for data settings form }}
<div id="data-control" class="tab-pane active">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for chart graphics control form }}
<div id="chart-control" class="tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for data statistics }}
<div id="stats-display" class="tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for actual chart }}
<div id="chart-display" class="tab-pane">
{{! chart rendered separately }}
</div>
</div>{{! end .tab-content }}
</div>{{! end .chart-control }}
@@ -1,8 +0,0 @@
<p class="help-text">By column:</p>
<table id="chart-stats-table">
<thead><th></th><th>X</th><th>Y</th></thead>
{{#each stats}}
<tr><td>{{name}}</td><td>{{xval}}</td><td>{{yval}}</td></tr>
</tr>
{{/each}}
</table>
@@ -0,0 +1,276 @@
/* =============================================================================
todo:
import button(display), func(model) - when user doesn't match
Move margins into wid/hi calcs (so final svg dims are w/h)
Better separation of AJAX in scatterplot.js (maybe pass in function?)
Labels should auto fill in chart control when dataset has column_names
Allow column selection/config using the peek output as a base for UI
Allow setting perPage in config
Allow option to auto set width/height based on screen real estate avail.
Handle large number of pages better (Known genes hg19)
Use d3.nest to allow grouping, pagination/filtration by group (e.g. chromCol)
Semantic HTML (figure, caption)
Save as SVG/png
Does it work w/ Galaxy.Frame?
Embedding
Small multiples
Drag & Drop other splots onto current (redraw with new axis and differentiate the datasets)
Remove 'chart' names
Somehow link out from info box?
Subclass on specific datatypes? (vcf, cuffdiff, etc.)
What can be common/useful to other visualizations?
============================================================================= */
/**
* Scatterplot config control UI as a backbone view
* handles:
* configuring which data will be used
* configuring the plot display
*/
var ScatterplotConfigEditor = Backbone.View.extend( LoggableMixin ).extend({
//TODO: !should be a view on a visualization model
//logger : console,
className : 'scatterplot-control-form',
/** initialize requires a configuration Object containing a dataset Object */
initialize : function( attributes ){
if( !this.model ){
this.model = new Visualization({ type: 'scatterplot' });
}
this.log( this + '.initialize, attributes:', attributes );
if( !attributes || !attributes.dataset ){
throw new Error( "ScatterplotConfigEditor requires a dataset" );
}
this.dataset = attributes.dataset;
this.log( 'dataset:', this.dataset );
this.display = new ScatterplotDisplay({
dataset : attributes.dataset,
model : this.model
});
},
// ------------------------------------------------------------------------- CONTROLS RENDERING
render : function(){
//console.log( this + '.render' );
// render the tab controls, areas and loading indicator
this.$el.empty().append( ScatterplotConfigEditor.templates.mainLayout({}));
if( this.model.id ){
this.$el.find( '.copy-btn' ).show();
this.$el.find( '.save-btn' ).text( 'Update saved' );
}
this.$el.find( '[title]' ).tooltip();
// render the tab content
this._render_dataControl();
this._render_chartControls();
this._render_chartDisplay();
// set up behaviours
// auto render if given both x, y column choices
var config = this.model.get( 'config' );
if( this.model.id && _.isFinite( config.xColumn ) && _.isFinite( config.yColumn ) ){
this.renderChart();
}
return this;
},
/** controls for which columns are used to plot datapoints (and ids/additional info to attach if desired) */
_render_dataControl : function( $where ){
//TODO: better handling of missing column names, column types
$where = $where || this.$el;
var editor = this,
dataset = this.dataset,
column_names = dataset.metadata_column_names || [],
config = this.model.get( 'config' );
//console.log( 'metadata_column_types:', this.dataset.metadata_column_types );
//console.log( 'metadata_column_names:', this.dataset.metadata_column_names );
//TODO: to peek based control
var numericColumns = [],
allColumns = _.map( dataset.metadata_column_types, function( type, i ){
// save column data for select rendering, adding metadata name if available in dataset
var column = { index: i, type: type, name: ( column_names[ i ] || ( 'column ' + ( i + 1 )) ) };
// also add column to numerics if numeric type
if( ( column.type === 'int' ) || ( column.type === 'float' ) ){
numericColumns.push( column );
}
return column;
});
if( numericColumns.length < 2 ){
numericColumns = allColumns;
}
//console.log( 'allColumns:', allColumns );
//console.log( 'numericColumns:', numericColumns );
// render the html
var $dataControl = $where.find( '.tab-pane#data-control' );
$dataControl.html( ScatterplotConfigEditor.templates.dataControl({
allColumns : allColumns,
numericColumns : numericColumns
}));
//TODO: column selection boilerplate
// preset to column selectors if they were passed in the config in the query string; set up events
var newConfig = {
xColumn : ( _.isFinite( config.xColumn ) )? ( config.xColumn ): ( numericColumns[0].index ),
yColumn : ( _.isFinite( config.yColumn ) )? ( config.yColumn ): ( numericColumns[1].index ),
idColumn : allColumns[0].index
};
// use an idColumn from the config or attempt to get one different from the numeric
if( _.isFinite( config.idColumn ) ){
newConfig.idColumn = config.idColumn;
} else {
if( allColumns.length > 2 ){
var uniqueCol = _.find( allColumns, function( column, i ){
return i !== newConfig.xColumn && i !== newConfig.yColumn;
});
newConfig.idColumn = uniqueCol.index;
}
}
config = this.model.set( 'config', newConfig, { silent: true }).get( 'config' );
$dataControl.find( '[name="xColumn"]' ).val( config.xColumn ).on( 'change', function(){
editor.model.set( 'config', { xColumn: Number( $( this ).val() ) });
});
$dataControl.find( '[name="yColumn"]' ).val( config.yColumn ).on( 'change', function(){
editor.model.set( 'config', { yColumn: Number( $( this ).val() ) });
});
$dataControl.find( 'select[name="idColumn"]' ).val( config.idColumn ).on( 'change', function(){
editor.model.set( 'config', { idColumn: Number( $( this ).val() ) });
});
if( config.idColumn !== undefined ){
$dataControl.find( '#include-id-checkbox' ).prop( 'checked', true ).trigger( 'change' );
}
$dataControl.find( '[title]' ).tooltip();
return $dataControl;
},
/** tab content to control how the chart is rendered (data glyph size, chart size, etc.) */
_render_chartControls : function( $where ){
$where = $where || this.$el;
var editor = this,
config = this.model.get( 'config' ),
$chartControls = $where.find( '#chart-control' );
// ---- skeleton/form for controls
$chartControls.html( ScatterplotConfigEditor.templates.chartControl( config ) );
//console.debug( '$chartControl:', $chartControls );
// ---- slider controls
// limits for controls (by control/chartConfig id)
//TODO: as class attribute
var controlRanges = {
'datapointSize' : { min: 2, max: 10, step: 1 },
'width' : { min: 200, max: 800, step: 20 },
'height' : { min: 200, max: 800, step: 20 }
};
function onSliderChange(){
// set the model config when changed and update the slider output text
var $this = $( this ),
//note: returns a number nicely enough
newVal = $this.slider( 'value' );
// parent of slide event target has html5 attr data-config-key
editor.model.set( 'config', _.object([[ $this.parent().data( 'config-key' ), newVal ]]) );
$this.siblings( '.slider-output' ).text( newVal );
}
//console.debug( 'numeric sliders:', $chartControls.find( '.numeric-slider-input' ) );
$chartControls.find( '.numeric-slider-input' ).each( function(){
// set up the slider with control ranges, change event; set output text to initial value
var $this = $( this ),
configKey = $this.attr( 'data-config-key' ),
sliderSettings = _.extend( controlRanges[ configKey ], {
value : config[ configKey ],
change : onSliderChange,
slide : onSliderChange
});
//console.debug( configKey + ' slider settings:', sliderSettings );
$this.find( '.slider' ).slider( sliderSettings );
$this.children( '.slider-output' ).text( config[ configKey ] );
});
// ---- axes labels
var columnNames = this.dataset.metadata_column_names || [];
var xLabel = config.xLabel || columnNames[ config.xColumn ] || 'X';
var yLabel = config.yLabel || columnNames[ config.yColumn ] || 'Y';
// set label inputs to current x, y metadata_column_names (if any)
$chartControls.find( 'input[name="X-axis-label"]' ).val( xLabel )
.on( 'change', function(){
editor.model.set( 'config', { xLabel: $( this ).val() });
});
$chartControls.find( 'input[name="Y-axis-label"]' ).val( yLabel )
.on( 'change', function(){
editor.model.set( 'config', { yLabel: $( this ).val() });
});
//console.debug( '$chartControls:', $chartControls );
$chartControls.find( '[title]' ).tooltip();
return $chartControls;
},
/** render the tab content where the chart is displayed (but not the chart itself) */
_render_chartDisplay : function( $where ){
$where = $where || this.$el;
var $chartDisplay = $where.find( '.tab-pane#chart-display' );
this.display.setElement( $chartDisplay );
this.display.render();
$chartDisplay.find( '[title]' ).tooltip();
return $chartDisplay;
},
// ------------------------------------------------------------------------- EVENTS
events : {
'change #include-id-checkbox' : 'toggleThirdColumnSelector',
'click #data-control .render-button' : 'renderChart',
'click #chart-control .render-button' : 'renderChart',
'click .save-btn' : 'saveVisualization',
//'click .copy-btn' : function(e){ this.model.save(); }
},
saveVisualization : function(){
var editor = this;
this.model.save()
.fail( function( xhr, status, message ){
console.error( xhr, status, message );
editor.trigger( 'save:error', view );
alert( 'Error loading data:\n' + xhr.responseText );
})
.then( function(){
editor.render();
});
},
toggleThirdColumnSelector : function(){
// show/hide the id selector on the data settings panel
this.$el.find( 'select[name="idColumn"]' ).parent().toggle();
},
// ------------------------------------------------------------------------- CHART/STATS RENDERING
renderChart : function(){
//console.log( this + '.renderChart' );
// fetch the data, (re-)render the chart
this.$el.find( '.nav li.disabled' ).removeClass( 'disabled' );
this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
this.display.fetchData();
//console.debug( this.display.$el );
},
toString : function(){
return 'ScatterplotConfigEditor(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')';
}
});
ScatterplotConfigEditor.templates = {
mainLayout : scatterplot.editor,
dataControl : scatterplot.datacontrol,
chartControl : scatterplot.chartcontrol
};
//==============================================================================
@@ -0,0 +1,285 @@
// =============================================================================
/**
* Scatterplot display control UI as a backbone view
* handles:
* fetching the data (if needed)
* computing and displaying data stats
* controls for pagination of data (if needed)
*/
var ScatterplotDisplay = Backbone.View.extend({
initialize : function( attributes ){
this.data = null,
this.dataset = attributes.dataset;
this.calcNumPages();
},
calcNumPages : function(){
var config = this.model.get( 'config' );
this.lineCount = this.dataset.metadata_data_lines,
this.numPages = ( this.lineCount )?( Math.ceil( this.lineCount / config.pagination.perPage ) ):( undefined );
if( !this.lineCount || this.numPages === undefined ){
console.warn( 'no data total found' );
}
},
fetchData : function(){
//TODO: doesn't work bc it's rendered in render()...
this.showLoadingIndicator( 'getting data' );
//console.debug( 'currPage', this.config.pagination.currPage );
var view = this,
config = this.model.get( 'config' ),
//TODO: very tied to datasets - should be generalized eventually
xhr = jQuery.getJSON( '/api/datasets/' + this.dataset.id, {
data_type : 'raw_data',
provider : 'dataset-column',
limit : config.pagination.perPage,
offset : ( config.pagination.currPage * config.pagination.perPage )
});
xhr.done( function( data ){
view.data = data.data;
view.trigger( 'data:fetched', view );
view.renderData();
});
xhr.fail( function( xhr, status, message ){
console.error( xhr, status, message );
view.trigger( 'data:error', view );
alert( 'Error loading data:\n' + xhr.responseText );
});
return xhr;
},
showLoadingIndicator : function(){
// display the loading indicator over the tab panels if hidden, update message (if passed)
this.$el.find( '.scatterplot-data-info' ).html([
'<div class="loading-indicator">',
'<span class="fa fa-spinner fa-spin"></span>',
'<span class="loading-indicator-message">loading...</span>',
'</div>'
].join( '' ));
},
template : function(){
var html = [
'<div class="controls clear">',
'<div class="left">',
'</div>',
'<div class="right">',
'<p class="scatterplot-data-info"></p>',
'<button class="stats-toggle-btn">Stats</button>',
'<button class="rerender-btn">Redraw</button>',
'</div>',
'</div>',
'<svg/>', //TODO: id
'<div class="stats-display"></div>'
].join( '' );
return html;
},
render : function(){
this.$el.addClass( 'scatterplot-display' ).html( this.template() );
if( this.data ){
this.renderData();
}
return this;
},
renderData : function(){
this.renderLeftControls();
this.renderRightControls();
this.renderPlot( this.data );
this.getStats();
},
renderLeftControls : function(){
if( this.lineCount ){
this.$el.find( '.controls .left' ).empty().append( this.renderPagination() );
} else {
this.$el.find( '.controls .left' ).empty().append( this.renderPrevNext() );
}
return this;
},
renderRightControls : function(){
var view = this;
this.setLineInfo( this.data );
// clear prev. handlers due to closure around data
this.$el.find( '.stats-toggle-btn' )
.off().click( function(){
view.toggleStats();
});
this.$el.find( '.rerender-btn' )
.off().click( function(){
view.resetZoom();
view.renderPlot( this.data );
});
},
/** render and show the d3 plot into the svg node of the view */
renderPlot : function(){
var view = this,
$svg = this.$el.find( 'svg' );
// turn off stats, clear previous svg, and make it visible
this.toggleStats( false );
$svg.off().empty().show()
// set up listeners for events from plot
.on( 'zoom.scatterplot', function( ev, zoom ){
//TODO: possibly throttle this
//console.debug( 'zoom.scatterplot', zoom.scale, zoom.translate );
view.model.set( 'config', zoom );
});
//TODO: may not be necessary to off/on this more than the initial on
// call the sep. d3 function to generate the plot
scatterplot( $svg.get( 0 ), this.model.get( 'config' ), this.data );
},
setLineInfo : function( data, contents ){
if( data ){
var config = this.model.get( 'config' ),
totalLines = this.lineCount || 'an unknown total',
lineStart = config.pagination.currPage * config.pagination.perPage,
lineEnd = lineStart + data.length;
this.$el.find( '.controls p.scatterplot-data-info' )
.text([ lineStart + 1, 'to', lineEnd, 'of', totalLines ].join( ' ' ));
} else {
this.$el.find( '.controls p.scatterplot-data-info' ).html( contents || '' );
}
return this;
},
resetZoom : function( scale, translate ){
scale = ( scale !== undefined )?( scale ):( 1 );
translate = ( translate !== undefined )?( translate ):( [ 0, 0 ] );
this.model.set( 'config', { scale: scale, translate: translate } );
return this;
},
// ------------------------------------------------------------------------ data pagination
//TODO: to pagination control
goToPage : function( page ){
var pagination = this.model.get( 'config' ).pagination;
//console.debug( 'goToPage', page, pagination, this.numPages );
if( page <= 0 ){ page = 0; }
if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; }
if( page === pagination.currPage ){ return this; }
//console.debug( '\t going to page ' + page )
pagination.currPage = page;
this.model.set( 'config', { pagination: pagination });
this.resetZoom();
this.fetchData();
return this;
},
nextPage : function(){
var currPage = this.model.get( 'config' ).pagination.currPage;
return this.goToPage( currPage + 1 );
},
prevPage : function(){
var currPage = this.model.get( 'config' ).pagination.currPage;
return this.goToPage( currPage - 1 );
},
/** render previous and next pagination buttons */
renderPrevNext : function(){
var config = this.model.get( 'config' );
// if there's no data or there's less than one page of data - return null
if( !this.data ){ return null; }
if( config.pagination.currPage === 0 && this.data.length < config.pagination.perPage ){ return null; }
var view = this,
$prev = $( '<li><a href="javascript:void(0);">Prev</a></li>' )
.click( function(){ view.prevPage(); }),
$next = $( '<li><a href="javascript:void(0);">Next</a></li>' )
.click( function(){ view.nextPage(); });
// disable if it either end
if( config.pagination.currPage === 0 ){
$prev.addClass( 'disabled' );
}
if( this.numPages && config.pagination.currPage === ( this.numPages - 1 ) ){
$next.addClass( 'disabled' );
}
return $( '<ul/>' ).addClass( 'pagination data-prev-next' ).append([ $prev, $next ]);
},
/** render page links for each possible page (if we can) */
renderPagination : function(){
var config = this.model.get( 'config' );
// if there's no data, no page count, or there's less than one page of data - return null
if( !this.data ){ return null; }
if( !this.numPages ){ return null; }
if( config.pagination.currPage === 0 && this.data.length < config.pagination.perPage ){ return null; }
var view = this,
$pagesList = $( '<ul/>' ).addClass( 'pagination data-pages' );
pageNumClick = function( ev ){
view.goToPage( $( this ).data( 'page' ) );
};
for( var i=0; i<this.numPages; i+=1 ){
// add html5 data tag 'page' for later click event handler use
var $pageLi = $([ '<li><a href="javascript:void(0);">', i + 1, '</a></li>' ].join( '' ))
.attr( 'data-page', i ).click( pageNumClick );
// highlight the current page
if( i === config.pagination.currPage ){
$pageLi.addClass( 'active' );
}
$pagesList.append( $pageLi );
}
return $pagesList;
},
// ------------------------------------------------------------------------ statistics display
/** create a webworker to calc stats for data given */
getStats : function(){
if( !this.data ){ return; }
var view = this,
config = this.model.get( 'config' ),
meanWorker = new Worker( '/plugins/visualizations/scatterplot/static/worker-stats.js' );
meanWorker.postMessage({
data : this.data,
keys : [ config.xColumn, config.yColumn ]
});
meanWorker.onerror = function( event ){
meanWorker.terminate();
};
meanWorker.onmessage = function( event ){
view.renderStats( event.data );
};
},
renderStats : function( stats, error ){
//console.debug( 'renderStats:', stats, error );
//console.debug( JSON.stringify( stats, null, ' ' ) );
var config = this.model.get( 'config' ),
$statsTable = this.$el.find( '.stats-display' ),
xLabel = config.x.label, yLabel = config.y.label,
$table = $( '<table/>' ).addClass( 'table' )
.append([ '<thead><th></th><th>', xLabel, '</th><th>', yLabel, '</th></thead>' ].join( '' ))
.append( _.map( stats, function( stat, key ){
return $([ '<tr><td>', key, '</td><td>', stat[0], '</td><td>', stat[1], '</td></tr>' ].join( '' ));
}));
$statsTable.empty().append( $table );
},
toggleStats : function( showStats ){
var $statsDisplay = this.$el.find( '.stats-display' );
showStats = ( showStats === undefined )?( $statsDisplay.is( ':hidden' ) ):( showStats );
if( showStats ){
this.$el.find( 'svg' ).hide();
$statsDisplay.show();
this.$el.find( '.controls .stats-toggle-btn' ).text( 'Plot' );
} else {
$statsDisplay.hide();
this.$el.find( 'svg' ).show();
this.$el.find( '.controls .stats-toggle-btn' ).text( 'Stats' );
}
},
toString : function(){
return 'ScatterplotView()';
}
});
@@ -1,488 +1,283 @@
/* =============================================================================
todo:
outside this:
BUG: setting width, height in plot controls doesn't re-interpolate data locations!!
BUG?: get metadata_column_names (from datatype if necessary)
BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations'
wire label setters, anim setter
TwoVarScatterplot:
??: maybe better to do this with a canvas...
save as visualization
to seperate file?
remove underscore dependencies
add interface to change values (seperate)?
download svg -> base64 encode
incorporate glyphs, glyph state renderers
ScatterplotSettingsForm:
some css bug that lowers the width of settings form when plot-controls tab is open
causes chart to shift
what can be abstracted/reused for other graphs?
avoid direct manipulation of this.plot
allow option to put plot into seperate tab of interface (for small multiples)
provide callback in view to load data incrementally - for large sets
paginate
handle rerender
use endpoint (here and on the server (fileptr))
fetch (new?) data
handle rerender
use d3.TSV?
render warning on long data (> maxDataPoints)
adjust endpoint
selectable list of preset column comparisons (rnaseq etc.)
how to know what sort of Tabular the data is?
smarter about headers
validate columns selection (here or server)
set stats column names by selected columns
move chart into tabbed area...
Scatterplot.mako:
multiple plots on one page (small multiples)
?? ensure svg styles thru d3 or css?
d3: configable (easily)
css: standard - better maintenance
? override at config
============================================================================= */
// =============================================================================
/**
* Two Variable scatterplot visualization using d3
* Uses semi transparent circles to show density of data in x, y grid
* usage :
* var plot = new TwoVarScatterplot({ containerSelector : 'div#my-plot', ... })
* plot.render( xColumnData, yColumnData );
*
* depends on: d3, underscore
* var plot = new scatterplot( $( 'svg' ).get(0), config, data )
*/
function TwoVarScatterplot( config ){
var TICK_LINE_AND_PADDING = 10,
GUESS_AT_SVG_CHAR_WIDTH = 7,
GUESS_AT_SVG_CHAR_HEIGHT = 10,
PADDING = 8,
X_LABEL_TOO_LONG_AT = 5;
// set up logging
//this.debugging = true;
this.log = function(){
if( this.debugging && console && console.debug ){
var args = Array.prototype.slice.call( arguments );
args.unshift( this.toString() );
console.debug.apply( console, args );
}
};
this.log( 'new TwoVarScatterplot:', config );
// ........................................................ set up chart config
// config will default to these values when not passed in
//NOTE: called on new
this.defaults = {
id : 'TwoVarScatterplot',
containerSelector : 'body',
//TODO??: needed?
maxDataPoints : 30000,
datapointSize : 4,
animDuration : 500,
//TODO: variable effect (not always exactly # of ticks set to)
xNumTicks : 10,
yNumTicks : 10,
xAxisLabelBumpY : 40,
yAxisLabelBumpX : -40,
width : 400,
height : 400,
//TODO: anyway to make this a sub-obj?
marginTop : 50,
marginRight : 50,
marginBottom : 50,
marginLeft : 50,
xMin : null,
xMax : null,
yMin : null,
yMax : null,
xLabel : "X",
yLabel : "Y"
};
this.config = _.extend( {}, this.defaults, config );
this.log( 'intial config:', this.config );
this.updateConfig = function( newConfig, rerender ){
// setter for chart config
//TODO: validate here
_.extend( this.config, newConfig );
this.log( this + '.updateConfig:', this.config );
//TODO: implement rerender flag
};
// ........................................................ helpers
this.toString = function(){
return this.config.id;
};
// conv. methods for svg transforms
this.translateStr = function( x, y ){
return 'translate(' + x + ',' + y + ')';
};
this.rotateStr = function( d, x, y ){
return 'rotate(' + d + ',' + x + ',' + y + ')';
function scatterplot( renderTo, config, data ){
//console.log( 'scatterplot', config );
var translateStr = function( x, y ){
return 'translate(' + x + ',' + y + ')';
},
rotateStr = function( d, x, y ){
return 'rotate(' + d + ',' + x + ',' + y + ')';
},
getX = function( d, i ){
//console.debug( d[ config.xColumn ] );
return d[ config.xColumn ];
},
getY = function( d, i ){
//console.debug( d[ config.yColumn ] );
return d[ config.yColumn ];
};
// .................................................................... scales
var stats = {
x : { extent: d3.extent( data, getX ) },
y : { extent: d3.extent( data, getY ) }
};
//TODO: set pan/zoom limits
// from http://stackoverflow.com/questions/10422738/limiting-domain-when-zooming-or-panning-in-d3-js
//self.x.domain([Math.max(self.x.domain()[0], self.options.xmin), Math.min(self.x.domain()[1], self.options.xmax)]);
//self.y.domain([Math.max(self.y.domain()[0], self.options.ymin), Math.min(self.y.domain()[1], self.options.ymax)]);
var interpolaterFns = {
x : d3.scale.linear()
.domain( stats.x.extent )
.range([ 0, config.width ]),
y : d3.scale.linear()
.domain( stats.y.extent )
.range([ config.height, 0 ])
};
// ........................................................ initial element creation
this.adjustChartDimensions = function( top, right, bottom, left ){
//this.log( this + '.adjustChartDimensions', arguments );
top = top || 0;
right = right || 0;
bottom = bottom || 0;
left = left || 0;
this.svg
.attr( "width", this.config.width + ( this.config.marginRight + right )
+ ( this.config.marginLeft + left ) )
.attr( "height", this.config.height + ( this.config.marginTop + top )
+ ( this.config.marginBottom + bottom ) )
// initial is hidden - show it
.style( 'display', 'block' );
// move content group away from margins
//TODO: allow top, right axis
this.content = this.svg.select( "g.content" )
.attr( "transform", this.translateStr( this.config.marginLeft + left, this.config.marginTop + top ) );
};
// ........................................................ data and scales
this.preprocessData = function( data, min, max ){
//this.log( this + '.preprocessData', arguments );
//TODO: filter by min, max if set
// set a cap on the data, limit to first n points
return ( data.length > this.config.maxDataPoints )? ( data.slice( 0, this.config.maxDataPoints ) ): ( data );
};
this.findMinMaxes = function( xCol, yCol, meta ){
//this.log( this + '.findMinMaxes', arguments );
// configuration takes priority, otherwise meta (from the server) if passed, last-resort: compute it here
this.xMin = this.config.xMin || ( meta )?( meta[0].min ):( d3.min( xCol ) );
this.xMax = this.config.xMax || ( meta )?( meta[0].max ):( d3.max( xCol ) );
this.yMin = this.config.yMin || ( meta )?( meta[1].min ):( d3.min( yCol ) );
this.yMax = this.config.yMax || ( meta )?( meta[1].max ):( d3.max( yCol ) );
};
this.setUpScales = function(){
//this.log( this + '.setUpScales', arguments );
// Interpolation for x, y based on data domains
this.xScale = d3.scale.linear()
.domain([ this.xMin, this.xMax ])
.range([ 0, this.config.width ]),
this.yScale = d3.scale.linear()
.domain([ this.yMin, this.yMax ])
.range([ this.config.height, 0 ]);
};
// ........................................................ axis and ticks
this.setUpXAxis = function(){
//this.log( this + '.setUpXAxis', arguments );
// origin: bottom, left
//TODO: incoporate top, right
this.xAxisFn = d3.svg.axis()
.scale( this.xScale )
.ticks( this.config.xNumTicks )
.orient( 'bottom' );
this.xAxis// = content.select( 'g#x-axis' )
.attr( 'transform', this.translateStr( 0, this.config.height ) )
.call( this.xAxisFn );
//this.log( 'xAxis:', this.xAxis );
//TODO: adjust ticks when tick labels are long - move odds down and extend tick line
// (for now) hide them
var xLongestTickLabel = d3.max( _.map( [ this.xMin, this.xMax ],
function( number ){ return ( String( number ) ).length; } ) );
//this.log( 'xLongestTickLabel:', xLongestTickLabel );
if( xLongestTickLabel >= X_LABEL_TOO_LONG_AT ){
this.xAxis.selectAll( 'g' ).filter( ':nth-child(odd)' ).style( 'display', 'none' );
}
this.log( 'this.config.xLabel:', this.config.xLabel );
this.xAxisLabel// = xAxis.select( 'text#x-axis-label' )
.attr( 'x', this.config.width / 2 )
.attr( 'y', this.config.xAxisLabelBumpY )
.attr( 'text-anchor', 'middle' )
.text( this.config.xLabel );
this.log( 'xAxisLabel:', this.xAxisLabel );
// .................................................................... main components
var zoom = d3.behavior.zoom()
.x( interpolaterFns.x )
.y( interpolaterFns.y )
.scaleExtent([ 1, 30 ])
.scale( config.scale || 1 )
.translate( config.translate || [ 0, 0 ] );
//console.debug( renderTo );
var svg = d3.select( renderTo )
.attr( "class", "scatterplot" )
//.attr( "width", config.width + ( config.margin.right + config.margin.left ) )
.attr( "width", '100%' )
.attr( "height", config.height + ( config.margin.top + config.margin.bottom ) );
var content = svg.append( "g" )
.attr( "class", "content" )
.attr( "transform", translateStr( config.margin.left, config.margin.top ) )
.call( zoom );
// a BIG gotcha - zoom (or any mouse/touch event in SVG?) requires the pointer to be over an object
// create a transparent rect to be that object here
content.append( 'rect' )
.attr( "class", "zoom-rect" )
.attr( "width", config.width ).attr( "height", config.height )
.style( "fill", "transparent" );
//console.log( 'svg:', svg, 'content:', content );
// .................................................................... axes
var axis = { x : {}, y : {} };
//console.log( 'x.ticks:', config.x.ticks );
//console.log( 'y.ticks:', config.y.ticks );
axis.x.fn = d3.svg.axis()
.orient( 'bottom' )
.scale( interpolaterFns.x )
.ticks( config.x.ticks )
// this will convert thousands -> k, millions -> M, etc.
.tickFormat( d3.format( 's' ) );
axis.y.fn = d3.svg.axis()
.orient( 'left' )
.scale( interpolaterFns.y )
.ticks( config.y.ticks )
.tickFormat( d3.format( 's' ) );
axis.x.g = content.append( 'g' )
.attr( 'class', 'x axis' )
.attr( 'transform', translateStr( 0, config.height ) )
.call( axis.x.fn );
//console.log( 'axis.x.g:', axis.x.g );
axis.y.g = content.append( 'g' )
.attr( 'class', 'y axis' )
.call( axis.y.fn );
//console.log( 'axis.y.g:', axis.y.g );
// ................................ axis labels
var padding = 6;
// x-axis label
axis.x.label = svg.append( 'text' )
.attr( 'class', 'axis-label' )
.text( config.x.label )
// align to the top-middle
.attr( 'text-anchor', 'middle' )
.attr( 'dominant-baseline', 'text-after-edge' )
.attr( 'x', ( config.width / 2 ) + config.margin.left )
// place 4 pixels below the axis bounds
.attr( 'y', ( config.height + config.margin.bottom + config.margin.top ) - padding );
//console.log( 'axis.x.label:', axis.x.label );
//TODO: anchor to left of x margin/graph
// y-axis label
// place 4 pixels left of the axis.y.g left edge
axis.y.label = svg.append( 'text' )
.attr( 'class', 'axis-label' )
.text( config.y.label )
// align to bottom-middle
.attr( 'text-anchor', 'middle' )
.attr( 'dominant-baseline', 'text-before-edge' )
.attr( 'x', padding )
.attr( 'y', config.height / 2 )
// rotate around the alignment point
.attr( 'transform', rotateStr( -90, padding, config.height / 2 ) );
//console.log( 'axis.y.label:', axis.y.label );
axis.redraw = function _redrawAxis(){
svg.select( ".x.axis" ).call( axis.x.fn );
svg.select( ".y.axis" ).call( axis.y.fn );
};
this.setUpYAxis = function(){
//this.log( this + '.setUpYAxis', arguments );
this.yAxisFn = d3.svg.axis()
.scale( this.yScale )
.ticks( this.config.yNumTicks )
.orient( 'left' );
this.yAxis// = content.select( 'g#y-axis' )
.call( this.yAxisFn );
//this.log( 'yAxis:', this.yAxis );
// a too complicated section for increasing the left margin when tick labels are long
// get the tick labels for the y axis
var yTickLabels = this.yAxis.selectAll( 'text' ).filter( function( e, i ){ return i !== 0; } );
this.log( 'yTickLabels:', yTickLabels );
// get the longest label length (or 0 if no labels)
this.yLongestLabel = d3.max(
//NOTE: d3 returns an nested array - use the plain array inside ([0])
yTickLabels[0].map( function( e, i ){
return ( d3.select( e ).text() ).length;
})
) || 0;
//this.log( 'yLongestLabel:', this.yLongestLabel );
//TODO: lose the guessing if possible
var neededY = TICK_LINE_AND_PADDING + ( this.yLongestLabel * GUESS_AT_SVG_CHAR_WIDTH )
+ PADDING + GUESS_AT_SVG_CHAR_HEIGHT;
//this.log( 'neededY:', neededY );
// increase width for yLongerStr, increase margin for y
//TODO??: (or transform each number: 2k)
this.config.yAxisLabelBumpX = -( neededY - GUESS_AT_SVG_CHAR_HEIGHT );
if( this.config.marginLeft < neededY ){
var adjusting = ( neededY ) - this.config.marginLeft;
adjusting = ( adjusting < 0 )?( 0 ):( adjusting );
//this.log( 'adjusting:', adjusting );
// update dimensions, translations
this.adjustChartDimensions( 0, 0, 0, adjusting );
}
//this.log( 'this.config.yAxisLableBumpx, this.config.marginLeft:',
// this.config.yAxisLabelBumpX, this.config.marginLeft );
this.yAxisLabel// = yAxis.select( 'text#y-axis-label' )
.attr( 'x', this.config.yAxisLabelBumpX )
.attr( 'y', this.config.height / 2 )
.attr( 'text-anchor', 'middle' )
.attr( 'transform', this.rotateStr( -90, this.config.yAxisLabelBumpX, this.config.height / 2 ) )
.text( this.config.yLabel );
//this.log( 'yAxisLabel:', this.yAxisLabel );
};
// ........................................................ grid lines
this.renderGrid = function(){
//this.log( this + '.renderGrid', arguments );
// VERTICAL
// select existing
this.vGridLines = this.content.selectAll( 'line.v-grid-line' )
.data( this.xScale.ticks( this.xAxisFn.ticks()[0] ) );
// append any extra lines needed (more ticks)
this.vGridLines.enter().append( 'svg:line' )
// .................................................................... grid
function renderGrid(){
var grid = { v : {}, h: {} };
// vertical
grid.v.lines = content.selectAll( 'line.v-grid-line' )
// data are the axis ticks; enter, update, exit
.data( interpolaterFns.x.ticks( axis.x.fn.ticks()[0] ) );
// enter: append any extra lines needed (more ticks)
grid.v.lines.enter()
.append( 'svg:line' )
.classed( 'grid-line v-grid-line', true );
// update the attributes of existing and appended
this.vGridLines
.attr( 'x1', this.xScale )
// update: set coords
grid.v.lines
.attr( 'x1', interpolaterFns.x )
.attr( 'x2', interpolaterFns.x )
.attr( 'y1', 0 )
.attr( 'x2', this.xScale )
.attr( 'y2', this.config.height );
// remove unneeded (less ticks)
this.vGridLines.exit().remove();
//this.log( 'vGridLines:', this.vGridLines );
.attr( 'y2', config.height );
// exit: just remove them
grid.v.lines.exit().remove();
//console.log( 'grid.v.lines:', grid.v.lines );
// HORIZONTAL
this.hGridLines = this.content.selectAll( 'line.h-grid-line' )
.data( this.yScale.ticks( this.yAxisFn.ticks()[0] ) );
this.hGridLines.enter().append( 'svg:line' )
// horizontal
grid.h.lines = content.selectAll( 'line.h-grid-line' )
.data( interpolaterFns.y.ticks( axis.y.fn.ticks()[0] ) );
grid.h.lines.enter()
.append( 'svg:line' )
.classed( 'grid-line h-grid-line', true );
this.hGridLines
grid.h.lines
.attr( 'x1', 0 )
.attr( 'y1', this.yScale )
.attr( 'x2', this.config.width )
.attr( 'y2', this.yScale );
this.hGridLines.exit().remove();
//this.log( 'hGridLines:', this.hGridLines );
};
// ........................................................ data points
this.renderDatapoints = function( xCol, yCol, ids ){
this.log( this + '.renderDatapoints', arguments );
var count = 0,
plot = this,
xPosFn = function( d, i ){
//if( d ){ this.log( 'x.data:', newXCol[ i ], 'plotted:', plot.xScale( newXCol[ i ] ) ); }
return plot.xScale( xCol[ i ] );
},
yPosFn = function( d, i ){
//if( d ){ this.log( 'y.data:', newYCol[ i ], 'plotted:', plot.yScale( newYCol[ i ] ) ); }
return plot.yScale( yCol[ i ] );
};
.attr( 'x2', config.width )
.attr( 'y1', interpolaterFns.y )
.attr( 'y2', interpolaterFns.y );
grid.h.lines.exit().remove();
//console.log( 'grid.h.lines:', grid.h.lines );
return grid;
}
var grid = renderGrid();
//this.datapoints = this.addDatapoints( xCol, yCol, ids, ".glyph" );
var datapoints = this.content.selectAll( '.glyph' ).data( xCol );
//// .................................................................... datapoints
var datapoints = content.selectAll( '.glyph' ).data( data )
// enter - NEW data to be added as glyphs
.enter().append( 'svg:circle' )
.classed( "glyph", true )
.attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); })
.attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); })
.attr( "r", 0 );
// enter - NEW data to be added as glyphs: give them a 'entry' position and style
count = 0;
datapoints.enter()
.append( 'svg:circle' )
.each( function(){ count += 1; } )
.classed( "glyph", true )
.attr( "cx", 0 )
.attr( "cy", this.config.height )
// start all bubbles small...
.attr( "r", 0 );
this.log( count, ' new glyphs created' );
// for all EXISTING glyphs and those that need to be added: transition anim to final state
count = 0;
datapoints
// ...animate to final position
.transition().duration( this.config.animDuration )
.each( function(){ count += 1; } )
.attr( "cx", xPosFn )
.attr( "cy", yPosFn )
.attr( "r", plot.config.datapointSize );
this.log( count, ' existing glyphs transitioned' );
// events
// glyphs that need to be removed: transition to from normal state to 'exit' state, remove from DOM
datapoints.exit()
.each( function(){ count += 1; } )
.transition().duration( this.config.animDuration )
.attr( "cy", this.config.height )
.attr( "r", 0 )
.remove();
this.log( count, ' glyphs removed' );
// for all EXISTING glyphs and those that need to be added: transition anim to final state
datapoints.transition().duration( config.animDuration )
.attr( "r", config.datapointSize );
//console.log( 'datapoints:', datapoints );
this._addDatapointEventhandlers( datapoints, xCol, yCol, ids );
};
function _redrawDatapointsClipped(){
return datapoints
//TODO: interpolates twice
.attr( "cx", function( d, i ){ return interpolaterFns.x( getX( d, i ) ); })
.attr( "cy", function( d, i ){ return interpolaterFns.y( getY( d, i ) ); })
.style( 'display', 'block' )
// filter out points now outside the graph content area and hide them
.filter( function( d, i ){
var cx = d3.select( this ).attr( "cx" ),
cy = d3.select( this ).attr( "cy" );
if( cx < 0 || cx > config.width ){ return true; }
if( cy < 0 || cy > config.height ){ return true; }
return false;
}).style( 'display', 'none' );
}
_redrawDatapointsClipped();
this._addDatapointEventhandlers = function( datapoints, xCol, yCol, ids ){
var plot = this;
datapoints
//TODO: remove magic numbers
.on( 'mouseover', function( d, i ){
var datapoint = d3.select( this );
datapoint
.style( 'fill', 'red' )
.style( 'fill-opacity', 1 );
// create horiz, vert lines to axis
plot.content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
// start not at center, but at the edge of the circle - to prevent mouseover thrashing
.attr( 'x1', datapoint.attr( 'cx' ) - plot.config.datapointSize )
.attr( 'y1', datapoint.attr( 'cy' ) )
.attr( 'x2', 0 )
.attr( 'y2', datapoint.attr( 'cy' ) )
.classed( 'hoverline', true );
// .................................................................... behaviors
function zoomed( scale, translateX, translateY ){
//console.debug( 'zoom', this, zoom.scale(), zoom.translate() );
// if the vertical hoverline
if( datapoint.attr( 'cy' ) < plot.config.height ){
plot.content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
.attr( 'x1', datapoint.attr( 'cx' ) )
.attr( 'y1', datapoint.attr( 'cy' ) + plot.config.datapointSize )
.attr( 'x2', datapoint.attr( 'cx' ) )
.attr( 'y2', plot.config.height )
.classed( 'hoverline', true );
}
// re-render axis, grid, and datapoints
$( '.chart-info-box' ).remove();
axis.redraw();
_redrawDatapointsClipped();
grid = renderGrid();
var datapointWindowPos = $( this ).offset();
plot.datapointInfoBox = plot.infoBox(
datapointWindowPos.top, datapointWindowPos.left,
plot.infoHtml( xCol[ i ], yCol[ i ], ( ids )?( ids[ i ] ):( undefined ) )
);
$( 'body' ).append( plot.datapointInfoBox );
})
.on( 'mouseout', function(){
d3.select( this )
.style( 'fill', 'black' )
.style( 'fill-opacity', 0.2 );
plot.content.selectAll( '.hoverline' ).remove();
if( plot.datapointInfoBox ){
plot.datapointInfoBox.remove();
}
});
},
this.render = function( columnData, meta ){
this.log( this + '.render', arguments );
this.log( '\t config:', this.config );
$( svg.node() ).trigger( 'zoom.scatterplot', {
scale : zoom.scale(),
translate : zoom.translate()
});
}
//TODO: programmatically set zoom/pan and save in config
//TODO: set pan/zoom limits
zoom.on( "zoom", zoomed );
// prepare the data
//pre: columns passed are numeric
//pre: at least two columns are passed
//assume: first column is x, second column is y, any remaining aren't used
var xCol = columnData[0],
yCol = columnData[1],
ids = ( columnData.length > 2 )?( columnData[2] ):( undefined );
//this.log( this + '.render', xCol.length, yCol.length, this.config );
//pre: xCol.len == yCol.len
xCol = this.preprocessData( xCol );
yCol = this.preprocessData( yCol );
this.log( 'xCol len', xCol.length, 'yCol len', yCol.length );
this.findMinMaxes( xCol, yCol, meta );
//this.log( 'xMin, xMax, yMin, yMax:', this.xMin, this.xMax, this.yMin, this.yMax );
this.setUpScales();
function infoBox( top, left, d ){
// create an abs pos. element containing datapoint data (d) near the point (top, left)
// with added padding to clear the mouse pointer
left += 8;
return $([
'<div class="chart-info-box" style="position: absolute">',
(( config.idColumn !== undefined )?( '<div>' + d[ config.idColumn ] + '</div>' ):( '' )),
'<div>', getX( d ), '</div>',
'<div>', getY( d ), '</div>',
'</div>'
].join( '' ) ).css({ top: top, left: left, 'z-index': 2 });
}
// find (or build if it doesn't exist) the svg dom infrastructure
if( !this.svg ){ this.svg = d3.select( 'svg' ).attr( "class", "chart" ); }
if( !this.content ){
this.content = this.svg.append( "svg:g" ).attr( "class", "content" ).attr( 'id', this.config.id );
datapoints.on( 'mouseover', function( d, i ){
var datapoint = d3.select( this );
datapoint
.style( 'fill', 'red' )
.style( 'fill-opacity', 1 );
// create horiz line to axis
content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
// start not at center, but at the edge of the circle - to prevent mouseover thrashing
.attr( 'x1', datapoint.attr( 'cx' ) - config.datapointSize )
.attr( 'y1', datapoint.attr( 'cy' ) )
.attr( 'x2', 0 )
.attr( 'y2', datapoint.attr( 'cy' ) )
.classed( 'hoverline', true );
// create vertical line to axis - if not on the x axis
if( datapoint.attr( 'cy' ) < config.height ){
content.append( 'line' )
.attr( 'stroke', 'red' )
.attr( 'stroke-width', 1 )
.attr( 'x1', datapoint.attr( 'cx' ) )
// attributes are strings so, (accrd. to js) '3' - 1 = 2 but '3' + 1 = '31': coerce
.attr( 'y1', +datapoint.attr( 'cy' ) + config.datapointSize )
.attr( 'x2', datapoint.attr( 'cx' ) )
.attr( 'y2', config.height )
.classed( 'hoverline', true );
}
//this.log( 'svg:', this.svg );
//this.log( 'content:', this.content );
this.adjustChartDimensions();
if( !this.xAxis ){ this.xAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'x-axis' ); }
if( !this.xAxisLabel ){
this.xAxisLabel = this.xAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'x-axis-label' );
}
//this.log( 'xAxis:', this.xAxis, 'xAxisLabel:', this.xAxisLabel );
if( !this.yAxis ){ this.yAxis = this.content.append( 'g' ).attr( 'class', 'axis' ).attr( 'id', 'y-axis' ); }
if( !this.yAxisLabel ){
this.yAxisLabel = this.yAxis.append( 'text' ).attr( 'class', 'axis-label' ).attr( 'id', 'y-axis-label' );
}
//this.log( 'yAxis:', this.yAxis, 'yAxisLabel:', this.yAxisLabel );
this.setUpXAxis();
this.setUpYAxis();
this.renderGrid();
this.renderDatapoints( xCol, yCol, ids );
};
this.infoHtml = function( x, y, id ){
var retDiv = $( '<div/>' );
if( id ){
$( '<div/>' ).text( id ).css( 'font-weight', 'bold' ).appendTo( retDiv );
}
$( '<div/>' ).text( x ).appendTo( retDiv );
$( '<div/>' ).text( y ).appendTo( retDiv );
return retDiv.html();
};
//TODO: html for now
this.infoBox = function( top, left, html, adjTop, adjLeft ){
adjTop = adjTop || 0;
adjLeft = adjLeft || 20;
var infoBox = $( '<div />' )
.addClass( 'chart-info-box' )
.css({
'position' : 'absolute',
'top' : top + adjTop,
'left' : left + adjLeft
});
infoBox.html( html );
return infoBox;
};
// show the info box and trigger an event
var bbox = this.getBoundingClientRect();
$( 'body' ).append( infoBox( bbox.top, bbox.right, d ) );
$( svg.node() ).trigger( 'mouseover-datapoint.scatterplot', [ this, d, i ] );
});
datapoints.on( 'mouseout', function(){
// return the point to normal, remove hoverlines and info box
d3.select( this )
.style( 'fill', 'black' )
.style( 'fill-opacity', 0.2 );
content.selectAll( '.hoverline' ).remove();
$( '.chart-info-box' ).remove();
});
}
//==============================================================================
@@ -1,631 +0,0 @@
/* =============================================================================
todo:
I'd like to move the svg creation out of the splot constr. to:
allow adding splots to an existing canvas
allow mult. splots sharing a canvas
outside this:
BUG: setting width, height in plot controls doesn't re-interpolate data locations!!
BUG?: get metadata_column_names (from datatype if necessary)
BUG: single vis in popupmenu should have tooltip with that name NOT 'Visualizations'
wire label setters, anim setter
TwoVarScatterplot:
??: maybe better to do this with a canvas...
save as visualization
to seperate file?
remove underscore dependencies
add interface to change values (seperate)?
download svg -> base64 encode
incorporate glyphs, glyph state renderers
ScatterplotSettingsForm:
some css bug that lowers the width of settings form when plot-controls tab is open
causes chart to shift
what can be abstracted/reused for other graphs?
avoid direct manipulation of this.plot
allow option to put plot into seperate tab of interface (for small multiples)
provide callback in view to load data incrementally - for large sets
paginate
handle rerender
use endpoint (here and on the server (fileptr))
fetch (new?) data
handle rerender
use d3.TSV?
render warning on long data (> maxDataPoints)
adjust endpoint
selectable list of preset column comparisons (rnaseq etc.)
how to know what sort of Tabular the data is?
smarter about headers
validate columns selection (here or server)
set stats column names by selected columns
move chart into tabbed area...
Scatterplot.mako:
multiple plots on one page (small multiples)
?? ensure svg styles thru d3 or css?
d3: configable (easily)
css: standard - better maintenance
? override at config
============================================================================= */
/**
* Scatterplot control UI as a backbone view
* handles:
* getting the desired data
* configuring the plot display
* showing (general) statistics
*
* initialize attributes REQUIRES a dataset and an apiDatasetsURL
*/
var ScatterplotControlForm = BaseView.extend( LoggableMixin ).extend({
//logger : console,
className : 'scatterplot-control-form',
//NOTE: should include time needed to render
dataLoadDelay : 4000,
dataLoadSize : 5000,
loadingIndicatorImage : 'loading_small_white_bg.gif',
fetchMsg : 'Fetching data...',
renderMsg : 'Rendering...',
initialize : function( attributes ){
this.log( this + '.initialize, attributes:', attributes );
this.dataset = null;
this.chartConfig = null;
this.chart = null;
this.loader = null;
// set up refs to the four tab areas
this.$dataControl = null;
this.$chartControl = null;
this.$statsDisplay = null;
this.$chartDisplay = null;
this.dataFetch = null;
this.initializeFromAttributes( attributes );
this.initializeChart( attributes );
this.initializeDataLoader( attributes );
},
initializeFromAttributes : function( attributes ){
// required settings: ensure certain vars we need are passed in attributes
if( !attributes || !attributes.dataset ){
throw( "ScatterplotView requires a dataset" );
} else {
this.dataset = attributes.dataset;
}
if( jQuery.type( this.dataset.metadata_column_types ) === 'string' ){
this.dataset.metadata_column_types = this.dataset.metadata_column_types.split( ', ' );
}
this.log( '\t dataset:', this.dataset );
// attempt to get possible headers from the data's first line
if( this.dataset.comment_lines && this.dataset.comment_lines.length ){
//TODO:??
var firstLine = this.dataset.comment_lines[0],
possibleHeaders = firstLine.split( '\t' );
if( possibleHeaders.length === this.dataset.metadata_column_types.length ){
this.possibleHeaders = possibleHeaders;
}
}
// passed from mako helper
//TODO: integrate to galaxyPaths
//TODO: ?? seems like data loader section would be better
if( !attributes.apiDatasetsURL ){
throw( "ScatterplotView requires a apiDatasetsURL" );
} else {
this.dataURL = attributes.apiDatasetsURL + '/' + this.dataset.id + '?';
}
this.log( '\t dataURL:', this.dataURL );
},
initializeChart : function( attributes ){
// set up the basic chart infrastructure and config (if any)
this.chartConfig = attributes.chartConfig || {};
//if( this.logger ){ this.chartConfig.debugging = true; }
this.log( '\t initial chartConfig:', this.chartConfig );
this.chart = new TwoVarScatterplot( this.chartConfig );
//TODO: remove 2nd ref, use this.chart.config
this.chartConfig = this.chart.config;
},
initializeDataLoader : function( attributes ){
// set up data loader
var view = this;
this.loader = new LazyDataLoader({
//logger : ( this.logger )?( this.logger ):( null ),
// we'll generate this when columns are chosen
url : null,
start : attributes.start || 0,
//NOTE: metadata_data_lines can be null (so we won't know the total)
total : attributes.total || this.dataset.metadata_data_lines,
delay : this.dataLoadDelay,
size : this.dataLoadSize,
buildUrl : function( start, size ){
// currently VERY SPECIFIC to using data_providers.py start_val, max_vals params
return this.url + '&' + jQuery.param({
start_val: start,
max_vals: size
});
}
});
$( this.loader ).bind( 'error', function( event, status, error ){
view.log( 'ERROR:', status, error );
alert( 'ERROR fetching data:\n' + status + '\n' + error );
view.hideLoadingIndicator();
});
},
// ------------------------------------------------------------------------- CONTROLS RENDERING
render : function(){
this.log( this + '.render' );
// render the tab controls, areas and loading indicator
this.$el.append( ScatterplotControlForm.templates.mainLayout({
loadingIndicatorImagePath : '/static/images/' + this.loadingIndicatorImage,
message : ''
}));
// render the tab content
this.$dataControl = this._render_dataControl();
this.$chartControl = this._render_chartControl();
this.$statsDisplay = this.$el.find( '.tab-pane#stats-display' );
this.$chartDisplay = this._render_chartDisplay();
// auto render if given both x, y column choices in query for page
//TODO:?? add autoRender=1 to query maybe?
if( this.chartConfig.xColumn && this.chartConfig.yColumn ){
this.renderChart();
}
// set up behaviours
this.$el.find( '.tooltip' ).tooltip();
// uncomment any of the following to have that tab show on initial load (for testing)
//this.$el.find( 'ul.nav' ).find( 'a[href="#data-control"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#chart-control"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#stats-display"]' ).tab( 'show' );
//this.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
return this;
},
_render_dataControl : function(){
// controls for which columns are used to plot datapoints (and ids/additional info to attach if desired)
var view = this,
allColumns = [],
numericColumns = [],
usePossibleHeaders = ( this.possibleHeaders && this.$dataControl )?
( this.$dataControl.find( '#first-line-header-checkbox' ).is( ':checked' ) ):( false );
// gather column indeces (from metadata_column_types) and names (from metadata_columnnames)
_.each( this.dataset.metadata_column_types, function( type, index ){
// use a 1 based index in names/values within the form (will be dec. when parsed out)
var oneBasedIndex = index + 1,
// default name is 'column <index>'...
name = 'column ' + oneBasedIndex;
// ...but label with the name if available...
if( view.dataset.metadata_column_names ){
name = view.dataset.metadata_column_names[ index ];
// ...or, use the first line as headers if the user wants
} else if( usePossibleHeaders ){
name = view.possibleHeaders[ index ];
}
// cache all columns here
allColumns.push({ index: oneBasedIndex, name: name });
// filter numeric columns to their own list
if( type === 'int' || type === 'float' ){
numericColumns.push({ index: oneBasedIndex, name: name });
}
});
//TODO: other vals: max_vals, start_val, pagination (chart-settings)
// render the html
var $dataControl = this.$el.find( '.tab-pane#data-control' );
$dataControl.html( ScatterplotControlForm.templates.dataControl({
allColumns : allColumns,
numericColumns : numericColumns,
possibleHeaders : ( this.possibleHeaders )?( this.possibleHeaders.join( ', ' ) ):( '' ),
usePossibleHeaders : usePossibleHeaders
}));
if( !this.dataset.metadata_column_names && this.possibleHeaders ){
$dataControl.find( '#first-line-header' ).show();
}
// preset to column selectors if they were passed in the config in the query string
$dataControl.find( '#X-select' ).val( this.chartConfig.xColumn );
$dataControl.find( '#Y-select' ).val( this.chartConfig.yColumn );
if( this.chartConfig.idColumn !== undefined ){
$dataControl.find( '#include-id-checkbox' )
.attr( 'checked', true ).trigger( 'change' );
$dataControl.find( '#ID-select' ).val( this.chartConfig.idColumn );
}
return $dataControl;
},
_render_chartControl : function(){
// tab content to control how the chart is rendered (data glyph size, chart size, etc.)
var view = this,
$chartControl = this.$el.find( '.tab-pane#chart-control' ),
// limits for controls (by control/chartConfig id)
//TODO: move into TwoVarScatterplot
controlRanges = {
'datapointSize' : { min: 2, max: 10, step: 1 },
'width' : { min: 200, max: 800, step: 20 },
'height' : { min: 200, max: 800, step: 20 }
};
// render the html
$chartControl.append( ScatterplotControlForm.templates.chartControl( this.chartConfig ) );
// set up behaviours, js on sliders
$chartControl.find( '.numeric-slider-input' ).each( function(){
var $this = $( this ),
$output = $this.find( '.slider-output' ),
$slider = $this.find( '.slider' ),
id = $this.attr( 'id' );
//chartControl.log( 'slider set up', 'this:', $this, 'slider:', $slider, 'id', id );
// what to do when the slider changes: update display and update chartConfig
//TODO: move out of loop
function onSliderChange(){
var $this = $( this ),
newValue = $this.slider( 'value' );
//chartControl.log( 'slider change', 'this:', $this, 'output:', $output, 'value', newValue );
$output.text( newValue );
//chartControl.chartConfig[ id ] = newValue;
}
$slider.slider( _.extend( controlRanges[ id ], {
value : view.chartConfig[ id ],
change : onSliderChange,
slide : onSliderChange
}));
});
return $chartControl;
},
_render_chartDisplay : function(){
// render the tab content where the chart is displayed (but not the chart itself)
var $chartDisplay = this.$el.find( '.tab-pane#chart-display' );
$chartDisplay.append( ScatterplotControlForm.templates.chartDisplay( this.chartConfig ) );
return $chartDisplay;
},
// ------------------------------------------------------------------------- EVENTS
events : {
'change #include-id-checkbox' : 'toggleThirdColumnSelector',
'change #first-line-header-checkbox' : 'rerenderDataControl',
'click #data-control #render-button' : 'renderChart',
'click #chart-control #render-button' : 'changeChartSettings'
},
toggleThirdColumnSelector : function(){
// show/hide the id selector on the data settings panel
this.$el.find( 'select[name="ID"]' ).parent().toggle();
},
rerenderDataControl : function(){
this.$dataControl = this._render_dataControl();
},
showLoadingIndicator : function( message, callback ){
// display the loading indicator over the tab panels if hidden, update message (if passed)
message = message || '';
var indicator = this.$el.find( 'div#loading-indicator' );
messageBox = indicator.find( '.loading-message' );
if( indicator.is( ':visible' ) ){
if( message ){
messageBox.fadeOut( 'fast', function(){
messageBox.text( message );
messageBox.fadeIn( 'fast', callback );
});
} else {
callback();
}
} else {
if( message ){ messageBox.text( message ); }
indicator.fadeIn( 'fast', callback );
}
},
hideLoadingIndicator : function( callback ){
this.$el.find( 'div#loading-indicator' ).fadeOut( 'fast', callback );
},
// ------------------------------------------------------------------------- CHART/STATS RENDERING
renderChart : function(){
// fetch the data, (re-)render the chart
this.log( this + '.renderChart' );
//TODO: separate data fetch
// this is a complete re-render, so clear the prev. data
this.data = null;
this.meta = null;
// update the chartConfig (here and chart) using chart settings
//TODO: separate and improve (used in changeChartSettings too)
_.extend( this.chartConfig, this.getChartSettings() );
this.log( '\t chartConfig:', this.chartConfig );
this.chart.updateConfig( this.chartConfig, false );
// build the url with the current data settings
this.loader.url = this.dataURL + '&' + jQuery.param( this.getDataSettings() );
this.log( '\t loader: total lines:', this.loader.total, ' url:', this.loader.url );
// bind the new data event to: aggregate data, update the chart and stats with new data
var view = this;
$( this.loader ).bind( 'loaded.new', function( event, response ){
view.log( view + ' loaded.new', response );
// aggregate data and meta
view.postProcessDataFetchResponse( response );
view.log( '\t postprocessed data:', view.data );
view.log( '\t postprocessed meta:', view.meta );
// update the chart and stats
view.showLoadingIndicator( view.renderMsg, function(){
view.chart.render( view.data, view.meta );
view.renderStats( view.data, view.meta );
view.hideLoadingIndicator();
});
});
// when all data loaded - unbind (or we'll start doubling event handlers)
$( this.loader ).bind( 'complete', function( event, data ){
view.log( view + ' complete', data );
$( view.loader ).unbind();
});
// begin loading the data, switch to the chart display tab
view.showLoadingIndicator( view.fetchMsg, function(){
view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
view.loader.load();
});
},
renderStats : function(){
this.log( this + '.renderStats' );
// render the stats table in the stats panel
//TODO: there's a better way
this.$statsDisplay.html( ScatterplotControlForm.templates.statsDisplay({
stats: [
{ name: 'Count', xval: this.meta[0].count, yval: this.meta[1].count },
{ name: 'Min', xval: this.meta[0].min, yval: this.meta[1].min },
{ name: 'Max', xval: this.meta[0].max, yval: this.meta[1].max },
{ name: 'Sum', xval: this.meta[0].sum, yval: this.meta[1].sum },
{ name: 'Mean', xval: this.meta[0].mean, yval: this.meta[1].mean },
{ name: 'Median', xval: this.meta[0].median, yval: this.meta[1].median }
]
}));
},
changeChartSettings : function(){
// re-render the chart with new chart settings and OLD data
var view = this;
newChartSettings = this.getChartSettings();
// update the chart config from the chartSettings panel controls
_.extend( this.chartConfig, newChartSettings );
this.log( 'this.chartConfig:', this.chartConfig );
this.chart.updateConfig( this.chartConfig, false );
// if there's current data, call chart.render with it (no data fetch)
if( view.data && view.meta ){
view.showLoadingIndicator( view.renderMsg, function(){
view.$el.find( 'ul.nav' ).find( 'a[href="#chart-display"]' ).tab( 'show' );
view.chart.render( view.data, view.meta );
view.hideLoadingIndicator();
});
// no current data, call renderChart instead (which will fetch data)
} else {
this.renderChart();
}
},
// ------------------------------------------------------------------------- DATA AGGREGATION
postProcessDataFetchResponse : function( response ){
// the loader only returns new data - it's up to this to munge the fetches together properly
//TODO: we're now storing data in two places: loader and here
// can't we reduce incoming data into loader.data[0]? are there concurrency problems?
this.postProcessData( response.data );
this.postProcessMeta( response.meta );
},
postProcessData : function( newData ){
// stack the column data on top of each other into this.data
//this.log( this + '.postProcessData:', newData );
var view = this;
// if we already have data: aggregate
if( view.data ){
_.each( newData, function( newColData, colIndex ){
//view.log( colIndex + ' data:', newColData );
//TODO??: time, space efficiency of this?
view.data[ colIndex ] = view.data[ colIndex ].concat( newColData );
});
// otherwise: assign (first load)
} else {
view.data = newData;
}
},
postProcessMeta : function( newMeta ){
// munge the meta data (stats) from the server fetches together
//pre: this.data must be preprocessed (needed for medians)
//this.log( this + '.postProcessMeta:', newMeta );
var view = this,
colTypes = this.dataset.metadata_column_types;
// if we already have meta: aggregate
if( view.meta ){
_.each( newMeta, function( newColMeta, colIndex ){
var colMeta = view.meta[ colIndex ],
colType = colTypes[ colIndex ];
//view.log( '\t ' + colIndex + ' postprocessing meta:', newColMeta );
//view.log( colIndex + ' old meta:',
// 'min:', colMeta.min,
// 'max:', colMeta.max,
// 'sum:', colMeta.sum,
// 'mean:', colMeta.mean,
// 'median:', colMeta.median
//);
//!TODO: at what point are we getting int/float overflow on these?!
//??: need to be null safe?
colMeta.count += ( newColMeta.count )?( newColMeta.count ):( 0 );
//view.log( colIndex, 'count:', colMeta.count );
if( ( colType === 'int' ) || ( colType === 'float' ) ){
//view.log( colIndex + ' incoming meta:',
// 'min:', newColMeta.min,
// 'max:', newColMeta.max,
// 'sum:', newColMeta.sum,
// 'mean:', newColMeta.mean,
// 'median:', newColMeta.median
//);
colMeta.min = Math.min( newColMeta.min, colMeta.min );
colMeta.max = Math.max( newColMeta.max, colMeta.max );
colMeta.sum = newColMeta.sum + colMeta.sum;
colMeta.mean = ( colMeta.count )?( colMeta.sum / colMeta.count ):( null );
// median's a pain bc of sorting (requires the data as well)
var sortedCol = view.data[ colIndex ].slice().sort(),
middleIndex = Math.floor( sortedCol.length / 2 );
if( sortedCol.length % 2 === 0 ){
colMeta.median = ( ( sortedCol[ middleIndex ] + sortedCol[( middleIndex + 1 )] ) / 2 );
} else {
colMeta.median = sortedCol[ middleIndex ];
}
//view.log( colIndex + ' new meta:',
// 'min:', colMeta.min,
// 'max:', colMeta.max,
// 'sum:', colMeta.sum,
// 'mean:', colMeta.mean,
// 'median:', colMeta.median
//);
}
});
// otherwise: assign (first load)
} else {
view.meta = newMeta;
//view.log( '\t meta (first load):', view.meta );
}
},
// ------------------------------------------------------------------------- GET DATA/CHART SETTINGS
getDataSettings : function(){
// parse the column values for both indeces (for the data fetch) and names (for the chart)
var columnSelections = this.getColumnSelections(),
columns = [];
this.log( '\t columnSelections:', columnSelections );
//TODO: validate columns - minimally: we can assume either set by selectors or via a good query string
// get column indices for params, include the desired ID column (if any)
//NOTE: these are presented in human-readable 1 base index (to match the data.peek) - adjust
columns = [
columnSelections.X.colIndex - 1,
columnSelections.Y.colIndex - 1
];
if( this.$dataControl.find( '#include-id-checkbox' ).attr( 'checked' ) ){
columns.push( columnSelections.ID.colIndex - 1 );
}
//TODO: other vals: max, start, page
var params = {
data_type : 'raw_data',
provider : 'column_with_stats',
columns : '[' + columns + ']'
};
this.log( '\t data settings (url params):', params );
return params;
},
getColumnSelections : function(){
// gets the current user-selected values for which columns to fetch from the data settings panel
// returns a map: { column-select name (eg. X) : { colIndex : column-selector val,
// colName : selected option text }, ... }
var selections = {};
this.$dataControl.find( 'div.column-select select' ).each( function(){
var $this = $( this ),
val = $this.val();
selections[ $this.attr( 'name' ) ] = {
colIndex : val,
colName : $this.children( '[value="' + val + '"]' ).text()
};
});
return selections;
},
getChartSettings : function(){
// gets the user-selected chartConfig from the chart settings panel
var settings = {},
colSelections = this.getColumnSelections();
//this.log( 'colSelections:', colSelections );
//TODO: simplify with keys and loop
settings.datapointSize = this.$chartControl.find( '#datapointSize.numeric-slider-input' )
.find( '.slider' ).slider( 'value' );
settings.width = this.$chartControl.find( '#width.numeric-slider-input' )
.find( '.slider' ).slider( 'value' );
settings.height = this.$chartControl.find( '#height.numeric-slider-input' )
.find( '.slider' ).slider( 'value' );
// update axes labels using chartSettings inputs (if not at defaults), otherwise the selects' colName
//TODO: a little confusing
var chartSettingsXLabel = this.$chartControl.find( 'input#X-axis-label' ).val(),
chartSettingsYLabel = this.$chartControl.find( 'input#Y-axis-label' ).val();
settings.xLabel = ( chartSettingsXLabel === 'X' )?
( colSelections.X.colName ):( chartSettingsXLabel );
settings.yLabel = ( chartSettingsYLabel === 'Y' )?
( colSelections.Y.colName ):( chartSettingsYLabel );
settings.animDuration = ( this.$chartControl.find( '#animate-chart' ).is( ':checked' ) )?
( this.chart.defaults.animDuration ):( 0 );
this.log( '\t chartSettings:', settings );
return settings;
},
toString : function(){
return 'ScatterplotControlForm(' + (( this.dataset )?( this.dataset.id ):( '' )) + ')';
}
});
ScatterplotControlForm.templates = {
mainLayout : Templates.scatterplotControlForm,
dataControl : Templates.dataControl,
chartControl : Templates.chartControl,
statsDisplay : Templates.statsDisplay,
chartDisplay : Templates.chartDisplay
};
//==============================================================================
@@ -1,182 +0,0 @@
<script type="text/template" class="template-visualization" id="template-visualization-scatterplotControlForm">
{{! main layout }}
<div class="scatterplot-container chart-container tabbable tabs-left">
{{! tab buttons/headers using Bootstrap }}
<ul class="nav nav-tabs">
{{! start with the data controls as the displayed tab }}
<li class="active"><a href="#data-control" data-toggle="tab" class="tooltip"
title="Use this tab to change which data are used">Data Controls</a></li>
<li><a href="#chart-control" data-toggle="tab" class="tooltip"
title="Use this tab to change how the chart is drawn">Chart Controls</a></li>
<li><a href="#stats-display" data-toggle="tab" class="tooltip"
title="This tab will display overall statistics for your data">Statistics</a></li>
<li><a href="#chart-display" data-toggle="tab" class="tooltip"
title="This tab will display the chart">Chart</a>
{{! loading indicator - initially hidden }}
<div id="loading-indicator" style="display: none;">
<img class="loading-img" src="{{loadingIndicatorImagePath}}" />
<span class="loading-message">{{message}}</span>
</div>
</li>
</ul>
{{! data form, chart config form, stats, and chart all get their own tab }}
<div class="tab-content">
{{! ---------------------------- tab for data settings form }}
<div id="data-control" class="tab-pane active">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for chart graphics control form }}
<div id="chart-control" class="tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for data statistics }}
<div id="stats-display" class="tab-pane">
{{! rendered separately }}
</div>
{{! ---------------------------- tab for actual chart }}
<div id="chart-display" class="tab-pane">
{{! chart rendered separately }}
</div>
</div>{{! end .tab-content }}
</div>{{! end .chart-control }}
</script>
<script type="text/template" class="template-visualization" id="template-visualization-dataControl">
<p class="help-text">
Use the following controls to change the data used by the chart.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
{{! column selector containers }}
<div class="column-select">
<label for="X-select">Data column for X: </label>
<select name="X" id="X-select">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="column-select">
<label for="Y-select">Data column for Y: </label>
<select name="Y" id="Y-select">
{{#each numericColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! optional id column }}
<div id="include-id">
<label for="include-id-checkbox">Include a third column as data point IDs?</label>
<input type="checkbox" name="include-id" id="include-id-checkbox" />
<p class="help-text-small">
These will be displayed (along with the x and y values) when you hover over
a data point.
</p>
</div>
<div class="column-select" style="display: none">
<label for="ID-select">Data column for IDs: </label>
<select name="ID" id="ID-select">
{{#each allColumns}}
<option value="{{index}}">{{name}}</option>
{{/each}}
</select>
</div>
{{! if we're using generic column selection names ('column 1') - allow the user to use the first line }}
<div id="first-line-header" style="display: none;">
<p>Possible headers: {{ possibleHeaders }}
</p>
<label for="first-line-header-checkbox">Use the above as column headers?</label>
<input type="checkbox" name="include-id" id="first-line-header-checkbox"
{{#if usePossibleHeaders }}checked="true"{{/if}}/>
<p class="help-text-small">
It looks like Galaxy couldn't get proper column headers for this data.
Would you like to use the column headers above as column names to select columns?
</p>
</div>
<input id="render-button" type="button" value="Draw" />
<div class="clear"></div>
</script>
<script type="text/template" class="template-visualization" id="template-visualization-chartControl">
<p class="help-text">
Use the following controls to how the chart is displayed.
The slide controls can be moved by the mouse or, if the 'handle' is in focus, your keyboard's arrow keys.
Move the focus between controls by using the tab or shift+tab keys on your keyboard.
Use the 'Draw' button to render (or re-render) the chart with the current settings.
</p>
<div id="datapointSize" class="form-input numeric-slider-input">
<label for="datapointSize">Size of data point: </label>
<div class="slider-output">{{datapointSize}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
Size of the graphic representation of each data point
</p>
</div>
<div id="animDuration" class="form-input checkbox-input">
<label for="animate-chart">Animate chart transitions?: </label>
<input type="checkbox" id="animate-chart"
class="checkbox control"{{#if animDuration}} checked="true"{{/if}} />
<p class="form-help help-text-small">
Uncheck this to disable the animations used on the chart
</p>
</div>
<div id="width" class="form-input numeric-slider-input">
<label for="width">Chart width: </label>
<div class="slider-output">{{width}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
(not including chart margins and axes)
</p>
</div>
<div id="height" class="form-input numeric-slider-input">
<label for="height">Chart height: </label>
<div class="slider-output">{{height}}</div>
<div class="slider"></div>
<p class="form-help help-text-small">
(not including chart margins and axes)
</p>
</div>
<div id="X-axis-label"class="text-input form-input">
<label for="X-axis-label">Re-label the X axis: </label>
<input type="text" name="X-axis-label" id="X-axis-label" value="{{xLabel}}" />
<p class="form-help help-text-small"></p>
</div>
<div id="Y-axis-label" class="text-input form-input">
<label for="Y-axis-label">Re-label the Y axis: </label>
<input type="text" name="Y-axis-label" id="Y-axis-label" value="{{yLabel}}" />
<p class="form-help help-text-small"></p>
</div>
<input id="render-button" type="button" value="Draw" />
</script>
<script type="text/template" class="template-visualization" id="template-visualization-statsDisplay">
<p class="help-text">By column:</p>
<table id="chart-stats-table">
<thead><th></th><th>X</th><th>Y</th></thead>
{{#each stats}}
<tr><td>{{name}}</td><td>{{xval}}</td><td>{{yval}}</td></tr>
</tr>
{{/each}}
</table>
</script>
<script type="text/template" class="template-visualization" id="template-visualization-chartDisplay">
<svg width="{{width}}" height="{{height}}"></svg>
</script>
@@ -0,0 +1,89 @@
var numericColumnStats = function( data, keys ){
var console = console || { debug: function(){} },
stats = {};
// precondition: keys is an array of keys accessible on each data in data
if( !keys || !keys.length ){
throw new Error( 'keys is a required parameter and must be an array:' + keys );
}
if( !data || !data.length ){
return stats;
}
function parseVal( val ){
if( val === null ){
throw new Error( 'Null value' );
}
val = Number( val );
if( isNaN( val ) || !isFinite( val ) ){
throw new Error( 'NaN or non-finite number' );
}
return val;
}
// does this effect the data in the other thread?
[ 'min', 'max', 'sum', 'mean', 'median', 'count' ].forEach( function( name, i ){
stats[ name ] = new Array( keys.length );
});
var keyIndex = 0,
separatedCols = new Array( keys.length );
keys.forEach( function( key, keyIndex ){
stats.min[ keyIndex ] = 0;
stats.max[ keyIndex ] = 0;
stats.sum[ keyIndex ] = 0;
separatedCols[ keyIndex ] = [];
});
// work backwards to prevent co-modification problems while splitting up data into columns
for( var dataIndex=( data.length - 1 ); dataIndex>=0; dataIndex-=1 ){
var datum = data.pop( dataIndex );
for( keyIndex=0; keyIndex<keys.length; keyIndex+=1 ){
var key = keys[ keyIndex ],
datumColVal = datum[ key ];
try {
//NOTE: parsing value as number
datumColVal = parseVal( datumColVal );
} catch( e ){
continue;
}
// separate the columns
separatedCols[ keyIndex ].unshift( datumColVal );
// get the other stats
stats.min[ keyIndex ] = Math.min( stats.min[ keyIndex ], datumColVal );
stats.max[ keyIndex ] = Math.max( stats.max[ keyIndex ], datumColVal );
stats.sum[ keyIndex ] += datumColVal;
}
}
// get counts, mean, median
function comparator( a, b ){
if( a < b ){ return -1; }
if( a < b ){ return 1; }
return 0;
}
for( keyIndex=0; keyIndex<keys.length; keyIndex+=1 ){
var count = separatedCols[ keyIndex ].length,
sum = stats.sum[ keyIndex ];
stats.count[ keyIndex ] = count;
stats.mean[ keyIndex ] = ( sum / count );
// sort columns for median
separatedCols[ keyIndex ].sort( comparator );
// odd count -> straight forward median
var middleDataIndex = Math.floor( count / 2 );
if( count % 2 === 1 ){
stats.median[ keyIndex ] = separatedCols[ keyIndex ][ middleDataIndex ];
} else {
var middleValA = separatedCols[ keyIndex ][ middleDataIndex ],
middleValB = separatedCols[ keyIndex ][( middleDataIndex + 1 )];
stats.median[ keyIndex ] = ( middleValA + middleValB ) / 2;
}
}
return stats;
};
File diff suppressed because one or more lines are too long
@@ -1,171 +1,213 @@
/*TODO: use/move into base.less*/
* { margin: 0px; padding: 0px; }
/* -------------------------------------------- general layout */
div.tab-pane {
padding: 8px;
}
* { margin: 0; padding: 0; }
/* -------------------------------------------- header */
.header {
margin-bottom: 8px;
}
.chart-header > * { margin: 0; padding: 0; }
#chart-header {
padding : 8px;
background-color: #ebd9b2;
.chart-header {
margin-bottom: 16px;
overflow: auto;
background-color: #ebd9b2;
padding : 16px 12px 12px 12px;
}
#chart-header .subtitle {
margin: -4px 0px 0px 4px;
padding : 0;
.chart-header h2 {
margin-bottom: 4px;
}
.chart-header p {
color: white;
font-size: small;
}
/* -------------------------------------------- main layout */
#scatterplot {
/*from width + margin of chart?*/
.scatterplot-editor > * { margin: 0; padding: 0; }
.scatterplot-editor .tab-pane {
padding: 8px;
}
.scatterplot-container .tab-pane {
.scatterplot-editor .help-text,
.scatterplot-editor .help-text-small {
color: grey;
margin-bottom: 8px;
}
/* -------------------------------------------- all controls */
.scatterplot-editor .help-text {
margin-top: 4px;
}
#scatterplot input[type=button],
#scatterplot select {
.scatterplot-editor .help-text-small {
font-size: smaller;
}
.scatterplot-editor .file-controls {
float: right;
/*margin-right: 16px;*/
}
.file-controls .btn {
height: 24px;
padding: 0px 10px 0px 10px;
line-height: 24px;
}
.file-controls .copy-btn {
display: none;
}
/* ============================================ config controls */
.scatterplot-config-control {
max-width: 768px;
}
.scatterplot-config-control input[type="button"],
.scatterplot-config-control button,
.scatterplot-config-control select {
width: 100%;
max-width: 256px;
margin-bottom: 8px;
}
#scatterplot .help-text,
#scatterplot .help-text-small {
color: grey;
.scatterplot-config-control input[type="text"] {
width: 100%;
max-width: 512px;
border: 1px solid lightgrey;
}
#scatterplot .help-text {
padding-bottom: 16px;
.scatterplot-config-control input[type="checkbox"] {
display: inline-block;
margin-top: -3px;
}
#scatterplot .help-text-small {
padding: 4px;
font-size: smaller;
}
#scatterplot > * {
}
#scatterplot input[value=Draw] {
.scatterplot-config-control .render-button {
display: block;
margin-top: 16px;
}
#scatterplot .numeric-slider-input {
max-width: 70%;
}
/* -------------------------------------------- data controls */
/* -------------------------------------------- chart controls */
#chart-control .form-input {
/*display: table-row;*/
}
#chart-control label {
/*text-align: right;*/
.scatterplot-config-control .form-input {
margin-bottom: 8px;
/*display: table-cell;*/
}
#chart-control .slider {
/*display: table-cell;*/
.scatterplot-config-control .slider {
height: 8px;
display: block;
margin: 8px 0px 0px 8px;
margin-bottom: 8px;
}
#chart-control .slider-output {
/*display: table-cell;*/
.scatterplot-config-control .slider-output {
float: right;
}
#chart-control input[type="text"] {
border: 1px solid lightgrey;
}
/* -------------------------------------------- statistics */
#stats-display table#chart-stats-table {
width: 100%;
}
#stats-display #chart-stats-table th {
width: 30%;
padding: 4px;
font-weight: bold;
color: grey;
}
#stats-display #chart-stats-table td {
border: solid lightgrey;
border-width: 1px 0px 0px 1px;
padding: 4px;
}
#stats-display #chart-stats-table td:nth-child(1) {
border-width: 1px 0px 0px 0px;
padding-right: 1em;
text-align: right;
font-weight: bold;
color: grey;
}
/* ============================================ plot display */
/* -------------------------------------------- load indicators */
#loading-indicator {
margin: 12px 0px 0px 8px;
.scatterplot-display .loading-indicator {
color: grey;
}
#scatterplot #loading-indicator .loading-message {
.scatterplot-display .loading-indicator .loading-indicator-message {
display: inline-block;
margin-left: 4px;
font-style: italic;
font-size: smaller;
color: grey;
}
/* -------------------------------------------- display controls */
.scatterplot-display .controls {
width: 100%;
margin-bottom: 4px;
border: 1px solid lightgrey;
border-radius: 3px;
padding: 8px 8px 4px 8px;
}
.scatterplot-display .controls button {
display: inline-block;
min-width: 0;
width: auto;
margin: 0px;
}
.scatterplot-display .controls .left {
float: left;
}
.scatterplot-display .controls .pagination {
display: inline-block;
margin: 0px;
}
.scatterplot-display .controls .data-prev-next {
display: inline-block;
margin: 0px 4px 0px 0px;
}
.scatterplot-display .controls .pagination-container {
display: inline-block;
overflow: auto;
max-width: 256px;
/*very tweaky*/
margin: 0px 8px 0px 0px;
height: 27px;
padding-top: 5px;
}
.scatterplot-display .controls .pagination-container a {
display: inline;
/* WTH, bootstrap? */
position: static;
float: none;
/* up from 4px -> 5px */
padding: 5px 10px 5px 10px;
}
.scatterplot-display .controls .right {
float: right;
margin-bottom: 4px;
}
.scatterplot-display .controls p {
display: inline-block;
margin: 0px 4px 0px 4px;
/*to better match the buttons*/
vertical-align: middle;
line-height: 24px;
}
.scatterplot-display button.rerender-btn {
margin-left: 4px;
}
.scatterplot-display button.stats-toggle-btn {
margin-left: 4px;
}
/* -------------------------------------------- chart area */
#chart-holder {
overflow: auto;
margin-left: 8px;
svg.scatterplot {
border: 1px solid lightgrey;
border-radius: 3px;
}
svg .grid-line {
svg.scatterplot .grid-line {
fill: none;
stroke: lightgrey;
stroke-opacity: 0.5;
stroke-opacity: 0.2;
shape-rendering: crispEdges;
stroke-dasharray: 3, 3;
}
svg .axis path, svg .axis line {
svg.scatterplot .axis path,
svg.scatterplot .axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
svg .axis text {
font-family: monospace;
font-size: 12px;
svg.scatterplot .axis text {
font-size: 9px;
}
svg #x-axis-label, svg #y-axis-label {
svg.scatterplot .axis-label {
font-family: sans-serif;
font-size: 10px;
font-size: 13px;
}
svg .glyph {
svg.scatterplot .glyph {
stroke: none;
fill: black;
fill-opacity: 0.2;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
onmessage = function( event ){
importScripts( 'numeric-column-stats.js' );
postMessage( numericColumnStats( event.data.data, event.data.keys ) );
self.close();
};
@@ -1,8 +1,19 @@
<%
default_title = "Scatterplot of '" + hda.name + "'"
info = hda.name
if hda.info:
info += ' : ' + hda.info
# optionally bootstrap data from dprov
##data = list( hda.datatype.dataset_column_dataprovider( hda, limit=10000 ) )
%>
## ----------------------------------------------------------------------------
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>${hda.name} | ${visualization_name}</title>
<title>${title or default_title} | ${visualization_display_name}</title>
## ----------------------------------------------------------------------------
<link type="text/css" rel="Stylesheet" media="screen" href="/static/style/base.css">
@@ -13,55 +24,50 @@
## ----------------------------------------------------------------------------
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery.js"></script>
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery.migrate.js"></script>
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery-ui.js"></script>
<script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script>
<script type="text/javascript" src="/static/scripts/libs/underscore.js"></script>
<script type="text/javascript" src="/static/scripts/libs/backbone/backbone.js"></script>
<script type="text/javascript" src="/static/scripts/libs/backbone/backbone-relational.js"></script>
<script type="text/javascript" src="/static/scripts/libs/handlebars.runtime.js"></script>
<script type="text/javascript" src="/static/scripts/libs/d3.js"></script>
<script type="text/javascript" src="/static/scripts/libs/bootstrap.js"></script>
<script type="text/javascript" src="/static/scripts/libs/jquery/jquery-ui.js"></script>
<script type="text/javascript" src="/static/scripts/utils/LazyDataLoader.js"></script>
<script type="text/javascript" src="/static/scripts/mvc/base-mvc.js"></script>
<script type="text/javascript" src="/static/scripts/mvc/visualization/visualization-model.js"></script>
<script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot.js"></script>
<script type="text/javascript" src="/plugins/visualizations/scatterplot/static/scatterplot-edit.js"></script>
</head>
## ----------------------------------------------------------------------------
<body>
%if not embedded:
## dataset info: only show if on own page
<div id="chart-header" class="header">
<h2 class="title">Scatterplot of '${hda.name}'</h2>
<p class="subtitle">${hda.info}</p>
<div class="chart-header">
<h2>${title or default_title}</h2>
<p>${info}</p>
</div>
%endif
<div id="scatterplot" class="scatterplot-control-form"></div>
<div class="scatterplot-editor"></div>
<script type="text/javascript">
$(function(){
var hda = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ) )},
querySettings = ${h.to_json_string( query_args )},
chartConfig = _.extend( querySettings, {
containerSelector : '#chart',
//TODO: move to ScatterplotControlForm.initialize
marginTop : ( querySettings.marginTop > 20 )?( querySettings.marginTop ):( 20 ),
xColumn : querySettings.xColumn,
yColumn : querySettings.yColumn,
idColumn : querySettings.idColumn
});
//console.debug( querySettings );
var settingsForm = new ScatterplotControlForm({
dataset : hda,
apiDatasetsURL : "${h.url_for( controller='/api/datasets', action='index' )}",
el : $( '#scatterplot' ),
chartConfig : chartConfig
}).render();
var model = new ScatterplotModel({
id : ${h.to_json_string( visualization_id )} || undefined,
title : "${title or default_title}",
config : ${h.to_json_string( config, indent=2 )}
}),
hdaJson = ${h.to_json_string( trans.security.encode_dict_ids( hda.to_dict() ), indent=2 )},
editor = new ScatterplotConfigEditor({
el : $( '.scatterplot-editor' ).attr( 'id', 'scatterplot-editor-hda-' + hdaJson.id ),
model : model,
dataset : hdaJson
}).render();
window.editor = editor;
model.on( 'change:title', function(){
$( '.chart-header h2' ).text( model.get( 'title' ) );
})
});
</script>
%endif
</body>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="sweepster">
<visualization name="Sweepster" disabled="true">
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
@@ -23,5 +23,5 @@
<!-- template_root and template are currently ignored for the 'built-in' visualizations -->
<template_root>webapps/galaxy/visualization</template_root>
<template>sweepster.mako</template>
<render_location>_top</render_location>
<render_target>_top</render_target>
</visualization>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="trackster">
<visualization name="Trackster">
<!--not tested yet -->
<data_sources>
<data_source>
@@ -26,5 +26,5 @@
<!-- template_root and template are currently ignored for the 'built-in' visualizations -->
<template_root>webapps/galaxy/visualization/tracks</template_root>
<template>browser.mako</template>
<render_location>_top</render_location>
<render_target>_top</render_target>
</visualization>
@@ -1,10 +1,17 @@
<!-- each visualization must have a template (all other elements are optional) -->
<!ELEMENT visualization (data_sources*,params*,template_root*,template,link_text*,render_location*)>
<!-- visualization name (e.g. 'trackster', 'scatterplot', etc.) is required -->
<!ELEMENT visualization (description*,data_sources*,params*,template_root*,template,render_target*)>
<!-- visualization
name: the title/display name of the visualization (e.g. 'Trackster', 'Fastq Stats', etc.) REQUIRED
disabled: if included (value does not matter), this attribute will prevent the visualization being loaded
-->
<!ATTLIST visualization
name CDATA #REQUIRED
disabled CDATA #IMPLIED
>
<!ELEMENT description (#PCDATA)>
<!-- a text description of what the visualization does -->
<!ELEMENT data_sources (data_source*)>
<!-- data sources are elements that describe what objects (HDAs, LDDAs, Job, User, etc.)
are applicable to a visualization. Often these are used to fetch applicable links
@@ -121,10 +128,8 @@
<!ELEMENT template_root (#PCDATA)>
<!-- template: the template used to render the visualization. REQUIRED -->
<!ELEMENT template (#PCDATA)>
<!-- link_text: the text component of an html anchor displayed when the registry builds the link information -->
<!ELEMENT link_text (#PCDATA)>
<!-- render_location: used as the target attribute of the link to the visualization.
<!-- render_target: used as the target attribute of the link to the visualization.
Can be 'galaxy_main', '_top', '_blank'. DEFAULT: 'galaxy_main'
-->
<!-- TODO: rename -> render_target -->
<!ELEMENT render_location (#PCDATA)>
<!ELEMENT render_target (#PCDATA)>
-8
View File
@@ -9,14 +9,6 @@ util Package
:undoc-members:
:show-inheritance:
:mod:`hyphy_util` Module
------------------------
.. automodule:: galaxy.tools.util.hyphy_util
:members:
:undoc-members:
:show-inheritance:
:mod:`maf_utilities` Module
---------------------------
@@ -205,6 +205,11 @@ used in the web form, like 'link_data_only' and etc.
The request and response format should be considered alpha and are subject to change.
API Return Codes and Formats
==================
A set of error codes for API requests is being established and will be
documented here. This is a long-term project however so stayed tuned.
API Controllers
===============
@@ -297,6 +302,14 @@ API Controllers
:undoc-members:
:show-inheritance:
:mod:`lda_datasets` Module
--------------------------
.. automodule:: galaxy.webapps.galaxy.api.lda_datasets
:members:
:undoc-members:
:show-inheritance:
:mod:`libraries` Module
-----------------------
@@ -393,3 +406,67 @@ API Controllers
:undoc-members:
:show-inheritance:
API Design Guidelines
=====================
The following section outlines guidelines related to extending and/or modifing
the Galaxy API. The Galaxy API has grown in an ad-hoc fashion over time by
many contributors and so clients SHOULD NOT expect the API will conform to
these guidelines - but developers contributing to the Galaxy API SHOULD follow
these guidelines.
- API functionality should include docstring documentation for consumption
by readthedocs.org.
- Developers should familarize themselves with the HTTP status code definitions
http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html. The API responses
should properly set the status code according to the result - in particular
2XX responses should be used for successful requests, 4XX for various
kinds of client errors, and 5XX for errors on the server side.
- If there is an error processing some part of request (one item in a list
for instance), the status code should be set to reflect the error and the
partial result may or may not be returned depending on the controller -
this behavior should be documented.
- (TODO) API methods should throw a finite number of exceptions (defined in)
`galaxy.exceptions` and these should subclass `MessageException` and not
paste/wsgi HTTP exceptions. When possible, the framework itself should be
responsible catching these exceptions, setting the status code, and
building an error response.
- Error responses should not consist of plain text strings - they should be
dictionaries describing the error and containing the following keys (TODO:
spell out nature of this.) Various error conditions (once a format has
been chosen and framework to enforce it in place) should be spelled out
in this document.
- Backward compatibility is important and should maintained when possible.
If changing behavior in a non-backward compatibile way please ensure one
of the following holds - there is a strong reason to believe no consumers
depend on a behavior, the behavior is effectively broken, or the API
method being modified has not been part of a tagged dist release.
The following bullet points represent good practices more than guidelines, please
consider them when modifying the API.
- Functionality should not be copied and pasted between controllers -
consider refactoring functionality into associated classes or short of
that into Mixins (http://en.wikipedia.org/wiki/Composition_over_inheritance).
- API additions are more permanent changes to Galaxy than many other potential
changes and so a second opinion on API changes should be sought. (Consider a
pull request!)
- New API functionality should include functional tests. These functional
tests should be implemented in Python and placed in
`test/functional/api`. (Once such a framework is in place - it is not
right now).
- Changes to reflect modifications to the API should be pushed upstream to
the BioBlend project possible.
Longer term goals/notes.
- It would be advantageous to have a clearer separation of anonymous and
admin handling functionality.
- If at some point in the future, functionality needs to be added that
breaks backward compatibility in a significant way to a compontent used by
the community should be alerted - a "dev" variant of the API will be
established and the community should be alerted and given a timeframe
for when the old behavior will be replaced with the new behavior.
- Consistent standards for range-based requests, batch requests, filtered
requests, etc... should be established and documented here.
+4 -4
View File
@@ -26,9 +26,8 @@ pycrypto = 2.5
pysam = 0.4.2
pysqlite = 2.5.6
python_lzo = 1.08_2.03_static
simplejson = 2.1.1
threadframe = 0.2
guppy = 0.1.8
guppy = 0.1.10
SQLAlchemy = 0.7.9
; msgpack_python = 0.2.4
@@ -54,6 +53,7 @@ PasteDeploy = 1.5.0
pexpect = 2.4
python_openid = 2.2.5
python_daemon = 1.5.5
pytz = 2013.9
Routes = 1.12.3
sqlalchemy_migrate = 0.7.2
ssh = 1.7.14
@@ -61,10 +61,10 @@ SVGFig = 1.1.6
Tempita = 0.5.1
twill = 0.9
WebError = 0.8a
WebHelpers = 0.2
WebHelpers = 1.3
WebOb = 0.8.5
wsgiref = 0.1.2
Babel = 0.9.4
Babel = 1.3
wchartype = 0.1
Whoosh = 0.3.18
; fluent_logger = 0.3.3
+79 -9
View File
@@ -2,20 +2,31 @@
# A good place to look for nose info: http://somethingaboutorange.com/mrl/projects/nose/
# The test/install_and_test_tool_shed_repositories/functional_tests.py can not be executed directly, because it must have certain functional test definitions
# in sys.argv. Running it through this shell script is the best way to ensure that it has the required definitions.
# The test/install_and_test_tool_shed_repositories/functional_tests.py cannot be executed directly because it must
# have certain functional test definitions in sys.argv. Running it through this shell script is the best way to
# ensure that it has the required definitions.
# This script requires the following environment variables:
# This script requires setting of the following environment variables:
# GALAXY_INSTALL_TEST_TOOL_SHED_API_KEY - must be set to the API key for the tool shed that is being checked.
# GALAXY_INSTALL_TEST_TOOL_SHED_URL - must be set to a URL that the tool shed is listening on.
# If the tool shed url is not specified in tool_sheds_conf.xml, GALAXY_INSTALL_TEST_TOOL_SHEDS_CONF must be set to a tool sheds configuration file
# that does specify that url, otherwise repository installation will fail.
# If the tool shed url is not specified in tool_sheds_conf.xml, GALAXY_INSTALL_TEST_TOOL_SHEDS_CONF must be set to
# a tool sheds configuration file that does specify that url or repository installation will fail.
# This script accepts the command line option -w to select which set of tests to run. The default behavior is to test
# first tool_dependency_definition repositories and then repositories with tools. Provide the value 'dependencies'
# to test only tool_dependency_definition repositories or 'tools' to test only repositories with tools.
if [ -z $GALAXY_INSTALL_TEST_TOOL_SHED_API_KEY ] ; then
echo "This script requires the GALAXY_INSTALL_TEST_TOOL_SHED_API_KEY environment variable to be set and non-empty."
exit 1
fi
if [ -z $GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR ] ; then
echo "This script requires the GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR environment variable to be set to your configured tool dependency path."
exit 1
fi
if [ -z $GALAXY_INSTALL_TEST_TOOL_SHED_URL ] ; then
echo "This script requires the GALAXY_INSTALL_TEST_TOOL_SHED_URL environment variable to be set and non-empty."
exit 1
@@ -37,7 +48,66 @@ else
fi
fi
python test/install_and_test_tool_shed_repositories/functional_tests.py $* -v --with-nosehtml --html-report-file \
test/install_and_test_tool_shed_repositories/run_functional_tests.html \
test/install_and_test_tool_shed_repositories/functional/test_install_repositories.py \
test/functional/test_toolbox.py
if [ -z $GALAXY_INSTALL_TEST_SHED_TOOL_PATH ] ; then
export GALAXY_INSTALL_TEST_SHED_TOOL_PATH='/tmp/shed_tools'
fi
if [ ! -d $GALAXY_INSTALL_TEST_SHED_TOOL_PATH ] ; then
mkdir -p $GALAXY_INSTALL_TEST_SHED_TOOL_PATH
fi
test_tool_dependency_definitions () {
# Test installation of repositories of type tool_dependency_definition.
if [ -f $GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR/stage_1_complete ] ; then
rm $GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR/stage_1_complete
fi
echo "Starting stage 1, tool dependency definitions."
python test/install_and_test_tool_shed_repositories/tool_dependency_definitions/functional_tests.py $* -v --with-nosehtml --html-report-file \
test/install_and_test_tool_shed_repositories/tool_dependency_definitions/run_functional_tests.html \
test/install_and_test_tool_shed_repositories/functional/test_install_repositories.py \
test/functional/test_toolbox.py
echo "Stage 1 complete, exit code $?"
touch $GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR/stage_1_complete
}
test_repositories_with_tools () {
if [ ! -f $GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR/stage_1_complete ] ; then
echo 'Stage 1 did not complete its run, exiting.'
exit 1
fi
echo "Starting stage 2, repositories with tools."
# Test installation of repositories that contain valid tools with defined functional tests and a test-data directory containing test files.
python test/install_and_test_tool_shed_repositories/repositories_with_tools/functional_tests.py $* -v --with-nosehtml --html-report-file \
test/install_and_test_tool_shed_repositories/repositories_with_tools/run_functional_tests.html \
test/install_and_test_tool_shed_repositories/functional/test_install_repositories.py \
test/functional/test_toolbox.py
echo "Stage 2 complete, exit code $?"
rm $GALAXY_INSTALL_TEST_TOOL_DEPENDENCY_DIR/stage_1_complete
}
which='both'
while getopts "w:" arg; do
case $arg in
w)
which=$OPTARG
;;
esac
done
case $which in
# Use "-w tool_dependency_definitions" when you want to test repositories of type tool_dependency_definition.
tool_dependency_definitions)
test_tool_dependency_definitions
;;
# Use "-w repositories_with_tools" parameter when you want to test repositories that contain tools.
repositories_with_tools)
test_repositories_with_tools
;;
# No received parameters or any received parameter not in [ tool_dependency_definitions, repositories_with_tools ]
# will execute both scripts.
*)
test_tool_dependency_definitions
test_repositories_with_tools
;;
esac
+54 -3
View File
@@ -6,7 +6,19 @@
-->
<plugin id="local" type="runner" load="galaxy.jobs.runners.local:LocalJobRunner"/>
<plugin id="pbs" type="runner" load="galaxy.jobs.runners.pbs:PBSJobRunner" workers="2"/>
<plugin id="drmaa" type="runner" load="galaxy.jobs.runners.drmaa:DRMAAJobRunner"/>
<plugin id="drmaa" type="runner" load="galaxy.jobs.runners.drmaa:DRMAAJobRunner">
<!-- Different DRMs handle successfully completed jobs differently,
these options can be changed to handle such differences and
are explained in detail on the Galaxy wiki. Defaults are shown -->
<param id="invalidjobexception_state">ok</param>
<param id="invalidjobexception_retries">0</param>
<param id="internalexception_state">ok</param>
<param id="internalexception_retries">0</param>
</plugin>
<plugin id="sge" type="runner" load="galaxy.jobs.runners.drmaa:DRMAAJobRunner">
<!-- Override the $DRMAA_LIBRARY_PATH environment variable -->
<param id="drmaa_library_path">/sge/lib/libdrmaa.so</param>
</plugin>
<plugin id="lwr" type="runner" load="galaxy.jobs.runners.lwr:LwrJobRunner">
<!-- More information on LWR can be found at https://lwr.readthedocs.org -->
<!-- Uncomment following line to use libcurl to perform HTTP calls (defaults to urllib) -->
@@ -14,6 +26,7 @@
</plugin>
<plugin id="cli" type="runner" load="galaxy.jobs.runners.cli:ShellJobRunner" />
<plugin id="condor" type="runner" load="galaxy.jobs.runners.condor:CondorJobRunner" />
<plugin id="slurm" type="runner" load="galaxy.jobs.runners.slurm:SlurmJobRunner" />
</plugins>
<handlers default="handlers">
<!-- Additional job handlers - the id should match the name of a
@@ -21,6 +34,15 @@
-->
<handler id="handler0" tags="handlers"/>
<handler id="handler1" tags="handlers"/>
<!-- Handlers will load all plugins defined in the <plugins> collection
above by default, but can be limited to a subset using <plugin>
tags. This is useful for heterogenous environments where the DRMAA
plugin would need to be loaded more than once with different
configs.
-->
<handler id="sge_handler">
<plugin id="sge"/>
</handler>
<handler id="special_handler0" tags="special_handlers"/>
<handler id="special_handler1" tags="special_handlers"/>
<handler id="trackster_handler"/>
@@ -52,15 +74,44 @@
<param id="private_token">123456789changeme</param>
<!-- Uncomment the following statement to disable file staging (e.g.
if there is a shared file system between Galaxy and the LWR
server). -->
server). Alternatively action can be set to 'copy' - to replace
http transfers with file system copies. -->
<!-- <param id="default_file_action">none</param> -->
<!-- The above option is just the default, the transfer behavior
none|copy|http can be configured on a per path basis via the
following file. See lib/galaxy/jobs/runners/lwr_client/action_mapper.py
for examples of how to configure this file. This is very beta
and nature of file will likely change.
-->
<!-- <param id="file_action_config">file_actions.json</param> -->
<!-- Uncomment following option to disable Galaxy tool dependency
resolution and utilize remote LWR's configuraiton of tool
dependency resolution instead (same options as Galaxy for
dependency resolution are available in LWR).
-->
<!-- <param id="dependency_resolution">remote</params> -->
<!-- Uncomment following option to enable setting metadata on remote
LWR server. The 'use_remote_datatypes' option is available for
determining whether to use remotely configured datatypes or local
ones (both alternatives are a little brittle). -->
<!-- <param id="remote_metadata">true</param> -->
<!-- <param id="use_remote_datatypes">false</param> -->
<!-- If remote LWR server is configured to run jobs as the real user,
uncomment the following line to pass the current Galaxy user
along. -->
<!-- <param id="submit_user">$__user_name__</param> -->
<!-- Various other submission parameters can be passed along to the LWR
whose use will depend on the remote LWR's configured job manager.
For instance:
-->
<!-- <param id="submit_native_specification">-P bignodes -R y -pe threads 8</param> -->
</destination>
<destination id="ssh_torque" runner="cli">
<param id="shell_plugin">SecureShell</param>
<param id="job_plugin">Torque</param>
<param id="shell_username">foo</param>
<param id="shell_hostname">foo.example.org</param>
<param id="Job_Execution_Time">24:00:00</param>
<param id="job_Resource_List">walltime=24:00:00,ncpus=4</param>
</destination>
<destination id="condor" runner="condor">
<!-- With no params, jobs are submitted to the 'vanilla' universe with:
+1 -1
View File
@@ -150,7 +150,7 @@ class AdminActions( object ):
message += ', '.join( names )
return message
def _undelete_quota( self, quota ):
def _undelete_quota( self, quota, params = None):
quotas = util.listify( quota )
names = []
for q in quotas:
+27 -66
View File
@@ -1,16 +1,10 @@
from __future__ import absolute_import
import sys, os, atexit
import sys
import os
from galaxy import config, jobs, util, tools, web
import galaxy.tools.search
import galaxy.tools.data
import tool_shed.galaxy_install
import tool_shed.tool_shed_registry
from galaxy.web import security
from galaxy import config, jobs
import galaxy.model
import galaxy.datatypes.registry
import galaxy.security
from galaxy.objectstore import build_object_store_from_config
import galaxy.quota
from galaxy.tags.tag_handler import GalaxyTagHandler
from galaxy.visualization.genomes import Genomes
@@ -27,7 +21,8 @@ from galaxy.web.base import pluginframework
import logging
log = logging.getLogger( __name__ )
class UniverseApplication( object ):
class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
"""Encapsulates the state of a Universe application"""
def __init__( self, **kwargs ):
print >> sys.stderr, "python path is: " + ", ".join( sys.path )
@@ -38,72 +33,38 @@ class UniverseApplication( object ):
self.config.check()
config.configure_logging( self.config )
self.configure_fluent_log()
# Determine the database url
if self.config.database_connection:
db_url = self.config.database_connection
else:
db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database
# Set up the tool sheds registry
if os.path.isfile( self.config.tool_sheds_config ):
self.tool_shed_registry = tool_shed.tool_shed_registry.Registry( self.config.root, self.config.tool_sheds_config )
else:
self.tool_shed_registry = None
# Initialize database / check for appropriate schema version. # If this
# is a new installation, we'll restrict the tool migration messaging.
from galaxy.model.migrate.check import create_or_verify_database
create_or_verify_database( db_url, kwargs.get( 'global_conf', {} ).get( '__file__', None ), self.config.database_engine_options, app=self )
# Alert the Galaxy admin to tools that have been moved from the distribution to the tool shed.
from tool_shed.galaxy_install.migrate.check import verify_tools
verify_tools( self, db_url, kwargs.get( 'global_conf', {} ).get( '__file__', None ), self.config.database_engine_options )
# Object store manager
self.object_store = build_object_store_from_config(self.config, fsmon=True)
self._configure_tool_shed_registry()
self._configure_object_store( fsmon=True )
# Setup the database engine and ORM
from galaxy.model import mapping
self.model = mapping.init( self.config.file_path,
db_url,
self.config.database_engine_options,
database_query_profiling_proxy = self.config.database_query_profiling_proxy,
object_store = self.object_store,
trace_logger=self.trace_logger,
use_pbkdf2=self.config.get_bool( 'use_pbkdf2', True ) )
config_file = kwargs.get( 'global_conf', {} ).get( '__file__', None )
self._configure_models( check_migrate_databases=True, check_migrate_tools=True, config_file=config_file )
# Manage installed tool shed repositories.
self.installed_repository_manager = tool_shed.galaxy_install.InstalledRepositoryManager( self )
# Create an empty datatypes registry.
self.datatypes_registry = galaxy.datatypes.registry.Registry()
# Load proprietary datatypes defined in datatypes_conf.xml files in all installed tool shed repositories. We
# load proprietary datatypes before datatypes in the distribution because Galaxy's default sniffers include some
# generic sniffers (eg text,xml) which catch anything, so it's impossible for proprietary sniffers to be used.
# However, if there is a conflict (2 datatypes with the same extension) between a proprietary datatype and a datatype
# in the Galaxy distribution, the datatype in the Galaxy distribution will take precedence. If there is a conflict
# between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence.
self.installed_repository_manager.load_proprietary_datatypes()
# Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config.
self.datatypes_registry.load_datatypes( self.config.root, self.config.datatypes_config )
from tool_shed.galaxy_install import installed_repository_manager
self.installed_repository_manager = installed_repository_manager.InstalledRepositoryManager( self )
self._configure_datatypes_registry( self.installed_repository_manager )
galaxy.model.set_datatypes_registry( self.datatypes_registry )
# Security helper
self.security = security.SecurityHelper( id_secret=self.config.id_secret )
self._configure_security()
# Tag handler
self.tag_handler = GalaxyTagHandler()
# Genomes
self.genomes = Genomes( self )
# Data providers registry.
self.data_provider_registry = DataProviderRegistry()
# Initialize tool data tables using the config defined by self.config.tool_data_table_config_path.
self.tool_data_tables = galaxy.tools.data.ToolDataTableManager( tool_data_path=self.config.tool_data_path,
config_filename=self.config.tool_data_table_config_path )
# Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables.
self.tool_data_tables.load_from_config_file( config_filename=self.config.shed_tool_data_table_config,
tool_data_path=self.tool_data_tables.tool_data_path,
from_shed_config=False )
self._configure_tool_data_tables( from_shed_config=False )
# Initialize the job management configuration
self.job_config = jobs.JobConfiguration(self)
# Initialize the tools, making sure the list of tool configs includes the reserved migrated_tools_conf.xml file.
tool_configs = self.config.tool_configs
if self.config.migrated_tools_config not in tool_configs:
tool_configs.append( self.config.migrated_tools_config )
self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self )
# Search support for tools
self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox )
self._configure_toolbox()
# Load Data Manager
self.data_managers = DataManagers( self )
# If enabled, poll respective tool sheds to see if updates are available for any installed tool shed repositories.
@@ -126,9 +87,9 @@ class UniverseApplication( object ):
load_genome_index_tools( self.toolbox )
# visualizations registry: associates resources with visualizations, controls how to render
self.visualizations_registry = None
if self.config.visualizations_plugins_directory:
if self.config.visualization_plugins_directory:
self.visualizations_registry = VisualizationsRegistry( self,
directories_setting=self.config.visualizations_plugins_directory,
directories_setting=self.config.visualization_plugins_directory,
template_cache_dir=self.config.template_cache )
# Load security policy.
self.security_agent = self.model.security_agent
+134 -6
View File
@@ -1,6 +1,8 @@
"""
Universe configuration builder.
"""
# absolute_import needed for tool_shed package.
from __future__ import absolute_import
import sys, os, tempfile, re
import logging, logging.config
@@ -33,15 +35,22 @@ class Configuration( object ):
self.umask = os.umask( 077 ) # get the current umask
os.umask( self.umask ) # can't get w/o set, so set it back
self.gid = os.getgid() # if running under newgrp(1) we'll need to fix the group of data created on the cluster
# Database related configuration
self.database = resolve_path( kwargs.get( "database_file", "database/universe.sqlite" ), self.root )
self.database_connection = kwargs.get( "database_connection", False )
self.database_engine_options = get_database_engine_options( kwargs )
self.database_create_tables = string_as_bool( kwargs.get( "database_create_tables", "True" ) )
self.database_query_profiling_proxy = string_as_bool( kwargs.get( "database_query_profiling_proxy", "False" ) )
# Don't set this to true for production databases, but probably should
# default to True for sqlite databases.
self.database_auto_migrate = string_as_bool( kwargs.get( "database_auto_migrate", "False" ) )
# Install database related configuration (if different).
self.install_database_connection = kwargs.get( "install_database_connection", None )
self.install_database_engine_options = get_database_engine_options( kwargs, model_prefix="install_" )
# Where dataset files are stored
self.file_path = resolve_path( kwargs.get( "file_path", "database/files" ), self.root )
self.new_file_path = resolve_path( kwargs.get( "new_file_path", "database/tmp" ), self.root )
@@ -49,7 +58,7 @@ class Configuration( object ):
self.openid_consumer_cache_path = resolve_path( kwargs.get( "openid_consumer_cache_path", "database/openid_consumer_cache" ), self.root )
self.cookie_path = kwargs.get( "cookie_path", "/" )
self.genome_data_path = kwargs.get( "genome_data_path", "tool-data/genome" )
self.rsync_url = kwargs.get( "rsync_url", "rsync://scofield.bx.psu.edu/indexes" )
self.rsync_url = kwargs.get( "rsync_url", "rsync://datacache.galaxyproject.org/indexes" )
# Galaxy OpenID settings
self.enable_openid = string_as_bool( kwargs.get( 'enable_openid', False ) )
self.openid_config = kwargs.get( 'openid_config_file', 'openid_conf.xml' )
@@ -58,7 +67,7 @@ class Configuration( object ):
self.enable_unique_workflow_defaults = string_as_bool( kwargs.get( 'enable_unique_workflow_defaults', False ) )
self.tool_path = resolve_path( kwargs.get( "tool_path", "tools" ), self.root )
self.tool_data_path = resolve_path( kwargs.get( "tool_data_path", "tool-data" ), os.getcwd() )
self.len_file_path = kwargs.get( "len_file_path", resolve_path(os.path.join(self.tool_data_path, 'shared','ucsc','chrom'), self.root) )
self.len_file_path = resolve_path( kwargs.get( "len_file_path", os.path.join( self.tool_data_path, 'shared','ucsc','chrom') ), self.root )
self.test_conf = resolve_path( kwargs.get( "test_conf", "" ), self.root )
# The value of migrated_tools_config is the file reserved for containing only those tools that have been eliminated from the distribution
# and moved to the tool shed.
@@ -113,7 +122,9 @@ class Configuration( object ):
self.id_secret = kwargs.get( "id_secret", "USING THE DEFAULT IS NOT SECURE!" )
self.retry_metadata_internally = string_as_bool( kwargs.get( "retry_metadata_internally", "True" ) )
self.use_remote_user = string_as_bool( kwargs.get( "use_remote_user", "False" ) )
self.normalize_remote_user_email = string_as_bool( kwargs.get( "normalize_remote_user_email", "False" ) )
self.remote_user_maildomain = kwargs.get( "remote_user_maildomain", None )
self.remote_user_header = kwargs.get( "remote_user_header", 'HTTP_REMOTE_USER' )
self.remote_user_logout_href = kwargs.get( "remote_user_logout_href", None )
self.require_login = string_as_bool( kwargs.get( "require_login", "False" ) )
self.allow_user_creation = string_as_bool( kwargs.get( "allow_user_creation", "True" ) )
@@ -141,6 +152,7 @@ class Configuration( object ):
h, m, s = [ int( v ) for v in self.job_walltime.split( ':' ) ]
self.job_walltime_delta = timedelta( 0, s, 0, 0, m, h )
self.admin_users = kwargs.get( "admin_users", "" )
self.reset_password_length = int( kwargs.get('reset_password_length', '15') )
self.mailing_join_addr = kwargs.get('mailing_join_addr',"galaxy-announce-join@bx.psu.edu")
self.error_email_to = kwargs.get( 'error_email_to', None )
self.activation_email = kwargs.get( 'activation_email', None )
@@ -303,7 +315,7 @@ class Configuration( object ):
self.datatypes_config = kwargs.get( 'datatypes_config_file', 'datatypes_conf.xml' )
# Cloud configuration options
self.enable_cloud_launch = string_as_bool( kwargs.get( 'enable_cloud_launch', False ) )
self.cloudlaunch_default_ami = kwargs.get( 'cloudlaunch_default_ami', 'ami-118bfc78' )
self.cloudlaunch_default_ami = kwargs.get( 'cloudlaunch_default_ami', 'ami-a7dbf6ce' )
# Galaxy messaging (AMQP) configuration options
self.amqp = {}
try:
@@ -316,6 +328,10 @@ class Configuration( object ):
self.biostar_key_name = kwargs.get( 'biostar_key_name', None )
self.biostar_key = kwargs.get( 'biostar_key', None )
self.pretty_datetime_format = expand_pretty_datetime_format( kwargs.get( 'pretty_datetime_format', '$locale (UTC)' ) )
self.master_api_key = kwargs.get( 'master_api_key', None )
if self.master_api_key == "changethis": # default in sample config file
raise Exception("Insecure configuration, please change master_api_key to something other than default (changethis)")
# Experimental: This will not be enabled by default and will hide
# nonproduction code.
# The api_folders refers to whether the API exposes the /folders section.
@@ -329,7 +345,7 @@ class Configuration( object ):
self.fluent_host = kwargs.get( 'fluent_host', 'localhost' )
self.fluent_port = int( kwargs.get( 'fluent_port', 24224 ) )
# visualization plugin framework
self.visualizations_plugins_directory = kwargs.get( 'visualizations_plugins_directory', None )
self.visualization_plugins_directory = kwargs.get( 'visualization_plugins_directory', None )
@property
def sentry_dsn_public( self ):
@@ -435,7 +451,7 @@ class Configuration( object ):
admin_users = [ x.strip() for x in self.get( "admin_users", "" ).split( "," ) ]
return ( user is not None and user.email in admin_users )
def get_database_engine_options( kwargs ):
def get_database_engine_options( kwargs, model_prefix='' ):
"""
Allow options for the SQLAlchemy database engine to be passed by using
the prefix "database_engine_option".
@@ -451,7 +467,7 @@ def get_database_engine_options( kwargs ):
'pool_threadlocal': string_as_bool,
'server_side_cursors': string_as_bool
}
prefix = "database_engine_option_"
prefix = "%sdatabase_engine_option_" % model_prefix
prefix_len = len( prefix )
rval = {}
for key, value in kwargs.iteritems():
@@ -462,6 +478,7 @@ def get_database_engine_options( kwargs ):
rval[ key ] = value
return rval
def configure_logging( config ):
"""
Allow some basic logging configuration to be read from ini file.
@@ -502,3 +519,114 @@ def configure_logging( config ):
sentry_handler.setLevel( logging.WARN )
root.addHandler( sentry_handler )
class ConfiguresGalaxyMixin:
""" Shared code for configuring Galaxy-like app objects.
"""
def _configure_toolbox( self ):
# Initialize the tools, making sure the list of tool configs includes the reserved migrated_tools_conf.xml file.
tool_configs = self.config.tool_configs
if self.config.migrated_tools_config not in tool_configs:
tool_configs.append( self.config.migrated_tools_config )
from galaxy import tools
self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self )
# Search support for tools
import galaxy.tools.search
self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox )
def _configure_tool_data_tables( self, from_shed_config ):
from galaxy.tools.data import ToolDataTableManager
# Initialize tool data tables using the config defined by self.config.tool_data_table_config_path.
self.tool_data_tables = ToolDataTableManager( tool_data_path=self.config.tool_data_path,
config_filename=self.config.tool_data_table_config_path )
# Load additional entries defined by self.config.shed_tool_data_table_config into tool data tables.
self.tool_data_tables.load_from_config_file( config_filename=self.config.shed_tool_data_table_config,
tool_data_path=self.tool_data_tables.tool_data_path,
from_shed_config=from_shed_config )
def _configure_datatypes_registry( self, installed_repository_manager=None ):
from galaxy.datatypes import registry
# Create an empty datatypes registry.
self.datatypes_registry = registry.Registry()
if installed_repository_manager:
# Load proprietary datatypes defined in datatypes_conf.xml files in all installed tool shed repositories. We
# load proprietary datatypes before datatypes in the distribution because Galaxy's default sniffers include some
# generic sniffers (eg text,xml) which catch anything, so it's impossible for proprietary sniffers to be used.
# However, if there is a conflict (2 datatypes with the same extension) between a proprietary datatype and a datatype
# in the Galaxy distribution, the datatype in the Galaxy distribution will take precedence. If there is a conflict
# between 2 proprietary datatypes, the datatype from the repository that was installed earliest will take precedence.
installed_repository_manager.load_proprietary_datatypes()
# Load the data types in the Galaxy distribution, which are defined in self.config.datatypes_config.
self.datatypes_registry.load_datatypes( self.config.root, self.config.datatypes_config )
def _configure_object_store( self, **kwds ):
from galaxy.objectstore import build_object_store_from_config
self.object_store = build_object_store_from_config( self.config, **kwds )
def _configure_security( self ):
from galaxy.web import security
self.security = security.SecurityHelper( id_secret=self.config.id_secret )
def _configure_tool_shed_registry( self ):
import tool_shed.tool_shed_registry
# Set up the tool sheds registry
if os.path.isfile( self.config.tool_sheds_config ):
self.tool_shed_registry = tool_shed.tool_shed_registry.Registry( self.config.root, self.config.tool_sheds_config )
else:
self.tool_shed_registry = None
def _configure_models( self, check_migrate_databases=False, check_migrate_tools=False, config_file=None ):
"""
Preconditions: object_store must be set on self.
"""
if self.config.database_connection:
db_url = self.config.database_connection
else:
db_url = "sqlite:///%s?isolation_level=IMMEDIATE" % self.config.database
install_db_url = self.config.install_database_connection
# TODO: Consider more aggressive check here that this is not the same
# database file under the hood.
combined_install_database = not( install_db_url and install_db_url != db_url )
install_db_url = install_db_url or db_url
if check_migrate_databases:
# Initialize database / check for appropriate schema version. # If this
# is a new installation, we'll restrict the tool migration messaging.
from galaxy.model.migrate.check import create_or_verify_database
create_or_verify_database( db_url, config_file, self.config.database_engine_options, app=self )
if not combined_install_database:
from galaxy.model.tool_shed_install.migrate.check import create_or_verify_database as tsi_create_or_verify_database
tsi_create_or_verify_database( install_db_url, self.config.install_database_engine_options, app=self )
if check_migrate_tools:
# Alert the Galaxy admin to tools that have been moved from the distribution to the tool shed.
from tool_shed.galaxy_install.migrate.check import verify_tools
if combined_install_database:
install_database_options = self.config.database_engine_options
else:
install_database_options = self.config.install_database_engine_options
verify_tools( self, install_db_url, config_file, install_database_options )
from galaxy.model import mapping
self.model = mapping.init( self.config.file_path,
db_url,
self.config.database_engine_options,
map_install_models=combined_install_database,
database_query_profiling_proxy=self.config.database_query_profiling_proxy,
object_store=self.object_store,
trace_logger=getattr(self, "trace_logger", None),
use_pbkdf2=self.config.get_bool( 'use_pbkdf2', True ) )
if combined_install_database:
log.info("Install database targetting Galaxy's database configuration.")
self.install_model = self.model
else:
from galaxy.model.tool_shed_install import mapping as install_mapping
install_db_url = self.config.install_database_connection
log.info("Install database using its own connection %s" % install_db_url)
install_db_engine_options = self.config.install_database_engine_options
self.install_model = install_mapping.init( install_db_url,
install_db_engine_options )
+6 -2
View File
@@ -2,6 +2,8 @@ import os, gzip, re, gzip, zipfile, binascii, bz2, imghdr
from galaxy import util
from StringIO import StringIO
HTML_CHECK_LINES = 100
try:
import Image as PIL
except ImportError:
@@ -32,9 +34,11 @@ def check_html( file_path, chunk=None ):
regexp1 = re.compile( "<A\s+[^>]*HREF[^>]+>", re.I )
regexp2 = re.compile( "<IFRAME[^>]*>", re.I )
regexp3 = re.compile( "<FRAMESET[^>]*>", re.I )
regexp4 = re.compile( "<META[^>]*>", re.I )
regexp4 = re.compile( "<META[\W][^>]*>", re.I )
regexp5 = re.compile( "<SCRIPT[^>]*>", re.I )
lineno = 0
# TODO: Potentially reading huge lines into string here, this should be
# reworked.
for line in temp:
lineno += 1
matches = regexp1.search( line ) or regexp2.search( line ) or regexp3.search( line ) or regexp4.search( line ) or regexp5.search( line )
@@ -42,7 +46,7 @@ def check_html( file_path, chunk=None ):
if chunk is None:
temp.close()
return True
if lineno > 100:
if HTML_CHECK_LINES and (lineno > HTML_CHECK_LINES):
break
if chunk is None:
temp.close()
@@ -1,4 +1,7 @@
<tool id="CONVERTER_Bam_Bai_0" name="Bam to Bai" hidden="true">
<requirements>
<requirement type="package">samtools</requirement>
</requirements>
<command>samtools index $input1 $output1</command>
<inputs>
<page>
@@ -10,4 +13,4 @@
</outputs>
<help>
</help>
</tool>
</tool>
@@ -1,5 +1,8 @@
<tool id="CONVERTER_bedgraph_to_bigwig" name="Convert BedGraph to BigWig" hidden="true">
<!-- Used internally to generate track indexes -->
<requirements>
<requirement type="package">ucsc_tools</requirement>
</requirements>
<command>grep -v "^track" $input | wigToBigWig -clip stdin $chromInfo $output</command>
<inputs>
<param format="bedgraph" name="input" type="data" label="Choose wiggle"/>
@@ -9,4 +12,4 @@
</outputs>
<help>
</help>
</tool>
</tool>
@@ -1,6 +1,9 @@
<tool id="CONVERTER_fasta_to_2bit" name="Convert FASTA to 2bit" version="1.0.0">
<!-- <description>__NOT_USED_CURRENTLY_FOR_CONVERTERS__</description> -->
<!-- Used on the metadata edit page. -->
<requirements>
<requirement type="package">ucsc_tools</requirement>
</requirements>
<command>faToTwoBit $input $output</command>
<inputs>
<param name="input" type="data" format="fasta" label="Fasta file"/>
@@ -2,6 +2,9 @@
<requirements><requirement type='package'>bowtie</requirement></requirements>
<!-- <description>__NOT_USED_CURRENTLY_FOR_CONVERTERS__</description> -->
<!-- Used on the metadata edit page. -->
<requirements>
<requirement type="package">bowtie</requirement>
</requirements>
<command>
mkdir ${output.files_path}
&amp;&amp; bowtie-build --quiet
@@ -2,6 +2,9 @@
<requirements><requirement type='package'>bowtie</requirement></requirements>
<!-- <description>__NOT_USED_CURRENTLY_FOR_CONVERTERS__</description> -->
<!-- Used on the metadata edit page. -->
<requirements>
<requirement type="package">bowtie</requirement>
</requirements>
<command>
mkdir ${output.files_path}
&amp;&amp; bowtie-build --quiet
@@ -5,6 +5,9 @@
To fix this: (a) merge sam_to_bam tool in tools with this conversion (like fasta_to_len
conversion); and (b) define a datatype-specific way to set converter parameters.
-->
<requirements>
<requirement type="package">samtools</requirement>
</requirements>
<command interpreter="python">sam_to_bam.py $input1 $output</command>
<inputs>
<param name="input1" type="data" format="sam" label="SAM file"/>
@@ -1,5 +1,8 @@
<tool id="CONVERTER_wig_to_bigwig" name="Convert Wiggle to BigWig" hidden="true">
<!-- Used internally to generate track indexes -->
<requirements>
<requirement type="package">ucsc_tools</requirement>
</requirements>
<command>grep -v "^track" $input | wigToBigWig -clip stdin $chromInfo $output</command>
<inputs>
<page>
@@ -11,4 +14,4 @@
</outputs>
<help>
</help>
</tool>
</tool>
+51 -25
View File
@@ -199,6 +199,26 @@ class Data( object ):
out = "Can't create peek %s" % str( exc )
return out
def _archive_main_file(self, archive, display_name, data_filename):
"""Called from _archive_composite_dataset to add central file to archive.
Unless subclassed, this will add the main dataset file (argument data_filename)
to the archive, as an HTML file with its filename derived from the dataset name
(argument outfname).
Returns a tuple of boolean, string, string: (error, msg, messagetype)
"""
error, msg, messagetype = False, "", ""
archname = '%s.html' % display_name # fake the real nature of the html file
try:
archive.add(data_filename, archname)
except IOError:
error = True
log.exception("Unable to add composite parent %s to temporary library download archive" % data_filename)
msg = "Unable to create archive for download, please report this error"
messagetype = "error"
return error, msg, messagetype
def _archive_composite_dataset( self, trans, data=None, **kwd ):
# save a composite object into a compressed archive for downloading
params = util.Params( kwd )
@@ -237,29 +257,27 @@ class Data( object ):
path = data.file_name
fname = os.path.split(path)[-1]
efp = data.extra_files_path
htmlname = os.path.splitext(outfname)[0]
if not htmlname.endswith(ext):
htmlname = '%s_%s' % (htmlname,ext)
archname = '%s.html' % htmlname # fake the real nature of the html file
try:
archive.add(data.file_name,archname)
except IOError:
error = True
log.exception( "Unable to add composite parent %s to temporary library download archive" % data.file_name)
msg = "Unable to create archive for download, please report this error"
messagetype = 'error'
for root, dirs, files in os.walk(efp):
for fname in files:
fpath = os.path.join(root,fname)
rpath = os.path.relpath(fpath,efp)
try:
archive.add( fpath,rpath )
except IOError:
error = True
log.exception( "Unable to add %s to temporary library download archive" % rpath)
msg = "Unable to create archive for download, please report this error"
messagetype = 'error'
continue
#Add any central file to the archive,
display_name = os.path.splitext(outfname)[0]
if not display_name.endswith(ext):
display_name = '%s_%s' % (display_name, ext)
error, msg, messagetype = self._archive_main_file(archive, display_name, path)
if not error:
#Add any child files to the archive,
for root, dirs, files in os.walk(efp):
for fname in files:
fpath = os.path.join(root,fname)
rpath = os.path.relpath(fpath,efp)
try:
archive.add( fpath,rpath )
except IOError:
error = True
log.exception( "Unable to add %s to temporary library download archive" % rpath)
msg = "Unable to create archive for download, please report this error"
messagetype = 'error'
continue
if not error:
if params.do_action == 'zip':
archive.close()
@@ -288,7 +306,14 @@ class Data( object ):
return open( dataset.file_name )
def display_data(self, trans, data, preview=False, filename=None, to_ext=None, size=None, offset=None, **kwd):
""" Old display method, for transition """
""" Old display method, for transition - though still used by API and
test framework. Datatypes should be very careful if overridding this
method and this interface between datatypes and Galaxy will likely
change.
TOOD: Document alternatives to overridding this method (data
providers?).
"""
#Relocate all composite datatype display to a common location.
composite_extensions = trans.app.datatypes_registry.get_composite_extensions( )
composite_extensions.append('html') # for archiving composite datatypes
@@ -487,13 +512,14 @@ class Data( object ):
def before_setting_metadata( self, dataset ):
"""This function is called on the dataset before metadata is set."""
dataset.clear_associated_files( metadata_safe = True )
def __new_composite_file( self, name, optional = False, mimetype = None, description = None, substitute_name_with_metadata = None, is_binary = False, space_to_tab = False, **kwds ):
def __new_composite_file( self, name, optional = False, mimetype = None, description = None, substitute_name_with_metadata = None, is_binary = False, to_posix_lines = True, space_to_tab = False, **kwds ):
kwds[ 'name' ] = name
kwds[ 'optional' ] = optional
kwds[ 'mimetype' ] = mimetype
kwds[ 'description' ] = description
kwds[ 'substitute_name_with_metadata' ] = substitute_name_with_metadata
kwds[ 'is_binary' ] = is_binary
kwds[ 'to_posix_lines' ] = to_posix_lines
kwds[ 'space_to_tab' ] = space_to_tab
return Bunch( **kwds )
def add_composite_file( self, name, **kwds ):
@@ -18,6 +18,7 @@ DataProvider related decorators.
from functools import wraps
#from types import MethodType
import urllib2
import copy
import logging
@@ -143,8 +144,9 @@ def _parse_query_string_settings( query_kwargs, settings=None ):
'int' : int,
'float' : float,
'bool' : bool,
'list:str' : lambda s: list_from_query_string( s ),
'list:int' : lambda s: [ int( i ) for i in list_from_query_string( s ) ],
'list:str' : lambda s: list_from_query_string( s ),
'list:escaped' : lambda s: [ urllib2.unquote( e ) for e in list_from_query_string( s ) ],
'list:int' : lambda s: [ int( i ) for i in list_from_query_string( s ) ],
}
settings = settings or {}
# yay! yet another set of query string parsers! <-- sarcasm
+2 -2
View File
@@ -78,7 +78,7 @@ class FilteredLineDataProvider( base.LimitedOffsetDataProvider ):
line = line.strip( '\n' )
if not self.provide_blank and line == '':
return None
elif line.startswith( self.comment_char ):
elif self.comment_char and line.startswith( self.comment_char ):
return None
return super( FilteredLineDataProvider, self ).filter( line )
@@ -94,7 +94,7 @@ class RegexLineDataProvider( FilteredLineDataProvider ):
the line it is considered valid and will be provided).
"""
settings = {
'regex_list' : 'list:str',
'regex_list' : 'list:escaped',
'invert' : 'bool',
}
+1 -5
View File
@@ -213,7 +213,6 @@ Binary.register_sniffable_binary_format("pdf", "pdf", Pdf)
def create_applet_tag_peek( class_name, archive, params ):
text = """
<!--[if !IE]>-->
<object classid="java:%s"
type="application/x-java-applet"
height="30" width="200" align="center" >
@@ -221,17 +220,14 @@ def create_applet_tag_peek( class_name, archive, params ):
for name, value in params.iteritems():
text += """<param name="%s" value="%s"/>""" % ( name, value )
text += """
<!--<![endif]-->
<object classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
height="30" width="200" >
<param name="code" value="%s" />
<param name="archive" value="%s"/>""" % ( class_name, archive )
for name, value in params.iteritems():
text += """<param name="%s" value="%s"/>""" % ( name, value )
text += """</object>
<!--[if !IE]>-->
text += """<div class="errormessage">You must install and enable Java in your browser in order to access this applet.<div></object>
</object>
<!--<![endif]-->
"""
return """<div><p align="center">%s</p></div>""" % text
+1 -1
View File
@@ -389,7 +389,7 @@ class Bed( Interval ):
MetadataElement( name="endCol", default=3, desc="End column", param=metadata.ColumnParameter )
MetadataElement( name="strandCol", desc="Strand column (click box & select)", param=metadata.ColumnParameter, optional=True, no_value=0 )
MetadataElement( name="columns", default=3, desc="Number of columns", readonly=True, visible=False )
MetadataElement( name="viz_filter_cols", desc="Score column for visualization", default=[4], param=metadata.ColumnParameter, multiple=True )
MetadataElement( name="viz_filter_cols", desc="Score column for visualization", default=[4], param=metadata.ColumnParameter, optional=True, multiple=True )
###do we need to repeat these? they are the same as should be inherited from interval type
def set_meta( self, dataset, overwrite = True, **kwd ):
+9 -11
View File
@@ -2,15 +2,13 @@
Galaxy Metadata
"""
from galaxy import eggs
eggs.require("simplejson")
import copy
import cPickle
import json
import logging
import os
import shutil
import simplejson
import sys
import tempfile
import weakref
@@ -130,7 +128,7 @@ class MetadataCollection( object ):
def from_JSON_dict( self, filename ):
dataset = self.parent
log.debug( 'loading metadata from file for: %s %s' % ( dataset.__class__.__name__, dataset.id ) )
JSONified_dict = simplejson.load( open( filename ) )
JSONified_dict = json.load( open( filename ) )
for name, spec in self.spec.items():
if name in JSONified_dict:
dataset._metadata[ name ] = spec.param.from_external_value( JSONified_dict[ name ], dataset )
@@ -146,7 +144,7 @@ class MetadataCollection( object ):
for name, spec in self.spec.items():
if name in dataset_meta_dict:
meta_dict[ name ] = spec.param.to_external_value( dataset_meta_dict[ name ] )
simplejson.dump( meta_dict, open( filename, 'wb+' ) )
json.dump( meta_dict, open( filename, 'wb+' ) )
def __getstate__( self ):
return None #cannot pickle a weakref item (self._parent), when data._metadata_collection is None, it will be recreated on demand
@@ -456,7 +454,7 @@ class ListParameter( MetadataParameter ):
class DictParameter( MetadataParameter ):
def to_string( self, value ):
return simplejson.dumps( value )
return json.dumps( value )
class PythonObjectParameter( MetadataParameter ):
@@ -594,7 +592,7 @@ class MetadataTempFile( object ):
@classmethod
def cleanup_from_JSON_dict_filename( cls, filename ):
try:
for key, value in simplejson.load( open( filename ) ).items():
for key, value in json.load( open( filename ) ).items():
if cls.is_JSONified_value( value ):
value = cls.from_JSON( value )
if isinstance( value, cls ) and os.path.exists( value.file_name ):
@@ -686,10 +684,10 @@ class JobExternalOutputMetadataWrapper( object ):
#file to store a 'return code' indicating the results of the set_meta() call
#results code is like (True/False - if setting metadata was successful/failed , exception or string of reason of success/failure )
metadata_files.filename_results_code = abspath( tempfile.NamedTemporaryFile( dir = tmp_dir, prefix = "metadata_results_%s_" % key ).name )
simplejson.dump( ( False, 'External set_meta() not called' ), open( metadata_files.filename_results_code, 'wb+' ) ) # create the file on disk, so it cannot be reused by tempfile (unlikely, but possible)
json.dump( ( False, 'External set_meta() not called' ), open( metadata_files.filename_results_code, 'wb+' ) ) # create the file on disk, so it cannot be reused by tempfile (unlikely, but possible)
#file to store kwds passed to set_meta()
metadata_files.filename_kwds = abspath( tempfile.NamedTemporaryFile( dir = tmp_dir, prefix = "metadata_kwds_%s_" % key ).name )
simplejson.dump( kwds, open( metadata_files.filename_kwds, 'wb+' ), ensure_ascii=True )
json.dump( kwds, open( metadata_files.filename_kwds, 'wb+' ), ensure_ascii=True )
#existing metadata file parameters need to be overridden with cluster-writable file locations
metadata_files.filename_override_metadata = abspath( tempfile.NamedTemporaryFile( dir = tmp_dir, prefix = "metadata_override_%s_" % key ).name )
open( metadata_files.filename_override_metadata, 'wb+' ) # create the file on disk, so it cannot be reused by tempfile (unlikely, but possible)
@@ -699,7 +697,7 @@ class JobExternalOutputMetadataWrapper( object ):
metadata_temp = MetadataTempFile()
shutil.copy( dataset.metadata.get( meta_key, None ).file_name, metadata_temp.file_name )
override_metadata.append( ( meta_key, metadata_temp.to_JSON() ) )
simplejson.dump( override_metadata, open( metadata_files.filename_override_metadata, 'wb+' ) )
json.dump( override_metadata, open( metadata_files.filename_override_metadata, 'wb+' ) )
#add to session and flush
sa_session.add( metadata_files )
sa_session.flush()
@@ -711,7 +709,7 @@ class JobExternalOutputMetadataWrapper( object ):
metadata_files = self.get_output_filenames_by_dataset( dataset, sa_session )
if not metadata_files:
return False # this file doesn't exist
rval, rstring = simplejson.load( open( metadata_files.filename_results_code ) )
rval, rstring = json.load( open( metadata_files.filename_results_code ) )
if not rval:
log.debug( 'setting metadata externally failed for %s %s: %s' % ( dataset.__class__.__name__, dataset.id, rstring ) )
return rval
+443 -321
View File
@@ -1,16 +1,38 @@
"""
Provides mapping between extensions and datatypes, mime-types, etc.
"""
import os, sys, tempfile, threading, logging, imp
import data, tabular, interval, images, sequence, qualityscore, genetics, xml, coverage, tracks, chrominfo, binary, assembly, ngsindex, graph
import os
import sys
import tempfile
import threading
import logging
import imp
import data
import tabular
import interval
import images
import sequence
import qualityscore
import genetics
import xml
import coverage
import tracks
import chrominfo
import binary
import assembly
import ngsindex
import graph
import galaxy.util
from galaxy.util.odict import odict
from display_applications.application import DisplayApplication
class ConfigurationError( Exception ):
pass
class Registry( object ):
def __init__( self ):
self.log = logging.getLogger(__name__)
self.log.addHandler( logging.NullHandler() )
@@ -47,19 +69,29 @@ class Registry( object ):
self.datatype_elems = []
self.sniffer_elems = []
self.xml_filename = None
def load_datatypes( self, root_dir=None, config=None, deactivate=False, override=True ):
"""
Parse a datatypes XML file located at root_dir/config. If deactivate is True, an installed tool shed
repository that includes proprietary datatypes is being deactivated, so appropriate loaded datatypes
will be removed from the registry. The value of override will be False when a tool shed repository is
being installed. Since installation is occurring after the datatypes registry has been initialized, its
contents cannot be overridden by new introduced conflicting data types.
Parse a datatypes XML file located at root_dir/config (if processing the Galaxy distributed config) or contained within
an installed Tool Shed repository. If deactivate is True, an installed Tool Shed repository that includes custom datatypes
is being deactivated or uninstalled, so appropriate loaded datatypes will be removed from the registry. The value of
override will be False when a Tool Shed repository is being installed. Since installation is occurring after the datatypes
registry has been initialized at server startup, it's contents cannot be overridden by newly introduced conflicting data types.
"""
def __import_module( full_path, datatype_module, datatype_class_name ):
open_file_obj, file_name, description = imp.find_module( datatype_module, [ full_path ] )
imported_module = imp.load_module( datatype_class_name, open_file_obj, file_name, description )
return imported_module
if root_dir and config:
# If handling_proprietary_datatypes is determined as True below, we'll have an elem that looks something like this:
# <datatype display_in_upload="true"
# extension="blastxml"
# mimetype="application/xml"
# proprietary_datatype_module="blast"
# proprietary_path="[cloned repository path]"
# type="galaxy.datatypes.blast:BlastXml" />
handling_proprietary_datatypes = False
# Parse datatypes_conf.xml
tree = galaxy.util.parse_xml( config )
@@ -82,308 +114,281 @@ class Registry( object ):
# Proprietary datatype's <registration> tag may have special attributes, proprietary_converter_path and proprietary_display_path.
proprietary_converter_path = registration.get( 'proprietary_converter_path', None )
proprietary_display_path = registration.get( 'proprietary_display_path', None )
if proprietary_converter_path or proprietary_display_path and not handling_proprietary_datatypes:
if proprietary_converter_path is not None or proprietary_display_path is not None and not handling_proprietary_datatypes:
handling_proprietary_datatypes = True
for elem in registration.findall( 'datatype' ):
try:
extension = elem.get( 'extension', None )
dtype = elem.get( 'type', None )
type_extension = elem.get( 'type_extension', None )
mimetype = elem.get( 'mimetype', None )
display_in_upload = galaxy.util.string_as_bool( elem.get( 'display_in_upload', False ) )
make_subclass = galaxy.util.string_as_bool( elem.get( 'subclass', False ) )
# Proprietary datatypes included in installed tool shed repositories will include two special attributes
# (proprietary_path and proprietary_datatype_module) if they depend on proprietary datatypes classes.
proprietary_path = elem.get( 'proprietary_path', None )
proprietary_datatype_module = elem.get( 'proprietary_datatype_module', None )
if proprietary_path or proprietary_datatype_module and not handling_proprietary_datatypes:
handling_proprietary_datatypes = True
if deactivate:
# We are deactivating an installed tool shed repository, so eliminate the
# datatype elem from the in-memory list of datatype elems.
for in_memory_elem in self.datatype_elems:
in_memory_extension = in_memory_elem.get( 'extension', None )
if in_memory_extension == extension:
in_memory_dtype = elem.get( 'type', None )
in_memory_type_extension = elem.get( 'type_extension', None )
in_memory_mimetype = elem.get( 'mimetype', None )
in_memory_display_in_upload = galaxy.util.string_as_bool( elem.get( 'display_in_upload', False ) )
in_memory_make_subclass = galaxy.util.string_as_bool( elem.get( 'subclass', False ) )
if in_memory_dtype == dtype and in_memory_type_extension == type_extension and in_memory_mimetype == mimetype \
and in_memory_display_in_upload == display_in_upload and in_memory_make_subclass == make_subclass:
self.datatype_elems.remove( in_memory_elem )
else:
# Keep an in-memory list of datatype elems to enable persistence.
if extension not in self.datatypes_by_extension:
self.datatype_elems.append( elem )
if extension and extension in self.datatypes_by_extension and deactivate:
# We are deactivating an installed tool shed repository, so eliminate the datatype from the registry.
# TODO: Handle deactivating datatype converters, etc before removing from self.datatypes_by_extension.
self.log.debug( "Removing datatype with extension '%s' from the registry." % extension )
# Keep a status of the process steps to enable stopping the process of handling the datatype if necessary.
ok = True
extension = elem.get( 'extension', None )
dtype = elem.get( 'type', None )
type_extension = elem.get( 'type_extension', None )
mimetype = elem.get( 'mimetype', None )
display_in_upload = galaxy.util.string_as_bool( elem.get( 'display_in_upload', False ) )
# If make_subclass is True, it does not necessarily imply that we are subclassing a datatype that is contained
# in the distribution.
make_subclass = galaxy.util.string_as_bool( elem.get( 'subclass', False ) )
# Proprietary datatypes included in installed tool shed repositories will include two special attributes
# (proprietary_path and proprietary_datatype_module) if they depend on proprietary datatypes classes.
# The value of proprietary_path is the path to the cloned location of the tool shed repository's contained
# datatypes_conf.xml file.
proprietary_path = elem.get( 'proprietary_path', None )
proprietary_datatype_module = elem.get( 'proprietary_datatype_module', None )
if proprietary_path is not None or proprietary_datatype_module is not None and not handling_proprietary_datatypes:
handling_proprietary_datatypes = True
if deactivate:
# We are deactivating or uninstalling an installed tool shed repository, so eliminate the datatype
# elem from the in-memory list of datatype elems.
for in_memory_elem in self.datatype_elems:
in_memory_extension = in_memory_elem.get( 'extension', None )
if in_memory_extension == extension:
in_memory_dtype = elem.get( 'type', None )
in_memory_type_extension = elem.get( 'type_extension', None )
in_memory_mimetype = elem.get( 'mimetype', None )
in_memory_display_in_upload = galaxy.util.string_as_bool( elem.get( 'display_in_upload', False ) )
in_memory_make_subclass = galaxy.util.string_as_bool( elem.get( 'subclass', False ) )
if in_memory_dtype == dtype and \
in_memory_type_extension == type_extension and \
in_memory_mimetype == mimetype and \
in_memory_display_in_upload == display_in_upload and \
in_memory_make_subclass == make_subclass:
self.datatype_elems.remove( in_memory_elem )
if extension is not None and extension in self.datatypes_by_extension:
# We are deactivating or uninstalling an installed tool shed repository, so eliminate the datatype
# from the registry. TODO: Handle deactivating datatype converters, etc before removing from
# self.datatypes_by_extension.
del self.datatypes_by_extension[ extension ]
if extension in self.upload_file_formats:
self.upload_file_formats.remove( extension )
can_process_datatype = False
else:
can_process_datatype = ( extension and ( dtype or type_extension ) ) and ( extension not in self.datatypes_by_extension or override )
self.log.debug( "Removed datatype with extension '%s' from the registry." % extension )
else:
# We are loading new datatype, so we'll make sure it is correctly defined before proceeding.
can_process_datatype = False
if extension is not None:
if dtype is not None or type_extension is not None:
if override or extension not in self.datatypes_by_extension:
can_process_datatype = True
if can_process_datatype:
if dtype:
fields = dtype.split( ':' )
datatype_module = fields[0]
datatype_class_name = fields[1]
datatype_class = None
if proprietary_path and proprietary_datatype_module and datatype_class_name:
# We need to change the value of sys.path, so do it in a way that is thread-safe.
lock = threading.Lock()
lock.acquire( True )
try:
imported_module = __import_module( proprietary_path, proprietary_datatype_module, datatype_class_name )
if imported_module not in self.imported_modules:
self.imported_modules.append( imported_module )
if hasattr( imported_module, datatype_class_name ):
datatype_class = getattr( imported_module, datatype_class_name )
except Exception, e:
full_path = os.path.join( proprietary_path, proprietary_datatype_module )
self.log.debug( "Exception importing proprietary code file %s: %s" % ( str( full_path ), str( e ) ) )
finally:
lock.release()
if datatype_class is None:
# The datatype class name must be contained in one of the datatype modules in the Galaxy distribution.
fields = datatype_module.split( '.' )
module = __import__( fields.pop(0) )
for mod in fields:
module = getattr( module, mod )
datatype_class = getattr( module, datatype_class_name )
elif type_extension:
datatype_class = self.datatypes_by_extension[type_extension].__class__
if make_subclass:
datatype_class = type( datatype_class_name, (datatype_class,), {} )
if extension in self.datatypes_by_extension:
self.log.warning( "Overriding conflicting datatype with extension '%s', using datatype from %s." % ( extension, config ) )
self.datatypes_by_extension[ extension ] = datatype_class()
if mimetype is None:
# Use default mime type as per datatype spec
mimetype = self.datatypes_by_extension[ extension ].get_mime()
self.mimetypes_by_extension[ extension ] = mimetype
if datatype_class.track_type:
self.available_tracks.append( extension )
if display_in_upload and extension not in self.upload_file_formats:
self.upload_file_formats.append( extension )
# Max file size cut off for setting optional metadata
self.datatypes_by_extension[ extension ].max_optional_metadata_filesize = elem.get( 'max_optional_metadata_filesize', None )
for converter in elem.findall( 'converter' ):
# Build the list of datatype converters which will later be loaded into the calling app's toolbox.
converter_config = converter.get( 'file', None )
target_datatype = converter.get( 'target_datatype', None )
depends_on = converter.get( 'depends_on', None )
if depends_on and target_datatype:
if extension not in self.converter_deps:
self.converter_deps[extension] = {}
self.converter_deps[extension][target_datatype] = depends_on.split(',')
if converter_config and target_datatype:
#if imported_modules:
if proprietary_converter_path:
self.proprietary_converters.append( ( converter_config, extension, target_datatype ) )
else:
self.converters.append( ( converter_config, extension, target_datatype ) )
for composite_file in elem.findall( 'composite_file' ):
# add composite files
name = composite_file.get( 'name', None )
if name is None:
self.log.warning( "You must provide a name for your composite_file (%s)." % composite_file )
optional = composite_file.get( 'optional', False )
mimetype = composite_file.get( 'mimetype', None )
self.datatypes_by_extension[extension].add_composite_file( name, optional=optional, mimetype=mimetype )
for display_app in elem.findall( 'display' ):
#if imported_modules:
if proprietary_display_path:
if elem not in self.proprietary_display_app_containers:
self.proprietary_display_app_containers.append( elem )
else:
if elem not in self.display_app_containers:
self.display_app_containers.append( elem )
elif not deactivate:
# A new tool shed repository that contains proprietary datatypes is being installed, and since installation
# is occurring after the datatypes registry has been initialized, its contents cannot be overridden by new
# introduced conflicting data types.
self.log.warning( "Ignoring conflicting datatype with extension '%s' from %s." % ( extension, config ) )
except Exception, e:
if deactivate:
self.log.warning( "Error deactivating datatype with extension '%s': %s" % ( extension, str( e ) ) )
if dtype is not None:
try:
fields = dtype.split( ':' )
datatype_module = fields[ 0 ]
datatype_class_name = fields[ 1 ]
except Exception, e:
self.log.exception( 'Error parsing datatype definition for dtype %s: %s' % ( str( dtype ), str( e ) ) )
ok = False
if ok:
datatype_class = None
if proprietary_path and proprietary_datatype_module and datatype_class_name:
# We need to change the value of sys.path, so do it in a way that is thread-safe.
lock = threading.Lock()
lock.acquire( True )
try:
imported_module = __import_module( proprietary_path,
proprietary_datatype_module,
datatype_class_name )
if imported_module not in self.imported_modules:
self.imported_modules.append( imported_module )
if hasattr( imported_module, datatype_class_name ):
datatype_class = getattr( imported_module, datatype_class_name )
except Exception, e:
full_path = os.path.join( proprietary_path, proprietary_datatype_module )
self.log.debug( "Exception importing proprietary code file %s: %s" % ( str( full_path ), str( e ) ) )
finally:
lock.release()
# Either the above exception was thrown because the proprietary_datatype_module is not derived from a class
# in the repository, or we are loading Galaxy's datatypes. In either case we'll look in the registry.
if datatype_class is None:
try:
# The datatype class name must be contained in one of the datatype modules in the Galaxy distribution.
fields = datatype_module.split( '.' )
module = __import__( fields.pop( 0 ) )
for mod in fields:
module = getattr( module, mod )
datatype_class = getattr( module, datatype_class_name )
self.log.debug( 'Retrieved datatype module %s from the datatype registry.' % str( datatype_module ) )
except Exception, e:
self.log.exception( 'Error importing datatype module %s: %s' % ( str( datatype_module ), str( e ) ) )
ok = False
elif type_extension is not None:
try:
datatype_class = self.datatypes_by_extension[ type_extension ].__class__
except Exception, e:
self.log.exception( 'Error determining datatype_class for type_extension %s: %s' % ( str( type_extension ), str( e ) ) )
ok = False
if ok:
if not deactivate:
# A new tool shed repository that contains custom datatypes is being installed, and since installation is
# occurring after the datatypes registry has been initialized at server startup, its contents cannot be
# overridden by new introduced conflicting data types unless the value of override is True.
if extension in self.datatypes_by_extension:
# Because of the way that the value of can_process_datatype was set above, we know that the value of
# override is True.
self.log.warning( "Overriding conflicting datatype with extension '%s', using datatype from %s." % \
( str( extension ), str( config ) ) )
if make_subclass:
datatype_class = type( datatype_class_name, ( datatype_class, ), {} )
self.datatypes_by_extension[ extension ] = datatype_class()
if mimetype is None:
# Use default mimetype per datatype specification.
mimetype = self.datatypes_by_extension[ extension ].get_mime()
self.mimetypes_by_extension[ extension ] = mimetype
if datatype_class.track_type:
self.available_tracks.append( extension )
if display_in_upload and extension not in self.upload_file_formats:
self.upload_file_formats.append( extension )
# Max file size cut off for setting optional metadata.
self.datatypes_by_extension[ extension ].max_optional_metadata_filesize = elem.get( 'max_optional_metadata_filesize', None )
for converter in elem.findall( 'converter' ):
# Build the list of datatype converters which will later be loaded into the calling app's toolbox.
converter_config = converter.get( 'file', None )
target_datatype = converter.get( 'target_datatype', None )
depends_on = converter.get( 'depends_on', None )
if depends_on is not None and target_datatype is not None:
if extension not in self.converter_deps:
self.converter_deps[ extension ] = {}
self.converter_deps[ extension ][ target_datatype ] = depends_on.split( ',' )
if converter_config and target_datatype:
if proprietary_converter_path:
self.proprietary_converters.append( ( converter_config, extension, target_datatype ) )
else:
self.converters.append( ( converter_config, extension, target_datatype ) )
# Add composite files.
for composite_file in elem.findall( 'composite_file' ):
name = composite_file.get( 'name', None )
if name is None:
self.log.warning( "You must provide a name for your composite_file (%s)." % composite_file )
optional = composite_file.get( 'optional', False )
mimetype = composite_file.get( 'mimetype', None )
self.datatypes_by_extension[ extension ].add_composite_file( name, optional=optional, mimetype=mimetype )
for display_app in elem.findall( 'display' ):
if proprietary_display_path:
if elem not in self.proprietary_display_app_containers:
self.proprietary_display_app_containers.append( elem )
else:
if elem not in self.display_app_containers:
self.display_app_containers.append( elem )
# Processing the new datatype elem is now complete, so make sure the element defining it is retained by appending
# the new datatype to the in-memory list of datatype elems to enable persistence.
self.datatype_elems.append( elem )
else:
self.log.warning( "Error loading datatype with extension '%s': %s" % ( extension, str( e ) ) )
# Load datatype sniffers from the config
sniffers = root.find( 'sniffers' )
if sniffers:
for elem in sniffers.findall( 'sniffer' ):
# Keep an in-memory list of sniffer elems to enable persistence.
if elem not in self.sniffer_elems:
self.sniffer_elems.append( elem )
dtype = elem.get( 'type', None )
if dtype:
try:
fields = dtype.split( ":" )
datatype_module = fields[0]
datatype_class_name = fields[1]
module = None
#if imported_modules:
if handling_proprietary_datatypes:
# See if one of the imported modules contains the datatype class name.
for imported_module in self.imported_modules:
if hasattr( imported_module, datatype_class_name ):
module = imported_module
break
if module is None:
if extension is not None:
if dtype is not None or type_extension is not None:
if extension in self.datatypes_by_extension:
if not override:
# Do not load the datatype since it conflicts with an existing datatype which we are not supposed
# to override.
self.log.warning( "Ignoring conflicting datatype with extension '%s' from %s." % ( extension, config ) )
# Load datatype sniffers from the config - we'll do this even if one or more datatypes were not properly processed in the config
# since sniffers are not tightly coupled with datatypes.
self.load_datatype_sniffers( root,
deactivate=deactivate,
handling_proprietary_datatypes=handling_proprietary_datatypes,
override=override )
self.upload_file_formats.sort()
# Persist the xml form of the registry into a temporary file so that it can be loaded from the command line by tools and
# set_metadata processing.
self.to_xml_file()
self.set_default_values()
def append_to_sniff_order():
# Just in case any supported data types are not included in the config's sniff_order section.
for ext in self.datatypes_by_extension:
datatype = self.datatypes_by_extension[ ext ]
included = False
for atype in self.sniff_order:
if isinstance( atype, datatype.__class__ ):
included = True
break
if not included:
self.sniff_order.append( datatype )
append_to_sniff_order()
def load_datatype_sniffers( self, root, deactivate=False, handling_proprietary_datatypes=False, override=False ):
"""
Process the sniffers element from a parsed a datatypes XML file located at root_dir/config (if processing the Galaxy
distributed config) or contained within an installed Tool Shed repository. If deactivate is True, an installed Tool
Shed repository that includes custom sniffers is being deactivated or uninstalled, so appropriate loaded sniffers will
be removed from the registry. The value of override will be False when a Tool Shed repository is being installed.
Since installation is occurring after the datatypes registry has been initialized at server startup, it's contents
cannot be overridden by newly introduced conflicting sniffers.
"""
sniffer_elem_classes = [ e.attrib[ 'type' ] for e in self.sniffer_elems ]
sniffers = root.find( 'sniffers' )
if sniffers:
for elem in sniffers.findall( 'sniffer' ):
# Keep a status of the process steps to enable stopping the process of handling the sniffer if necessary.
ok = True
dtype = elem.get( 'type', None )
if dtype is not None:
try:
fields = dtype.split( ":" )
datatype_module = fields[ 0 ]
datatype_class_name = fields[ 1 ]
module = None
except Exception, e:
self.log.exception( 'Error determining datatype class or module for dtype %s: %s' % ( str( dtype ), str( e ) ) )
ok = False
if ok:
if handling_proprietary_datatypes:
# See if one of the imported modules contains the datatype class name.
for imported_module in self.imported_modules:
if hasattr( imported_module, datatype_class_name ):
module = imported_module
break
if module is None:
try:
# The datatype class name must be contained in one of the datatype modules in the Galaxy distribution.
module = __import__( datatype_module )
for comp in datatype_module.split( '.' )[ 1: ]:
module = getattr( module, comp )
aclass = getattr( module, datatype_class_name )()
if deactivate:
if elem in self.sniffer_elems:
self.sniffer_elems.remove( elem )
for sniffer_class in self.sniff_order:
if sniffer_class.__class__ == aclass.__class__:
self.sniff_order.remove( sniffer_class )
break
self.log.debug( "Deactivated sniffer for datatype '%s'" % dtype )
else:
# See if we have a conflicting sniffer already loaded.
conflict = False
for conflict_loc, sniffer_class in enumerate( self.sniff_order ):
if sniffer_class.__class__ == aclass.__class__:
# We have a conflicting sniffer, so replace the one previously loaded.
conflict = True
except Exception, e:
self.log.exception( "Error importing datatype class for '%s': %s" % ( str( dtype ), str( e ) ) )
ok = False
if ok:
try:
aclass = getattr( module, datatype_class_name )()
except Exception, e:
self.log.exception( 'Error calling method %s from class %s: %s', str( datatype_class_name ), str( module ), str( e ) )
ok = False
if ok:
if deactivate:
# We are deactivating or uninstalling an installed Tool Shed repository, so eliminate the appropriate sniffers.
sniffer_class = elem.get( 'type', None )
if sniffer_class is not None:
for index, s_e_c in enumerate( sniffer_elem_classes ):
if sniffer_class == s_e_c:
del self.sniffer_elems[ index ]
sniffer_elem_classes = [ e.attrib[ 'type' ] for e in self.sniffer_elems ]
self.log.debug( "Removed sniffer element for datatype '%s'" % str( dtype ) )
break
for sniffer_class in self.sniff_order:
if sniffer_class.__class__ == aclass.__class__:
self.sniff_order.remove( sniffer_class )
self.log.debug( "Removed sniffer class for datatype '%s' from sniff order" % str( dtype ) )
break
else:
# We are loading new sniffer, so see if we have a conflicting sniffer already loaded.
conflict = False
for conflict_loc, sniffer_class in enumerate( self.sniff_order ):
if sniffer_class.__class__ == aclass.__class__:
# We have a conflicting sniffer, so replace the one previously loaded.
conflict = True
if override:
del self.sniff_order[ conflict_loc ]
self.log.debug( "Removed conflicting sniffer for datatype '%s'" % dtype )
break
if conflict:
if override:
del self.sniff_order[ conflict_loc ]
self.log.debug( "Replaced conflicting sniffer for datatype '%s'" % dtype )
break
if conflict:
if override:
self.sniff_order.append( aclass )
self.log.debug( "Loaded sniffer for datatype '%s'" % dtype )
else:
self.sniff_order.append( aclass )
self.log.debug( "Loaded sniffer for datatype '%s'" % dtype )
else:
self.sniff_order.append( aclass )
self.log.debug( "Loaded sniffer for datatype '%s'" % dtype )
except Exception, exc:
if deactivate:
self.log.warning( "Error deactivating sniffer for datatype '%s': %s" % ( dtype, str( exc ) ) )
else:
self.log.warning( "Error appending sniffer for datatype '%s' to sniff_order: %s" % ( dtype, str( exc ) ) )
self.upload_file_formats.sort()
# Persist the xml form of the registry into a temporary file so that it
# can be loaded from the command line by tools and set_metadata processing.
self.to_xml_file()
# Default values.
if not self.datatypes_by_extension:
self.datatypes_by_extension = {
'ab1' : binary.Ab1(),
'axt' : sequence.Axt(),
'bam' : binary.Bam(),
'bed' : interval.Bed(),
'coverage' : coverage.LastzCoverage(),
'customtrack' : interval.CustomTrack(),
'csfasta' : sequence.csFasta(),
'fasta' : sequence.Fasta(),
'eland' : tabular.Eland(),
'fastq' : sequence.Fastq(),
'fastqsanger' : sequence.FastqSanger(),
'gtf' : interval.Gtf(),
'gff' : interval.Gff(),
'gff3' : interval.Gff3(),
'genetrack' : tracks.GeneTrack(),
'interval' : interval.Interval(),
'laj' : images.Laj(),
'lav' : sequence.Lav(),
'maf' : sequence.Maf(),
'pileup' : tabular.Pileup(),
'qualsolid' : qualityscore.QualityScoreSOLiD(),
'qualsolexa' : qualityscore.QualityScoreSolexa(),
'qual454' : qualityscore.QualityScore454(),
'sam' : tabular.Sam(),
'scf' : binary.Scf(),
'sff' : binary.Sff(),
'tabular' : tabular.Tabular(),
'taxonomy' : tabular.Taxonomy(),
'txt' : data.Text(),
'wig' : interval.Wiggle(),
'xml' : xml.GenericXml(),
}
self.mimetypes_by_extension = {
'ab1' : 'application/octet-stream',
'axt' : 'text/plain',
'bam' : 'application/octet-stream',
'bed' : 'text/plain',
'customtrack' : 'text/plain',
'csfasta' : 'text/plain',
'eland' : 'application/octet-stream',
'fasta' : 'text/plain',
'fastq' : 'text/plain',
'fastqsanger' : 'text/plain',
'gtf' : 'text/plain',
'gff' : 'text/plain',
'gff3' : 'text/plain',
'interval' : 'text/plain',
'laj' : 'text/plain',
'lav' : 'text/plain',
'maf' : 'text/plain',
'memexml' : 'application/xml',
'pileup' : 'text/plain',
'qualsolid' : 'text/plain',
'qualsolexa' : 'text/plain',
'qual454' : 'text/plain',
'sam' : 'text/plain',
'scf' : 'application/octet-stream',
'sff' : 'application/octet-stream',
'tabular' : 'text/plain',
'taxonomy' : 'text/plain',
'txt' : 'text/plain',
'wig' : 'text/plain',
'xml' : 'application/xml',
}
# super supertype fix for input steps in workflows.
if 'data' not in self.datatypes_by_extension:
self.datatypes_by_extension['data'] = data.Data()
self.mimetypes_by_extension['data'] = 'application/octet-stream'
# Default values - the order in which we attempt to determine data types is critical
# because some formats are much more flexibly defined than others.
if len(self.sniff_order) < 1:
self.sniff_order = [
binary.Bam(),
binary.Sff(),
xml.GenericXml(),
sequence.Maf(),
sequence.Lav(),
sequence.csFasta(),
qualityscore.QualityScoreSOLiD(),
qualityscore.QualityScore454(),
sequence.Fasta(),
sequence.Fastq(),
interval.Wiggle(),
images.Html(),
sequence.Axt(),
interval.Bed(),
interval.CustomTrack(),
interval.Gtf(),
interval.Gff(),
interval.Gff3(),
tabular.Pileup(),
interval.Interval(),
tabular.Sam(),
tabular.Eland()
]
def append_to_sniff_order():
# Just in case any supported data types are not included in the config's sniff_order section.
for ext in self.datatypes_by_extension:
datatype = self.datatypes_by_extension[ext]
included = False
for atype in self.sniff_order:
if isinstance(atype, datatype.__class__):
included = True
break
if not included:
self.sniff_order.append(datatype)
append_to_sniff_order()
# Processing the new sniffer elem is now complete, so make sure the element defining it is loaded if necessary.
sniffer_class = elem.get( 'type', None )
if sniffer_class is not None:
if sniffer_class not in sniffer_elem_classes:
self.sniffer_elems.append( elem )
def get_datatype_class_by_name( self, name ):
"""
@@ -414,25 +419,28 @@ class Registry( object ):
#
#return datatype
def get_available_tracks(self):
def get_available_tracks( self ):
return self.available_tracks
def get_mimetype_by_extension(self, ext, default = 'application/octet-stream' ):
def get_mimetype_by_extension( self, ext, default='application/octet-stream' ):
"""Returns a mimetype based on an extension"""
try:
mimetype = self.mimetypes_by_extension[ext]
mimetype = self.mimetypes_by_extension[ ext ]
except KeyError:
#datatype was never declared
mimetype = default
self.log.warning('unknown mimetype in data factory %s' % ext)
self.log.warning( 'unknown mimetype in data factory %s' % str( ext ) )
return mimetype
def get_datatype_by_extension(self, ext ):
def get_datatype_by_extension( self, ext ):
"""Returns a datatype based on an extension"""
try:
builder = self.datatypes_by_extension[ext]
builder = self.datatypes_by_extension[ ext ]
except KeyError:
builder = data.Text()
return builder
def change_datatype(self, data, ext):
def change_datatype( self, data, ext ):
data.extension = ext
# call init_meta and copy metadata from itself. The datatype
# being converted *to* will handle any metadata copying and
@@ -441,13 +449,15 @@ class Registry( object ):
data.set_size()
data.init_meta( copy_from=data )
return data
def old_change_datatype(self, data, ext):
def old_change_datatype( self, data, ext ):
"""Creates and returns a new datatype based on an existing data and an extension"""
newdata = factory(ext)(id=data.id)
newdata = factory( ext )( id=data.id )
for key, value in data.__dict__.items():
setattr(newdata, key, value)
setattr( newdata, key, value )
newdata.ext = ext
return newdata
def load_datatype_converters( self, toolbox, installed_repository_dict=None, deactivate=False ):
"""
If deactivate is False, add datatype converters from self.converters or self.proprietary_converters
@@ -461,9 +471,9 @@ class Registry( object ):
# Load converters defined by local datatypes_conf.xml.
converters = self.converters
for elem in converters:
tool_config = elem[0]
source_datatype = elem[1]
target_datatype = elem[2]
tool_config = elem[ 0 ]
source_datatype = elem[ 1 ]
target_datatype = elem[ 2 ]
if installed_repository_dict:
converter_path = installed_repository_dict[ 'converter_path' ]
else:
@@ -490,7 +500,8 @@ class Registry( object ):
if converter.id in toolbox.tools_by_id:
del toolbox.tools_by_id[ converter.id ]
if source_datatype in self.datatype_converters:
del self.datatype_converters[ source_datatype ][ target_datatype ]
if target_datatype in self.datatype_converters[ source_datatype ]:
del self.datatype_converters[ source_datatype ][ target_datatype ]
self.log.debug( "Deactivated converter: %s", converter.id )
else:
toolbox.tools_by_id[ converter.id ] = converter
@@ -503,6 +514,7 @@ class Registry( object ):
self.log.exception( "Error deactivating converter from (%s): %s" % ( converter_path, str( e ) ) )
else:
self.log.exception( "Error loading converter (%s): %s" % ( converter_path, str( e ) ) )
def load_display_applications( self, installed_repository_dict=None, deactivate=False ):
"""
If deactivate is False, add display applications from self.display_app_containers or
@@ -577,6 +589,7 @@ class Registry( object ):
if current_app is None and isinstance( d_type1, type( d_type2 ) ):
self.log.debug( "Adding inherited display application '%s' to datatype '%s'" % ( display_app.id, extension ) )
d_type1.add_display_application( display_app )
def load_external_metadata_tool( self, toolbox ):
"""Adds a tool which is used to set external metadata"""
# We need to be able to add a job to the queue to set metadata. The queue will currently only accept jobs with an associated
@@ -585,6 +598,9 @@ class Registry( object ):
tool_xml_text = """
<tool id="__SET_METADATA__" name="Set External Metadata" version="1.0.1" tool_type="set_metadata">
<type class="SetMetadataTool" module="galaxy.tools"/>
<requirements>
<requirement type="package">samtools</requirement>
</requirements>
<action module="galaxy.tools.actions.metadata" class="SetMetadataToolAction"/>
<command>$__SET_EXTERNAL_METADATA_COMMAND_LINE__</command>
<inputs>
@@ -601,25 +617,128 @@ class Registry( object ):
toolbox.tools_by_id[ set_meta_tool.id ] = set_meta_tool
self.set_external_metadata_tool = set_meta_tool
self.log.debug( "Loaded external metadata tool: %s", self.set_external_metadata_tool.id )
def get_converters_by_datatype(self, ext):
def set_default_values( self ):
# Default values.
if not self.datatypes_by_extension:
self.datatypes_by_extension = {
'ab1' : binary.Ab1(),
'axt' : sequence.Axt(),
'bam' : binary.Bam(),
'bed' : interval.Bed(),
'coverage' : coverage.LastzCoverage(),
'customtrack' : interval.CustomTrack(),
'csfasta' : sequence.csFasta(),
'fasta' : sequence.Fasta(),
'eland' : tabular.Eland(),
'fastq' : sequence.Fastq(),
'fastqsanger' : sequence.FastqSanger(),
'gtf' : interval.Gtf(),
'gff' : interval.Gff(),
'gff3' : interval.Gff3(),
'genetrack' : tracks.GeneTrack(),
'interval' : interval.Interval(),
'laj' : images.Laj(),
'lav' : sequence.Lav(),
'maf' : sequence.Maf(),
'pileup' : tabular.Pileup(),
'qualsolid' : qualityscore.QualityScoreSOLiD(),
'qualsolexa' : qualityscore.QualityScoreSolexa(),
'qual454' : qualityscore.QualityScore454(),
'sam' : tabular.Sam(),
'scf' : binary.Scf(),
'sff' : binary.Sff(),
'tabular' : tabular.Tabular(),
'taxonomy' : tabular.Taxonomy(),
'txt' : data.Text(),
'wig' : interval.Wiggle(),
'xml' : xml.GenericXml(),
}
self.mimetypes_by_extension = {
'ab1' : 'application/octet-stream',
'axt' : 'text/plain',
'bam' : 'application/octet-stream',
'bed' : 'text/plain',
'customtrack' : 'text/plain',
'csfasta' : 'text/plain',
'eland' : 'application/octet-stream',
'fasta' : 'text/plain',
'fastq' : 'text/plain',
'fastqsanger' : 'text/plain',
'gtf' : 'text/plain',
'gff' : 'text/plain',
'gff3' : 'text/plain',
'interval' : 'text/plain',
'laj' : 'text/plain',
'lav' : 'text/plain',
'maf' : 'text/plain',
'memexml' : 'application/xml',
'pileup' : 'text/plain',
'qualsolid' : 'text/plain',
'qualsolexa' : 'text/plain',
'qual454' : 'text/plain',
'sam' : 'text/plain',
'scf' : 'application/octet-stream',
'sff' : 'application/octet-stream',
'tabular' : 'text/plain',
'taxonomy' : 'text/plain',
'txt' : 'text/plain',
'wig' : 'text/plain',
'xml' : 'application/xml',
}
# super supertype fix for input steps in workflows.
if 'data' not in self.datatypes_by_extension:
self.datatypes_by_extension[ 'data' ] = data.Data()
self.mimetypes_by_extension[ 'data' ] = 'application/octet-stream'
# Default values - the order in which we attempt to determine data types is critical
# because some formats are much more flexibly defined than others.
if len( self.sniff_order ) < 1:
self.sniff_order = [
binary.Bam(),
binary.Sff(),
xml.GenericXml(),
sequence.Maf(),
sequence.Lav(),
sequence.csFasta(),
qualityscore.QualityScoreSOLiD(),
qualityscore.QualityScore454(),
sequence.Fasta(),
sequence.Fastq(),
interval.Wiggle(),
images.Html(),
sequence.Axt(),
interval.Bed(),
interval.CustomTrack(),
interval.Gtf(),
interval.Gff(),
interval.Gff3(),
tabular.Pileup(),
interval.Interval(),
tabular.Sam(),
tabular.Eland()
]
def get_converters_by_datatype( self, ext ):
"""Returns available converters by source type"""
converters = odict()
source_datatype = type(self.get_datatype_by_extension(ext))
source_datatype = type( self.get_datatype_by_extension( ext ) )
for ext2, dict in self.datatype_converters.items():
converter_datatype = type(self.get_datatype_by_extension(ext2))
if issubclass(source_datatype, converter_datatype):
converters.update(dict)
converter_datatype = type( self.get_datatype_by_extension( ext2 ) )
if issubclass( source_datatype, converter_datatype ):
converters.update( dict )
#Ensure ext-level converters are present
if ext in self.datatype_converters.keys():
converters.update(self.datatype_converters[ext])
converters.update( self.datatype_converters[ ext ] )
return converters
def get_converter_by_target_type(self, source_ext, target_ext):
def get_converter_by_target_type( self, source_ext, target_ext ):
"""Returns a converter based on source and target datatypes"""
converters = self.get_converters_by_datatype(source_ext)
converters = self.get_converters_by_datatype( source_ext )
if target_ext in converters.keys():
return converters[target_ext]
return converters[ target_ext ]
return None
def find_conversion_destination_for_dataset_by_extensions( self, dataset, accepted_formats, converter_safe = True ):
def find_conversion_destination_for_dataset_by_extensions( self, dataset, accepted_formats, converter_safe=True ):
"""Returns ( target_ext, existing converted dataset )"""
for convert_ext in self.get_converters_by_datatype( dataset.ext ):
if self.get_datatype_by_extension( convert_ext ).matches_any( accepted_formats ):
@@ -632,8 +751,10 @@ class Registry( object ):
ret_data = None
return ( convert_ext, ret_data )
return ( None, None )
def get_composite_extensions( self ):
return [ ext for ( ext, d_type ) in self.datatypes_by_extension.iteritems() if d_type.composite_type is not None ]
def get_upload_metadata_params( self, context, group, tool ):
"""Returns dict of case value:inputs for metadata conditional for upload tool"""
rval = {}
@@ -649,16 +770,17 @@ class Registry( object ):
if 'auto' not in rval and 'txt' in rval: #need to manually add 'auto' datatype
rval[ 'auto' ] = rval[ 'txt' ]
return rval
@property
def integrated_datatypes_configs( self ):
if self.xml_filename and os.path.isfile( self.xml_filename ):
return self.xml_filename
self.to_xml_file()
return self.xml_filename
def to_xml_file( self ):
if self.xml_filename is not None:
# If persisted previously, attempt to remove
# the temporary file in which we were written.
# If persisted previously, attempt to remove the temporary file in which we were written.
try:
os.unlink( self.xml_filename )
except:
+9 -9
View File
@@ -2,12 +2,14 @@
Sequence classes
"""
import data
from . import data
import gzip
import json
import logging
import os
import re
import string
from cgi import escape
from galaxy import eggs, util
@@ -16,8 +18,6 @@ from galaxy.datatypes.checkers import is_gzip
from galaxy.datatypes.sniff import get_test_fname, get_headers
from galaxy.datatypes.metadata import MetadataElement
eggs.require("simplejson")
import simplejson
try:
eggs.require( "bx-python" )
@@ -44,8 +44,8 @@ class SequenceSplitLocations( data.Text ):
def set_peek( self, dataset, is_multi_byte=False ):
if not dataset.dataset.purged:
try:
parsed_data = simplejson.load(open(dataset.file_name))
# dataset.peek = simplejson.dumps(data, sort_keys=True, indent=4)
parsed_data = json.load(open(dataset.file_name))
# dataset.peek = json.dumps(data, sort_keys=True, indent=4)
dataset.peek = data.get_file_peek( dataset.file_name, is_multi_byte=is_multi_byte )
dataset.blurb = '%d sections' % len(parsed_data['sections'])
except Exception, e:
@@ -60,7 +60,7 @@ class SequenceSplitLocations( data.Text ):
def sniff( self, filename ):
if os.path.getsize(filename) < 50000:
try:
data = simplejson.load(open(filename))
data = json.load(open(filename))
sections = data['sections']
for section in sections:
if 'start' not in section or 'end' not in section or 'sequences' not in section:
@@ -155,7 +155,7 @@ class Sequence( data.Text ):
do_slow_split = classmethod(do_slow_split)
def do_fast_split( cls, input_datasets, toc_file_datasets, subdir_generator_function, split_params):
data = simplejson.load(open(toc_file_datasets[0].file_name))
data = json.load(open(toc_file_datasets[0].file_name))
sections = data['sections']
total_sequences = long(0)
for section in sections:
@@ -191,7 +191,7 @@ class Sequence( data.Text ):
toc = toc_file_datasets[ds_no]
split_data['args']['toc_file'] = toc.file_name
f = open(os.path.join(dir, 'split_info_%s.json' % base_name), 'w')
simplejson.dump(split_data, f)
json.dump(split_data, f)
f.close()
start_sequence += sequences_per_file[part_no]
return directories
@@ -557,7 +557,7 @@ class Fastq ( Sequence ):
sequence_count = long(args['num_sequences'])
if 'toc_file' in args:
toc_file = simplejson.load(open(args['toc_file'], 'r'))
toc_file = json.load(open(args['toc_file'], 'r'))
commands = Sequence.get_split_commands_with_toc(input_name, output_name, toc_file, start_sequence, sequence_count)
else:
commands = Sequence.get_split_commands_sequential(is_gzip(input_name), input_name, output_name, start_sequence, sequence_count)
+2 -2
View File
@@ -275,7 +275,7 @@ class Tabular( data.Text ):
cursor = f.read(1)
return to_json_string( { 'ck_data': util.unicodify( ck_data ), 'ck_index': ck_index + 1 } )
def display_data(self, trans, dataset, preview=False, filename=None, to_ext=None, chunk=None):
def display_data(self, trans, dataset, preview=False, filename=None, to_ext=None, chunk=None, **kwd):
preview = util.string_as_bool( preview )
if chunk:
return self.get_chunk(trans, dataset, chunk)
@@ -652,7 +652,7 @@ class Vcf( Tabular ):
MetadataElement( name="columns", default=10, desc="Number of columns", readonly=True, visible=False )
MetadataElement( name="column_types", default=['str','int','str','str','str','int','str','list','str','str'], param=metadata.ColumnTypesParameter, desc="Column types", readonly=True, visible=False )
MetadataElement( name="viz_filter_cols", desc="Score column for visualization", default=[5], param=metadata.ColumnParameter, multiple=True, visible=False )
MetadataElement( name="viz_filter_cols", desc="Score column for visualization", default=[5], param=metadata.ColumnParameter, optional=True, multiple=True, visible=False )
MetadataElement( name="sample_names", default=[], desc="Sample names", readonly=True, visible=False, optional=True, no_value=[] )
def sniff( self, filename ):
+1 -1
View File
@@ -75,7 +75,7 @@ class GFFFeature( GFFInterval ):
# Preference for name: GTF, GFF3, GFF.
for attr_name in [
# GTF:
'transcript_id', 'gene_id',
'gene_id', 'transcript_id',
# GFF3:
'ID', 'id',
# GFF (TODO):
+44 -7
View File
@@ -1,34 +1,71 @@
"""
Custom exceptions for Galaxy
"""
from galaxy import eggs
eggs.require( "Paste" )
from paste import httpexceptions
from ..exceptions import error_codes
class MessageException( Exception ):
"""
Exception to make throwing errors from deep in controllers easier
Exception to make throwing errors from deep in controllers easier.
"""
def __init__( self, err_msg, type="info" ):
self.err_msg = err_msg
# status code to be set when used with API.
status_code = 400
# Error code information embedded into API json responses.
err_code = error_codes.UNKNOWN
def __init__( self, err_msg=None, type="info", **extra_error_info ):
self.err_msg = err_msg or self.err_code.default_error_message
self.type = type
self.extra_error_info = extra_error_info
def __str__( self ):
return self.err_msg
class ItemDeletionException( MessageException ):
pass
class ItemAccessibilityException( MessageException ):
pass
status_code = 403
err_code = error_codes.USER_CANNOT_ACCESS_ITEM
class ItemOwnershipException( MessageException ):
pass
status_code = 403
err_code = error_codes.USER_DOES_NOT_OWN_ITEM
class DuplicatedSlugException( MessageException ):
status_code = 400
err_code = error_codes.USER_SLUG_DUPLICATE
class ObjectAttributeInvalidException( MessageException ):
status_code = 400
err_code = error_codes.USER_OBJECT_ATTRIBUTE_INVALID
class ObjectAttributeMissingException( MessageException ):
status_code = 400
err_code = error_codes.USER_OBJECT_ATTRIBUTE_MISSING
class ActionInputError( MessageException ):
def __init__( self, err_msg, type="error" ):
super( ActionInputError, self ).__init__( err_msg, type )
class ObjectNotFound( Exception ):
class ObjectNotFound( MessageException ):
""" Accessed object was not found """
pass
status_code = 404
err_code = error_codes.USER_OBJECT_NOT_FOUND
class ObjectInvalid( Exception ):
""" Accessed object store ID is invalid """
+57
View File
@@ -0,0 +1,57 @@
[
{
"name": "UNKNOWN",
"code": 0,
"message": "Unknown error occurred while processing request."
},
{
"name": "USER_CANNOT_RUN_AS",
"code": 400001,
"message": "User does not have permissions to run jobs as another user."
},
{
"name": "USER_INVALID_RUN_AS",
"code": 400002,
"message": "Invalid run_as request - run_as user does not exist."
},
{
"name": "USER_INVALID_JSON",
"code": 400003,
"message": "Your request did not appear to be valid JSON, please consult the API documentation."
},
{
"name": "USER_OBJECT_ATTRIBUTE_INVALID",
"code": 400004,
"message": "Attempted to create or update object with invalid attribute value."
},
{
"name": "USER_OBJECT_ATTRIBUTE_MISSING",
"code": 400005,
"message": "Attempted to create object without required attribute."
},
{
"name": "USER_SLUG_DUPLICATE",
"code": 400006,
"message": "Slug must be unique per user."
},
{
"name": "USER_NO_API_KEY",
"code": 403001,
"message": "API Authentication Required for this request"
},
{
"name": "USER_CANNOT_ACCESS_ITEM",
"code": 403002,
"message": "User cannot access specified item."
},
{
"name": "USER_DOES_NOT_OWN_ITEM",
"code": 403003,
"message": "User does not own specified item."
},
{
"name": "USER_OBJECT_NOT_FOUND",
"code": 404001,
"message": "No such object found."
}
]
+32
View File
@@ -0,0 +1,32 @@
from pkg_resources import resource_string
from json import loads
# Error codes are provided as a convience to Galaxy API clients, but at this
# time they do represent part of the more stable interface. They can change
# without warning between releases.
UNKNOWN_ERROR_MESSAGE = "Unknown error occurred while processing request."
class ErrorCode( object ):
def __init__( self, code, default_error_message ):
self.code = code
self.default_error_message = default_error_message or UNKNOWN_ERROR_MESSAGE
def __str__( self ):
return str( self.default_error_message )
def __int__( self ):
return int( self.code )
@staticmethod
def from_dict( entry ):
name = entry.get("name")
code = entry.get("code")
message = entry.get("message")
return ( name, ErrorCode( code, message ) )
error_codes_json = resource_string( __name__, 'error_codes.json' )
for entry in loads( error_codes_json ):
name, error_code_obj = ErrorCode.from_dict( entry )
globals()[ name ] = error_code_obj
+94 -91
View File
@@ -14,7 +14,6 @@ import re
import shutil
import subprocess
import sys
import threading
import traceback
from galaxy import model, util
from galaxy.datatypes import metadata
@@ -39,21 +38,6 @@ DATABASE_MAX_STRING_SIZE_PRETTY = util.DATABASE_MAX_STRING_SIZE_PRETTY
# and should eventually become API'd
TOOL_PROVIDED_JOB_METADATA_FILE = 'galaxy.json'
class Sleeper( object ):
"""
Provides a 'sleep' method that sleeps for a number of seconds *unless*
the notify method is called (from a different thread).
"""
def __init__( self ):
self.condition = threading.Condition()
def sleep( self, seconds ):
self.condition.acquire()
self.condition.wait( seconds )
self.condition.release()
def wake( self ):
self.condition.acquire()
self.condition.notify()
self.condition.release()
class JobDestination( Bunch ):
"""
@@ -74,6 +58,7 @@ class JobDestination( Bunch ):
if self.tags is not None:
self['tags'] = [ x.strip() for x in self.tags.split(',') ]
class JobToolConfiguration( Bunch ):
"""
Provides details on what handler and destination a tool should use
@@ -87,18 +72,21 @@ class JobToolConfiguration( Bunch ):
self['params'] = dict()
super(JobToolConfiguration, self).__init__(**kwds)
class JobConfiguration( object ):
"""A parser and interface to advanced job management features.
These features are configured in the job configuration, by default, ``job_conf.xml``
"""
DEFAULT_NWORKERS = 4
def __init__(self, app):
"""Parse the job configuration XML.
"""
self.app = app
self.runner_plugins = []
self.handlers = {}
self.handler_runner_plugins = {}
self.default_handler_id = None
self.destinations = {}
self.destination_tags = {}
@@ -151,6 +139,10 @@ class JobConfiguration( object ):
else:
log.debug("Read definition for handler '%s'" % id)
self.handlers[id] = (id,)
for plugin in handler.findall('plugin'):
if id not in self.handler_runner_plugins:
self.handler_runner_plugins[id] = []
self.handler_runner_plugins[id].append( plugin.get('id') )
if handler.get('tags', None) is not None:
for tag in [ x.strip() for x in handler.get('tags').split(',') ]:
if tag in self.handlers:
@@ -188,17 +180,17 @@ class JobConfiguration( object ):
self.tools[id].append(JobToolConfiguration(**dict(tool.items())))
self.tools[id][-1]['params'] = self.__get_params(tool)
types = dict(registered_user_concurrent_jobs = int,
anonymous_user_concurrent_jobs = int,
walltime = str,
output_size = int)
types = dict(registered_user_concurrent_jobs=int,
anonymous_user_concurrent_jobs=int,
walltime=str,
output_size=int)
self.limits = Bunch(registered_user_concurrent_jobs = None,
anonymous_user_concurrent_jobs = None,
walltime = None,
walltime_delta = None,
output_size = None,
concurrent_jobs = {})
self.limits = Bunch(registered_user_concurrent_jobs=None,
anonymous_user_concurrent_jobs=None,
walltime=None,
walltime_delta=None,
output_size=None,
concurrent_jobs={})
# Parse job limits
limits = root.find('limits')
@@ -269,12 +261,12 @@ class JobConfiguration( object ):
self.default_destination_id = self.app.config.default_cluster_job_runner
# Set the job limits
self.limits = Bunch(registered_user_concurrent_jobs = self.app.config.registered_user_job_limit,
anonymous_user_concurrent_jobs = self.app.config.anonymous_user_job_limit,
walltime = self.app.config.job_walltime,
walltime_delta = self.app.config.job_walltime_delta,
output_size = self.app.config.output_size_limit,
concurrent_jobs = {})
self.limits = Bunch(registered_user_concurrent_jobs=self.app.config.registered_user_job_limit,
anonymous_user_concurrent_jobs=self.app.config.anonymous_user_job_limit,
walltime=self.app.config.job_walltime,
walltime_delta=self.app.config.job_walltime_delta,
output_size=self.app.config.output_size_limit,
concurrent_jobs={})
log.debug('Done loading job configuration')
@@ -433,13 +425,19 @@ class JobConfiguration( object ):
"""
return self.destinations.get(id_or_tag, None)
def get_job_runner_plugins(self):
def get_job_runner_plugins(self, handler_id):
"""Load all configured job runner plugins
:returns: list of job runner plugins
"""
rval = {}
for runner in self.runner_plugins:
if handler_id in self.handler_runner_plugins:
plugins_to_load = [ rp for rp in self.runner_plugins if rp['id'] in self.handler_runner_plugins[handler_id] ]
log.info( "Handler '%s' will load specified runner plugins: %s", handler_id, ', '.join( [ rp['id'] for rp in plugins_to_load ] ) )
else:
plugins_to_load = self.runner_plugins
log.info( "Handler '%s' will load all configured runner plugins", handler_id )
for runner in plugins_to_load:
class_names = []
module = None
id = runner['id']
@@ -490,7 +488,7 @@ class JobConfiguration( object ):
try:
rval[id] = runner_class( self.app, runner[ 'workers' ], **runner.get( 'kwds', {} ) )
except TypeError:
log.warning( "Job runner '%s:%s' has not been converted to a new-style runner" % ( module_name, class_name ) )
log.exception( "Job runner '%s:%s' has not been converted to a new-style runner or encountered TypeError on load" % ( module_name, class_name ) )
rval[id] = runner_class( self.app )
log.debug( "Loaded job runner '%s:%s' as '%s'" % ( module_name, class_name, id ) )
return rval
@@ -549,6 +547,7 @@ class JobConfiguration( object ):
else:
log.warning("Legacy destination with id '%s' could not be converted: Unknown runner plugin: %s" % (id, destination.runner))
class JobWrapper( object ):
"""
Wraps a 'model.Job' with convenience methods for running processes and
@@ -643,7 +642,7 @@ class JobWrapper( object ):
Prepare the job to run by creating the working directory and the
config files.
"""
self.sa_session.expunge_all() #this prevents the metadata reverting that has been seen in conjunction with the PBS job runner
self.sa_session.expunge_all() # this prevents the metadata reverting that has been seen in conjunction with the PBS job runner
if not os.path.exists( self.working_directory ):
os.mkdir( self.working_directory )
@@ -702,12 +701,9 @@ class JobWrapper( object ):
self.command_line = self.tool.build_command_line( param_dict )
# FIXME: for now, tools get Galaxy's lib dir in their path
if self.command_line and self.command_line.startswith( 'python' ):
self.galaxy_lib_dir = os.path.abspath( "lib" ) # cwd = galaxy root
self.galaxy_lib_dir = os.path.abspath( "lib" ) # cwd = galaxy root
# Shell fragment to inject dependencies
if self.app.config.use_tool_dependencies:
self.dependency_shell_commands = self.tool.build_dependency_shell_commands()
else:
self.dependency_shell_commands = None
self.dependency_shell_commands = self.tool.build_dependency_shell_commands()
# We need command_line persisted to the db in order for Galaxy to re-queue the job
# if the server was stopped and restarted before the job finished
job.command_line = self.command_line
@@ -738,7 +734,7 @@ class JobWrapper( object ):
job.traceback = traceback.format_exc()
# Get the exception and let the tool attempt to generate
# a better message
etype, evalue, tb = sys.exc_info()
etype, evalue, tb = sys.exc_info()
m = self.tool.handle_job_failure_exception( evalue )
if m:
message = m
@@ -793,8 +789,8 @@ class JobWrapper( object ):
# If the job was deleted, call tool specific fail actions (used for e.g. external metadata) and clean up
if self.tool:
self.tool.job_failed( self, message, exception )
if self.app.config.cleanup_job == 'always' or (self.app.config.cleanup_job == 'onsuccess' and job.state == job.states.DELETED):
self.cleanup()
delete_files = self.app.config.cleanup_job == 'always' or (self.app.config.cleanup_job == 'onsuccess' and job.state == job.states.DELETED)
self.cleanup( delete_files=delete_files )
def pause( self, job=None, message=None ):
if job is None:
@@ -809,7 +805,7 @@ class JobWrapper( object ):
job.state = job.states.PAUSED
self.sa_session.add( job )
def change_state( self, state, info = False ):
def change_state( self, state, info=False ):
job = self.get_job()
self.sa_session.refresh( job )
for dataset_assoc in job.output_datasets + job.output_library_datasets:
@@ -835,7 +831,7 @@ class JobWrapper( object ):
log.warning('set_runner() is deprecated, use set_job_destination()')
self.set_job_destination(self.job_destination, external_id)
def set_job_destination(self, job_destination, external_id=None ):
def set_job_destination( self, job_destination, external_id=None ):
"""
Persist job destination params in the database for recovery.
@@ -887,11 +883,10 @@ class JobWrapper( object ):
# We set final_job_state to use for dataset management, but *don't* set
# job.state until after dataset collection to prevent history issues
if job.states.ERROR != job.state:
if ( self.check_tool_output( stdout, stderr, tool_exit_code, job )):
final_job_state = job.states.OK
else:
final_job_state = job.states.ERROR
if ( self.check_tool_output( stdout, stderr, tool_exit_code, job ) ):
final_job_state = job.states.OK
else:
final_job_state = job.states.ERROR
if self.version_string_cmd:
version_filename = self.get_version_string_path()
@@ -915,11 +910,11 @@ class JobWrapper( object ):
job.state = final_job_state
return self.fail( "Job %s's output dataset(s) could not be read" % job.id )
job_context = ExpressionContext( dict( stdout = job.stdout, stderr = job.stderr ) )
job_context = ExpressionContext( dict( stdout=job.stdout, stderr=job.stderr ) )
for dataset_assoc in job.output_datasets + job.output_library_datasets:
context = self.get_dataset_finish_context( job_context, dataset_assoc.dataset.dataset )
#should this also be checking library associations? - can a library item be added from a history before the job has ended? - lets not allow this to occur
for dataset in dataset_assoc.dataset.dataset.history_associations + dataset_assoc.dataset.dataset.library_associations: #need to update all associated output hdas, i.e. history was shared with job running
for dataset in dataset_assoc.dataset.dataset.history_associations + dataset_assoc.dataset.dataset.library_associations: # need to update all associated output hdas, i.e. history was shared with job running
trynum = 0
while trynum < self.app.config.retry_job_output_collection:
try:
@@ -930,9 +925,9 @@ class JobWrapper( object ):
except ( OSError, ObjectNotFound ), e:
trynum += 1
log.warning( 'Error accessing %s, will retry: %s', dataset.dataset.file_name, e )
time.sleep( 2 )
time.sleep( 2 )
dataset.blurb = 'done'
dataset.peek = 'no peek'
dataset.peek = 'no peek'
dataset.info = (dataset.info or '')
if context['stdout'].strip():
#Ensure white space between entries
@@ -960,7 +955,7 @@ class JobWrapper( object ):
#it would be quicker to just copy the metadata from the originating output dataset,
#but somewhat trickier (need to recurse up the copied_from tree), for now we'll call set_meta()
if ( not self.external_output_metadata.external_metadata_set_successfully( dataset, self.sa_session ) and self.app.config.retry_metadata_internally ):
dataset.datatype.set_meta( dataset, overwrite = False ) #call datatype.set_meta directly for the initial set_meta call during dataset creation
dataset.datatype.set_meta( dataset, overwrite=False ) # call datatype.set_meta directly for the initial set_meta call during dataset creation
elif not self.external_output_metadata.external_metadata_set_successfully( dataset, self.sa_session ) and job.states.ERROR != final_job_state:
dataset._state = model.Dataset.states.FAILED_METADATA
else:
@@ -1030,19 +1025,22 @@ class JobWrapper( object ):
out_data = dict( [ ( da.name, da.dataset ) for da in job.output_datasets ] )
inp_data.update( [ ( da.name, da.dataset ) for da in job.input_library_datasets ] )
out_data.update( [ ( da.name, da.dataset ) for da in job.output_library_datasets ] )
param_dict = dict( [ ( p.name, p.value ) for p in job.parameters ] ) # why not re-use self.param_dict here? ##dunno...probably should, this causes tools.parameters.basic.UnvalidatedValue to be used in following methods instead of validated and transformed values during i.e. running workflows
param_dict = dict( [ ( p.name, p.value ) for p in job.parameters ] ) # why not re-use self.param_dict here? ##dunno...probably should, this causes tools.parameters.basic.UnvalidatedValue to be used in following methods instead of validated and transformed values during i.e. running workflows
param_dict = self.tool.params_from_strings( param_dict, self.app )
# Check for and move associated_files
self.tool.collect_associated_files(out_data, self.working_directory)
gitd = self.sa_session.query( model.GenomeIndexToolData ).filter_by( job=job ).first()
if gitd:
self.tool.collect_associated_files({'' : gitd}, self.working_directory)
self.tool.collect_associated_files({'': gitd}, self.working_directory)
# Create generated output children and primary datasets and add to param_dict
collected_datasets = {'children':self.tool.collect_child_datasets(out_data, self.working_directory),'primary':self.tool.collect_primary_datasets(out_data, self.working_directory)}
param_dict.update({'__collected_datasets__':collected_datasets})
collected_datasets = {
'children': self.tool.collect_child_datasets(out_data, self.working_directory),
'primary': self.tool.collect_primary_datasets(out_data, self.working_directory)
}
param_dict.update({'__collected_datasets__': collected_datasets})
# Certain tools require tasks to be completed after job execution
# ( this used to be performed in the "exec_after_process" hook, but hooks are deprecated ).
self.tool.exec_after_process( self.queue.app, inp_data, out_data, param_dict, job = job )
self.tool.exec_after_process( self.queue.app, inp_data, out_data, param_dict, job=job )
# Call 'exec_after_process' hook
self.tool.call_hook( 'exec_after_process', self.queue.app, inp_data=inp_data,
out_data=out_data, param_dict=param_dict,
@@ -1068,22 +1066,26 @@ class JobWrapper( object ):
self.sa_session.flush()
log.debug( 'job %d ended' % self.job_id )
if self.app.config.cleanup_job == 'always' or ( not stderr and self.app.config.cleanup_job == 'onsuccess' ):
self.cleanup()
delete_files = self.app.config.cleanup_job == 'always' or ( job.state == job.states.OK and self.app.config.cleanup_job == 'onsuccess' )
self.cleanup( delete_files=delete_files )
def check_tool_output( self, stdout, stderr, tool_exit_code, job ):
return check_output( self.tool, stdout, stderr, tool_exit_code, job )
def cleanup( self ):
# remove temporary files
def cleanup( self, delete_files=True ):
# At least one of these tool cleanup actions (job import), is needed
# for thetool to work properly, that is why one might want to run
# cleanup but not delete files.
try:
for fname in self.extra_filenames:
os.remove( fname )
self.external_output_metadata.cleanup_external_metadata( self.sa_session )
if delete_files:
for fname in self.extra_filenames:
os.remove( fname )
self.external_output_metadata.cleanup_external_metadata( self.sa_session )
galaxy.tools.imp_exp.JobExportHistoryArchiveWrapper( self.job_id ).cleanup_after_job( self.sa_session )
galaxy.tools.imp_exp.JobImportHistoryArchiveWrapper( self.app, self.job_id ).cleanup_after_job()
galaxy.tools.genome_index.GenomeIndexToolWrapper( self.job_id ).postprocessing( self.sa_session, self.app )
self.app.object_store.delete(self.get_job(), base_dir='job_work', entire_dir=True, dir_only=True, extra_dir=str(self.job_id))
if delete_files:
self.app.object_store.delete(self.get_job(), base_dir='job_work', entire_dir=True, dir_only=True, extra_dir=str(self.job_id))
except:
log.exception( "Unable to cleanup job %d" % self.job_id )
@@ -1133,7 +1135,7 @@ class JobWrapper( object ):
def get_input_fnames( self ):
job = self.get_job()
filenames = []
for da in job.input_datasets + job.input_library_datasets: #da is JobToInputDatasetAssociation object
for da in job.input_datasets + job.input_library_datasets: # da is JobToInputDatasetAssociation object
if da.dataset:
filenames.extend(self.get_input_dataset_fnames(da.dataset))
return filenames
@@ -1155,11 +1157,12 @@ class JobWrapper( object ):
def compute_outputs( self ) :
class DatasetPath( object ):
def __init__( self, dataset_id, real_path, false_path = None, mutable = True ):
def __init__( self, dataset_id, real_path, false_path=None, mutable=True ):
self.dataset_id = dataset_id
self.real_path = real_path
self.false_path = false_path
self.mutable = mutable
def __str__( self ):
if self.false_path is None:
return self.real_path
@@ -1176,13 +1179,13 @@ class JobWrapper( object ):
self.output_hdas_and_paths = {}
for name, hda in [ ( da.name, da.dataset ) for da in job.output_datasets + job.output_library_datasets ]:
false_path = os.path.abspath( os.path.join( self.working_directory, "galaxy_dataset_%d.dat" % hda.dataset.id ) )
dsp = DatasetPath( hda.dataset.id, hda.dataset.file_name, false_path, mutable = hda.dataset.external_filename is None )
dsp = DatasetPath( hda.dataset.id, hda.dataset.file_name, false_path, mutable=hda.dataset.external_filename is None )
self.output_paths.append( dsp )
self.output_hdas_and_paths[name] = hda, dsp
if special:
false_path = os.path.abspath( os.path.join( self.working_directory, "galaxy_dataset_%d.dat" % special.dataset.id ) )
else:
results = [ ( da.name, da.dataset, DatasetPath( da.dataset.dataset.id, da.dataset.file_name, mutable = da.dataset.dataset.external_filename is None ) ) for da in job.output_datasets + job.output_library_datasets ]
results = [ ( da.name, da.dataset, DatasetPath( da.dataset.dataset.id, da.dataset.file_name, mutable=da.dataset.dataset.external_filename is None ) ) for da in job.output_datasets + job.output_library_datasets ]
self.output_paths = [t[2] for t in results]
self.output_hdas_and_paths = dict([(t[0], t[1:]) for t in results])
if special:
@@ -1257,13 +1260,13 @@ class JobWrapper( object ):
datatypes_config = self.app.datatypes_registry.integrated_datatypes_configs
return self.external_output_metadata.setup_external_metadata( [ output_dataset_assoc.dataset for output_dataset_assoc in job.output_datasets ],
self.sa_session,
exec_dir = exec_dir,
tmp_dir = tmp_dir,
dataset_files_path = dataset_files_path,
config_root = config_root,
config_file = config_file,
datatypes_config = datatypes_config,
job_metadata = os.path.join( self.working_directory, TOOL_PROVIDED_JOB_METADATA_FILE ),
exec_dir=exec_dir,
tmp_dir=tmp_dir,
dataset_files_path=dataset_files_path,
config_root=config_root,
config_file=config_file,
datatypes_config=datatypes_config,
job_metadata=os.path.join( self.working_directory, TOOL_PROVIDED_JOB_METADATA_FILE ),
**kwds )
@property
@@ -1337,7 +1340,7 @@ class JobWrapper( object ):
just copy these files directly to the ulimate destination.
"""
return output_path
@property
def requires_setting_metadata( self ):
if self.tool:
@@ -1410,7 +1413,7 @@ class TaskWrapper(JobWrapper):
out_data.update( [ ( da.name, da.dataset ) for da in job.output_library_datasets ] )
# DBTODO New method for generating command line for a task?
# These can be passed on the command line if wanted as $userId $userEmail
if job.history and job.history.user: # check for anonymous user!
if job.history and job.history.user: # check for anonymous user!
userId = '%d' % job.history.user.id
userEmail = str(job.history.user.email)
else:
@@ -1449,12 +1452,9 @@ class TaskWrapper(JobWrapper):
self.command_line = self.command_line.replace(k, v)
# FIXME: for now, tools get Galaxy's lib dir in their path
if self.command_line and self.command_line.startswith( 'python' ):
self.galaxy_lib_dir = os.path.abspath( "lib" ) # cwd = galaxy root
self.galaxy_lib_dir = os.path.abspath( "lib" ) # cwd = galaxy root
# Shell fragment to inject dependencies
if self.app.config.use_tool_dependencies:
self.dependency_shell_commands = self.tool.build_dependency_shell_commands()
else:
self.dependency_shell_commands = None
self.dependency_shell_commands = self.tool.build_dependency_shell_commands()
# We need command_line persisted to the db in order for Galaxy to re-queue the job
# if the server was stopped and restarted before the job finished
task.command_line = self.command_line
@@ -1474,7 +1474,7 @@ class TaskWrapper(JobWrapper):
self.status = 'error'
# How do we want to handle task failure? Fail the job and let it clean up?
def change_state( self, state, info = False ):
def change_state( self, state, info=False ):
task = self.get_task()
self.sa_session.refresh( task )
if info:
@@ -1523,8 +1523,8 @@ class TaskWrapper(JobWrapper):
# if the job was deleted, don't finish it
if task.state == task.states.DELETED:
# Job was deleted by an administrator
if self.app.config.cleanup_job in ( 'always', 'onsuccess' ):
self.cleanup()
delete_files = self.app.config.cleanup_job in ( 'always', 'onsuccess' )
self.cleanup( delete_files=delete_files )
return
elif task.state == task.states.ERROR:
self.fail( task.info )
@@ -1590,11 +1590,14 @@ class NoopQueue( object ):
"""
def put( self, *args, **kwargs ):
return
def put_stop( self, *args ):
return
def shutdown( self ):
return
class ParallelismInfo(object):
"""
Stores the information (if any) for running multiple instances of the tool in parallel
@@ -1606,7 +1609,7 @@ class ParallelismInfo(object):
items = tag.iteritems()
else:
items = tag.attrib.items()
self.attributes = dict([item for item in items if item[0] != 'method' ])
self.attributes = dict( [ item for item in items if item[ 0 ] != 'method' ])
if len(self.attributes) == 0:
# legacy basic mode - provide compatible defaults
self.attributes['split_size'] = 20
+2 -2
View File
@@ -12,7 +12,7 @@ def get_form_template(action_type, title, content, help, on_output = True ):
form = """
if (pja.action_type == "%s"){
p_str = "<div class='pjaForm toolForm'><span class='action_tag' style='display:none'>"+ pja.action_type + pja.output_name + "</span><div class='toolFormTitle'> %s <br/> on " + pja.output_name + "\
<div style='float: right;' class='buttons'><img src='/static/images/delete_icon.png'></div></div><div class='toolFormBody'>";
<div style='float: right;' class='buttons'><img src='/static/images/history-buttons/delete_icon.png'></div></div><div class='toolFormBody'>";
%s
p_str += "</div><div class='toolParamHelp'>%s</div></div>";
}""" % (action_type, title, content, help)
@@ -20,7 +20,7 @@ def get_form_template(action_type, title, content, help, on_output = True ):
form = """
if (pja.action_type == "%s"){
p_str = "<div class='pjaForm toolForm'><span class='action_tag' style='display:none'>"+ pja.action_type + "</span><div class='toolFormTitle'> %s \
<div style='float: right;' class='buttons'><img src='/static/images/delete_icon.png'></div></div><div class='toolFormBody'>";
<div style='float: right;' class='buttons'><img src='/static/images/history-buttons/delete_icon.png'></div></div><div class='toolFormBody'>";
%s
p_str += "</div><div class='toolParamHelp'>%s</div></div>";
}""" % (action_type, title, content, help)
+106 -42
View File
@@ -1,8 +1,11 @@
from os import getcwd
from os.path import abspath
CAPTURE_RETURN_CODE = "return_code=$?"
YIELD_CAPTURED_CODE = 'sh -c "exit $return_code"'
def build_command( job, job_wrapper, include_metadata=False, include_work_dir_outputs=True ):
def build_command( runner, job_wrapper, include_metadata=False, include_work_dir_outputs=True, remote_command_params={} ):
"""
Compose the sequence of commands necessary to execute a job. This will
currently include:
@@ -13,64 +16,125 @@ def build_command( job, job_wrapper, include_metadata=False, include_work_dir_ou
- commands to set metadata (if include_metadata is True)
"""
commands = job_wrapper.get_command_line()
commands_builder = CommandsBuilder(job_wrapper.get_command_line())
# All job runners currently handle this case which should never occur
if not commands:
if not commands_builder.commands:
return None
# Remove trailing semi-colon so we can start hacking up this command.
# TODO: Refactor to compose a list and join with ';', would be more clean.
commands = commands.rstrip("; ")
__handle_version_command(commands_builder, job_wrapper)
__handle_task_splitting(commands_builder, job_wrapper)
__handle_dependency_resolution(commands_builder, job_wrapper, remote_command_params)
if include_work_dir_outputs:
__handle_work_dir_outputs(commands_builder, job_wrapper, runner, remote_command_params)
if include_metadata and job_wrapper.requires_setting_metadata:
__handle_metadata(commands_builder, job_wrapper, runner, remote_command_params)
return commands_builder.build()
def __handle_version_command(commands_builder, job_wrapper):
# Prepend version string
if job_wrapper.version_string_cmd:
commands = "%s &> %s; " % ( job_wrapper.version_string_cmd, job_wrapper.get_version_string_path() ) + commands
version_command = "%s > %s 2>&1" % ( job_wrapper.version_string_cmd, job_wrapper.get_version_string_path() )
commands_builder.prepend_command(version_command)
def __handle_task_splitting(commands_builder, job_wrapper):
# prepend getting input files (if defined)
if hasattr(job_wrapper, 'prepare_input_files_cmds') and job_wrapper.prepare_input_files_cmds is not None:
commands = "; ".join( job_wrapper.prepare_input_files_cmds + [ commands ] )
if getattr(job_wrapper, 'prepare_input_files_cmds', None):
commands_builder.prepend_commands(job_wrapper.prepare_input_files_cmds)
def __handle_dependency_resolution(commands_builder, job_wrapper, remote_command_params):
local_dependency_resolution = remote_command_params.get("dependency_resolution", "local") == "local"
# Prepend dependency injection
if job_wrapper.dependency_shell_commands:
commands = "; ".join( job_wrapper.dependency_shell_commands + [ commands ] )
if job_wrapper.dependency_shell_commands and local_dependency_resolution:
commands_builder.prepend_commands(job_wrapper.dependency_shell_commands)
# Coping work dir outputs or setting metadata will mask return code of
# tool command. If these are used capture the return code and ensure
# the last thing that happens is an exit with return code.
capture_return_code_command = "; return_code=$?"
captured_return_code = False
def __handle_work_dir_outputs(commands_builder, job_wrapper, runner, remote_command_params):
# Append commands to copy job outputs based on from_work_dir attribute.
if include_work_dir_outputs:
work_dir_outputs = job.get_work_dir_outputs( job_wrapper )
if work_dir_outputs:
if not captured_return_code:
commands += capture_return_code_command
captured_return_code = True
work_dir_outputs_kwds = {}
if 'working_directory' in remote_command_params:
work_dir_outputs_kwds['job_working_directory'] = remote_command_params['working_directory']
work_dir_outputs = runner.get_work_dir_outputs( job_wrapper, **work_dir_outputs_kwds )
if work_dir_outputs:
commands_builder.capture_return_code()
copy_commands = map(__copy_if_exists_command, work_dir_outputs)
commands_builder.append_commands(copy_commands)
commands += "; " + "; ".join( [ "if [ -f %s ] ; then cp %s %s ; fi" %
( source_file, source_file, destination ) for ( source_file, destination ) in work_dir_outputs ] )
def __handle_metadata(commands_builder, job_wrapper, runner, remote_command_params):
# Append metadata setting commands, we don't want to overwrite metadata
# that was copied over in init_meta(), as per established behavior
if include_metadata and job_wrapper.requires_setting_metadata:
metadata_command = job_wrapper.setup_external_metadata(
exec_dir=abspath( getcwd() ),
tmp_dir=job_wrapper.working_directory,
dataset_files_path=job.app.model.Dataset.file_path,
output_fnames=job_wrapper.get_output_fnames(),
set_extension=False,
kwds={ 'overwrite' : False }
) or ''
metadata_command = metadata_command.strip()
if metadata_command:
if not captured_return_code:
commands += capture_return_code_command
captured_return_code = True
commands += "; cd %s; %s" % (abspath( getcwd() ), metadata_command)
metadata_kwds = remote_command_params.get('metadata_kwds', {})
exec_dir = metadata_kwds.get( 'exec_dir', abspath( getcwd() ) )
tmp_dir = metadata_kwds.get( 'tmp_dir', job_wrapper.working_directory )
dataset_files_path = metadata_kwds.get( 'dataset_files_path', runner.app.model.Dataset.file_path )
output_fnames = metadata_kwds.get( 'output_fnames', job_wrapper.get_output_fnames() )
config_root = metadata_kwds.get( 'config_root', None )
config_file = metadata_kwds.get( 'config_file', None )
datatypes_config = metadata_kwds.get( 'datatypes_config', None )
metadata_command = job_wrapper.setup_external_metadata(
exec_dir=exec_dir,
tmp_dir=tmp_dir,
dataset_files_path=dataset_files_path,
output_fnames=output_fnames,
set_extension=False,
config_root=config_root,
config_file=config_file,
datatypes_config=datatypes_config,
kwds={ 'overwrite' : False }
) or ''
metadata_command = metadata_command.strip()
if metadata_command:
commands_builder.capture_return_code()
commands_builder.append_command("cd %s; %s" % (exec_dir, metadata_command))
if captured_return_code:
commands += '; sh -c "exit $return_code"'
return commands
def __copy_if_exists_command(work_dir_output):
source_file, destination = work_dir_output
return "if [ -f %s ] ; then cp %s %s ; fi" % ( source_file, source_file, destination )
class CommandsBuilder(object):
def __init__(self, initial_command):
# Remove trailing semi-colon so we can start hacking up this command.
# TODO: Refactor to compose a list and join with ';', would be more clean.
commands = initial_command.rstrip("; ")
self.commands = commands
# Coping work dir outputs or setting metadata will mask return code of
# tool command. If these are used capture the return code and ensure
# the last thing that happens is an exit with return code.
self.return_code_captured = False
def prepend_command(self, command):
self.commands = "%s; %s" % (command, self.commands)
return self
def prepend_commands(self, commands):
return self.prepend_command("; ".join(commands))
def append_command(self, command):
self.commands = "%s; %s" % (self.commands, command)
def append_commands(self, commands):
self.append_command("; ".join(commands))
def capture_return_code(self):
if not self.return_code_captured:
self.return_code_captured = True
self.append_command(CAPTURE_RETURN_CODE)
def build(self):
if self.return_code_captured:
self.append_command(YIELD_CAPTURED_CODE)
return self.commands
__all__ = [build_command]
+4 -3
View File
@@ -11,7 +11,8 @@ from Queue import Queue, Empty
from sqlalchemy.sql.expression import and_, or_, select, func
from galaxy import model
from galaxy.jobs import Sleeper, JobWrapper, TaskWrapper, JobDestination
from galaxy.util.sleeper import Sleeper
from galaxy.jobs import JobWrapper, TaskWrapper, JobDestination
log = logging.getLogger( __name__ )
@@ -106,7 +107,7 @@ class JobHandlerQueue( object ):
if job.tool_id not in self.app.toolbox.tools_by_id:
log.warning( "(%s) Tool '%s' removed from tool config, unable to recover job" % ( job.id, job.tool_id ) )
JobWrapper( job, self ).fail( 'This tool was disabled before the job completed. Please contact your Galaxy administrator.' )
if job.job_runner_name is not None and job.job_runner_external_id is None:
elif job.job_runner_name is not None and job.job_runner_external_id is None:
# This could happen during certain revisions of Galaxy where a runner URL was persisted before the job was dispatched to a runner.
log.debug( "(%s) Job runner assigned but no external ID recorded, adding to the job handler queue" % job.id )
job.job_runner_name = None
@@ -564,7 +565,7 @@ class JobHandlerStopQueue( object ):
class DefaultJobDispatcher( object ):
def __init__( self, app ):
self.app = app
self.job_runners = self.app.job_config.get_job_runner_plugins()
self.job_runners = self.app.job_config.get_job_runner_plugins( self.app.config.server_name )
# Once plugins are loaded, all job destinations that were created from
# URLs can have their URL params converted to the destination's param
# dict by the plugin.
+2 -1
View File
@@ -11,7 +11,8 @@ import time
from Queue import Empty, Queue
from galaxy import model
from galaxy.jobs import handler, JobWrapper, NoopQueue, Sleeper
from galaxy.util.sleeper import Sleeper
from galaxy.jobs import handler, JobWrapper, NoopQueue
from galaxy.util.json import from_json_string
log = logging.getLogger( __name__ )
+55 -20
View File
@@ -15,19 +15,48 @@ import galaxy.jobs
from galaxy.jobs.command_factory import build_command
from galaxy import model
from galaxy.util import DATABASE_MAX_STRING_SIZE, shrink_stream_by_size
from galaxy.util import in_directory
from galaxy.jobs.runners.util.job_script import job_script
log = logging.getLogger( __name__ )
STOP_SIGNAL = object()
class RunnerParams( object ):
def __init__( self, specs = None, params = None ):
self.specs = specs or dict()
self.params = params or dict()
for name, value in self.params.items():
assert name in self.specs, 'Invalid job runner parameter for this plugin: %s' % name
if 'map' in self.specs[ name ]:
try:
self.params[ name ] = self.specs[ name ][ 'map' ]( value )
except Exception, e:
raise Exception( 'Job runner parameter "%s" value "%s" could not be converted to the correct type: %s' % ( name, value, e ) )
if 'valid' in self.specs[ name ]:
assert self.specs[ name ][ 'valid' ]( value ), 'Job runner parameter %s failed validation' % name
def __getattr__( self, name ):
return self.params.get( name, self.specs[ name ][ 'default' ] )
__getitem__ = __getattr__
class BaseJobRunner( object ):
def __init__( self, app, nworkers ):
def __init__( self, app, nworkers, **kwargs ):
"""Start the job runner
"""
self.app = app
self.sa_session = app.model.context
self.nworkers = nworkers
runner_param_specs = dict( recheck_missing_job_retries = dict( map = int, valid = lambda x: x >= 0, default = 0 ) )
if 'runner_param_specs' in kwargs:
runner_param_specs.update( kwargs.pop( 'runner_param_specs' ) )
if kwargs:
log.debug( 'Loading %s with params: %s', self.runner_name, kwargs )
self.runner_params = RunnerParams( specs = runner_param_specs, params = kwargs )
def _init_worker_threads(self):
"""Start ``nworkers`` worker threads.
@@ -114,7 +143,7 @@ class BaseJobRunner( object ):
job_wrapper.cleanup()
return False
elif job_state != model.Job.states.QUEUED:
log.info( "(%d) Job is in state %s, skipping execution" % ( job_id, job_state ) )
log.info( "(%s) Job is in state %s, skipping execution" % ( job_id, job_state ) )
# cleanup may not be safe in all states
return False
@@ -146,23 +175,13 @@ class BaseJobRunner( object ):
def build_command_line( self, job_wrapper, include_metadata=False, include_work_dir_outputs=True ):
return build_command( self, job_wrapper, include_metadata=include_metadata, include_work_dir_outputs=include_work_dir_outputs )
def get_work_dir_outputs( self, job_wrapper ):
def get_work_dir_outputs( self, job_wrapper, job_working_directory=None ):
"""
Returns list of pairs (source_file, destination) describing path
to work_dir output file and ultimate destination.
"""
def in_directory( file, directory ):
"""
Return true, if the common prefix of both is equal to directory
e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
"""
# Make both absolute.
directory = os.path.abspath( directory )
file = os.path.abspath( file )
return os.path.commonprefix( [ file, directory ] ) == directory
if not job_working_directory:
job_working_directory = os.path.abspath( job_wrapper.working_directory )
# Set up dict of dataset id --> output path; output path can be real or
# false depending on outputs_to_working_directory
@@ -186,9 +205,9 @@ class BaseJobRunner( object ):
if hda_tool_output and hda_tool_output.from_work_dir:
# Copy from working dir to HDA.
# TODO: move instead of copy to save time?
source_file = os.path.join( os.path.abspath( job_wrapper.working_directory ), hda_tool_output.from_work_dir )
source_file = os.path.join( job_working_directory, hda_tool_output.from_work_dir )
destination = job_wrapper.get_output_destination( output_paths[ dataset.dataset_id ] )
if in_directory( source_file, job_wrapper.working_directory ):
if in_directory( source_file, job_working_directory ):
output_pairs.append( ( source_file, destination ) )
log.debug( "Copying %s to %s as directed by from_work_dir" % ( source_file, destination ) )
else:
@@ -196,7 +215,7 @@ class BaseJobRunner( object ):
log.exception( "from_work_dir specified a location not in the working directory: %s, %s" % ( source_file, job_wrapper.working_directory ) )
return output_pairs
def _handle_metadata_externally(self, job_wrapper):
def _handle_metadata_externally( self, job_wrapper, resolve_requirements=False ):
"""
Set metadata externally. Used by the local and lwr job runners where this
shouldn't be attached to command-line to execute.
@@ -210,7 +229,19 @@ class BaseJobRunner( object ):
tmp_dir=job_wrapper.working_directory,
#we don't want to overwrite metadata that was copied over in init_meta(), as per established behavior
kwds={ 'overwrite' : False } )
if resolve_requirements:
dependency_shell_commands = self.app.datatypes_registry.set_external_metadata_tool.build_dependency_shell_commands()
if dependency_shell_commands:
if isinstance( dependency_shell_commands, list ):
dependency_shell_commands = "&&".join( dependency_shell_commands )
external_metadata_script = "%s&&%s" % ( dependency_shell_commands, external_metadata_script )
log.debug( 'executing external set_meta script for job %d: %s' % ( job_wrapper.job_id, external_metadata_script ) )
if resolve_requirements:
dependency_shell_commands = self.app.datatypes_registry.set_external_metadata_tool.build_dependency_shell_commands()
if dependency_shell_commands:
if isinstance( dependency_shell_commands, list ):
dependency_shell_commands = "&&".join( dependency_shell_commands )
external_metadata_script = "%s&&%s" % ( dependency_shell_commands, external_metadata_script )
external_metadata_proc = subprocess.Popen( args=external_metadata_script,
shell=True,
env=os.environ,
@@ -229,6 +260,10 @@ class BaseJobRunner( object ):
options.update(**kwds)
return job_script(**options)
def _complete_terminal_job( self, ajs, **kwargs ):
if ajs.job_wrapper.get_state() != model.Job.states.DELETED:
self.work_queue.put( ( self.finish_job, ajs ) )
class AsynchronousJobState( object ):
"""
@@ -290,8 +325,8 @@ class AsynchronousJobRunner( BaseJobRunner ):
to the correct methods (queue, finish, cleanup) at appropriate times..
"""
def __init__( self, app, nworkers ):
super( AsynchronousJobRunner, self ).__init__( app, nworkers )
def __init__( self, app, nworkers, **kwargs ):
super( AsynchronousJobRunner, self ).__init__( app, nworkers, **kwargs )
# 'watched' and 'queue' are both used to keep track of jobs to watch.
# 'queue' is used to add new watched jobs, and can be called from
# any thread (usually by the 'queue_job' method). 'watched' must only
+2 -1
View File
@@ -35,7 +35,8 @@ cd %s
echo $? > %s
"""
argmap = { 'Execution_Time' : '-a',
argmap = { 'destination' : '-q',
'Execution_Time' : '-a',
'Account_Name' : '-A',
'Checkpoint' : '-c',
'Error_Path' : '-e',

Some files were not shown because too many files have changed in this diff Show More