Merged galaxy/galaxy-central into default

This commit is contained in:
Eric Rasche
2015-02-05 14:43:26 -06:00
443 changed files with 30423 additions and 12871 deletions
+5 -8
View File
@@ -30,9 +30,9 @@ database/openid_consumer_cache
*.pyc
# Galaxy Runtime Files
paster.lock
paster.log
paster.pid
*.lock
*.log
*.pid
# Tool Shed Runtime Files
tool_shed_webapp.lock
@@ -108,12 +108,9 @@ tool-data/shared/jars/
# CSS build artifacts.
sprite-*.less
# Local node_modules directories
# Local node_modules and bower_components directories
node_modules
# Testing
selenium-server.jar
selenium_results.html
bower_components
# Documentation build files.
doc/build
+13 -11
View File
@@ -8,16 +8,18 @@ a4113cc1cb5eaa68091c9a73375f00555b66dd11 release_2013.01.13
5e605ed6069fe4c5ca9875e95e91b2713499e8ca release_2014.02.10
9e53251b0b7e93b9563008a2b112f2e815a04bbc release_2014.04.14
7e257c7b10badb65772b1528cb61d58175a42e47 release_2014.06.02
7a4d321c0e38fa263ea83d29a35a608c3181fcba latest_2014.06.02
9661b9d5d5b330483ae3ad2236410e0efaa7c500 latest_2014.04.14
6b0bd93038a843b1585155f0d63f0eea2459c70b latest_2013.01.13
3e62060b14b9afc46f8e0ec02e1a4500d77db9e1 latest_2013.02.08
425009b3ff4d8b67d2812253b221f3c4f4a8d1e3 latest_2013.04.01
9713d86392ef985ffcdc39ff0c8ddf51a1f9ce47 latest_2013.06.03
9ed84cd208e07e8985ec917cb025fcbbb09edcfb latest_2013.08.12
81fbe25bd02edcd53065e8e4476dd1dfb5a72cf2 latest_2013.11.04
2a756ca2cb1826db7796018e77d12e2dd7b67603 latest_2014.02.10
9bce3f426863f8ba88062f67c7efc1836e82ee7c latest_2014.06.02
8f9dcac033694e4cabcf5daae5cca1cfefbe967f latest_2014.04.14
9c323aad4ffdd65a3deb06a4a36f6b2c5115a60f latest_2013.01.13
b986c184be88947b5d1d90be7f36cfd2627dd938 latest_2013.02.08
dec9431d66b837a208e2f060d90afd913c721227 latest_2013.04.01
19e56e66b0b344c6e2afa4541f6988e4fdb9af29 latest_2013.06.03
cee903b8b3eee9145627ee89742555dac581791e latest_2013.08.12
7d5aa19a166cba9039e15f338a1e3fc924c43d3a latest_2013.11.04
0c000cc2f9c05bf4c1c2bc3a10215014fd64e696 latest_2014.02.10
ca45b78adb4152fc6e7395514d46eba6b7d0b838 release_2014.08.11
548ab24667d6206780237bd807f7d857a484c461 latest_2014.08.11
f3fc4602e22b39468d780b955a1a05caf867a7e9 latest_2014.08.11
2092948937ac30ef82f71463a235c66d34987088 release_2014.10.06
7086b87d83a9092cbece0fec6f3e3ed03266be0c latest_2014.10.06
9bd6f8b5b8153db752f4e61ed62f2b6c01ae4a11 latest_2014.10.06
2e8dd2949dd3eee0f56f9a3a5ebf1b2baca24aee release_2015.01.13
4039bfd5584aac053f686197a76ac176253e6f3d latest_2015.01.13
+85 -2
View File
@@ -47,15 +47,77 @@ module.exports = function(grunt) {
options: {
spawn: false
}
},
// call bower to install libraries and other external resources
"bower-install-simple" : {
options: {
color: true
},
"prod": {
options: {
production: true
}
},
"dev": {
options: {
production: false
}
}
},
// where to move fetched bower components into the build structure (libName: [ bower-location, libs-location ])
libraryLocations : {
"jquery": [ "dist/jquery.js", "jquery/jquery.js" ],
"jquery-migrate": [ "jquery-migrate.js", "jquery/jquery.migrate.js" ],
"traceKit": [ "tracekit.js", "tracekit.js" ],
"ravenjs": [ "dist/raven.js", "raven.js" ],
"underscore": [ "underscore.js", "underscore.js" ],
"handlebars": [ "handlebars.runtime.js", "handlebars.runtime.js" ],
"lunr.js": [ "lunr.js", "lunr.js" ]
//"backbone": [ "backbone.js", "backbone/backbone.js" ],
// these need to be updated and tested
//"require": [ "build/require.js", "require.js" ],
//"d3": [ "d3.js", "d3.js" ],
//"farbtastic": [ "src/farbtastic.js", "farbtastic.js" ],
//"jQTouch": [ "src/reference/jqtouch.js", "jquery/jqtouch.js" ],
//"bib2json": [ "Parser.js", "bibtex.js" ],
//"jquery-form": [ "jquery.form.js", "jquery/jquery.form.js" ],
//"jquery-autocomplete": [ "src/jquery.autocomplete.js", "jquery/jquery.autocomplete.js" ],
//"select2": [ "select2.js", "jquery/select2.js" ],
//"jStorage": [ "jstorage.js", "jquery/jstorage.js" ],
//"jquery.cookie": [ "", "jquery/jquery.cookie.js" ],
//"dynatree": [ "dist/jquery.dynatree.js", "jquery/jquery.dynatree.js" ],
//"jquery-mousewheel": [ "jquery.mousewheel.js", "jquery/jquery.mousewheel.js" ],
//"jquery.event.drag-drop": [
// [ "event.drag/jquery.event.drag.js", "jquery/jquery.event.drag.js" ],
// [ "event.drag/jquery.event.drop.js", "jquery/jquery.event.drop.js" ]
//],
// these are complicated by additional css/less
//"toastr": [ "toastr.js", "toastr.js" ],
//"wymeditor": [ "dist/wymeditor/jquery.wymeditor.js", "jquery/jquery.wymeditor.js" ],
//"jstree": [ "jstree.js", "jquery/jstree.js" ],
// these have been customized by Galaxy
//"bootstrap": [ "dist/js/bootstrap.js", "bootstrap.js" ],
//"jquery-ui": [
// // multiple components now
// [ "", "jquery/jquery-ui.js" ]
//],
}
});
grunt.loadNpmTasks( 'grunt-contrib-watch' );
grunt.loadNpmTasks( 'grunt-contrib-copy');
grunt.loadNpmTasks('grunt-exec');
grunt.loadNpmTasks( 'grunt-bower-install-simple');
grunt.loadNpmTasks( 'grunt-exec' );
grunt.registerTask( 'pack', [ 'exec' ] );
grunt.registerTask( 'default', [ 'copy', 'pack' ] );
grunt.registerTask( 'default', [ 'copy:main', 'pack' ] );
// -------------------------------------------------------------------------- copy,pack only those changed
// adapted from grunt-contrib-watch jslint example
@@ -85,4 +147,25 @@ module.exports = function(grunt) {
onChange();
});
// -------------------------------------------------------------------------- fetch/update external libraries
/** copy external libraries from bower components to scripts/libs */
function copyLibs(){
var libraryLocations = grunt.config( 'libraryLocations' );
for( var libName in libraryLocations ){
if( libraryLocations.hasOwnProperty( libName ) ){
var BOWER_DIR = 'bower_components',
location = libraryLocations[ libName ],
source = [ BOWER_DIR, libName, location[0] ].join( '/' ),
destination = 'galaxy/scripts/libs/' + location[1];
grunt.log.writeln( source + ' -> ' + destination );
grunt.file.copy( source, destination );
}
}
}
grunt.registerTask( 'copy-libs', 'copy external libraries to src', copyLibs );
grunt.registerTask( 'install-libs', 'fetch external libraries and copy to src',
[ 'bower-install-simple:prod', 'copy-libs' ] );
};
+41
View File
@@ -0,0 +1,41 @@
{
"name": "galaxy-client-libs",
"version": "0.0.0",
"description": "External client-side libraries used by Galaxy",
"keywords": [
"galaxy",
"galaxyproject"
],
"homepage": "usegalaxy.org",
"dependencies": {
"jquery": "~1.11.1",
"traceKit": "*",
"ravenjs": "~1.1.16",
"require": "*",
"underscore": "~1.7.0",
"backbone": "~1.1.2",
"bootstrap": "~3.3.2",
"d3": "~3.5.3",
"farbtastic": "~2.0.0-alpha.1",
"toastr": "~2.1.0",
"jQTouch": "git://github.com/senchalabs/jQTouch#~1.0.0",
"bib2json": "git://github.com/mayanklahiri/bib2json",
"jquery-form": "~3.46.0",
"jquery-autocomplete": "git://github.com/dyve/jquery-autocomplete",
"select2": "~3.5.2",
"jStorage": "~0.4.12",
"jquery.cookie": "~1.4.1",
"dynatree": "~1.2.5",
"jquery-mousewheel": "~3.1.12",
"wymeditor": "~1.0.0-rc.1",
"jstree": "~3.0.9",
"jquery-ui": "git://github.com/jquery/jquery-ui.git#~1.11.2",
"jquery.event.drag-drop": "~2.2.1",
"handlebars": "~2.0.0",
"jquery-migrate": "~1.2.1",
"lunr.js": "git://github.com/olivernn/lunr.js.git#~0.5.7"
},
"resolutions": {
"jquery": "~1.11.1"
}
}
+1 -1
View File
@@ -364,7 +364,7 @@ var GalaxyMastheadTab = Backbone.View.extend({
var $popover_element = $(this.el).find('.head');
$popover_element.popover({
html: true,
content: 'Please <a href="' + galaxy_config.root + '/user/login">log in</a> or <a href="' + galaxy_config.root + '/user/create">register</a> to use this feature.',
content: 'Please <a href="' + galaxy_config.root + 'user/login?use_panels=True">log in</a> or <a href="' + galaxy_config.root + 'user/create?use_panels=True">register</a> to use this feature.',
placement: 'bottom'
}).on('shown.bs.popover', function() { // hooking on bootstrap event to automatically hide popovers after delay
setTimeout(function() {
@@ -805,7 +805,7 @@ var Node = Backbone.Model.extend({
nodeView.addDataOutput( output );
} );
nodeView.render();
workflow.node_changed( this );
workflow.node_changed( this, true);
},
update_field_data : function( data ) {
var node = this;
@@ -1089,17 +1089,25 @@ $.extend( Workflow.prototype, {
if ( this.active_node != node ) {
this.check_changes_in_active_form();
this.clear_active_node();
parent.show_form_for_tool( node.form_html + node.tooltip, node );
if (parent.__NEWTOOLFORM__) {
parent.show_form_for_tool( node.form_html, node );
} else {
parent.show_form_for_tool( node.form_html + node.tooltip, node );
}
node.make_active();
this.active_node = node;
}
},
node_changed : function ( node ) {
node_changed : function ( node, force ) {
this.has_changes = true;
if ( this.active_node == node ) {
if ( this.active_node == node && (!parent.__NEWTOOLFORM__ || force)) {
// Reactive with new form_html
this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped)
parent.show_form_for_tool( node.form_html + node.tooltip, node );
if (parent.__NEWTOOLFORM__) {
parent.show_form_for_tool( node.form_html, node );
} else {
parent.show_form_for_tool( node.form_html + node.tooltip, node );
}
}
},
layout : function () {
@@ -1305,7 +1313,6 @@ function add_node( type, title_text, tool_id ) {
return node;
}
var ext_to_type = null;
var type_to_type = null;
@@ -1863,6 +1870,9 @@ $.extend( CanvasManager.prototype, {
left: x,
top: y
});
self.cv.css( { "background-position-x": x,
"background-position-y": y
});
self.update_viewport_overlay();
};
// Dragging within canvas background
+575 -277
View File
@@ -1,6 +1,8 @@
/*
/*!
Copyright (C) 2011 by Yehuda Katz
handlebars v2.0.0
Copyright (C) 2011-2014 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -20,343 +22,639 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@license
*/
// lib/handlebars/browser-prefix.js
var Handlebars = {};
(function(Handlebars, undefined) {
;
// lib/handlebars/base.js
Handlebars.VERSION = "1.0.0";
Handlebars.COMPILER_REVISION = 4;
Handlebars.REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '>= 1.0.0'
};
Handlebars.helpers = {};
Handlebars.partials = {};
var toString = Object.prototype.toString,
functionType = '[object Function]',
objectType = '[object Object]';
Handlebars.registerHelper = function(name, fn, inverse) {
if (toString.call(name) === objectType) {
if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); }
Handlebars.Utils.extend(this.helpers, name);
/* exported Handlebars */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
if (inverse) { fn.not = inverse; }
this.helpers[name] = fn;
root.Handlebars = root.Handlebars || factory();
}
};
Handlebars.registerPartial = function(name, str) {
if (toString.call(name) === objectType) {
Handlebars.Utils.extend(this.partials, name);
} else {
this.partials[name] = str;
}
};
Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Missing helper: '" + arg + "'");
}
});
Handlebars.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if(type === "[object Array]") {
if(context.length > 0) {
return Handlebars.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
return fn(context);
}
});
Handlebars.K = function() {};
Handlebars.createFrame = Object.create || function(object) {
Handlebars.K.prototype = object;
var obj = new Handlebars.K();
Handlebars.K.prototype = null;
return obj;
};
Handlebars.logger = {
DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'},
// can be overridden in the host environment
log: function(level, obj) {
if (Handlebars.logger.level <= level) {
var method = Handlebars.logger.methodMap[level];
if (typeof console !== 'undefined' && console[method]) {
console[method].call(console, obj);
}
}
}
};
Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); };
Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (options.data) {
data = Handlebars.createFrame(options.data);
}(this, function () {
// handlebars/safe-string.js
var __module3__ = (function() {
"use strict";
var __exports__;
// Build out our basic SafeString type
function SafeString(string) {
this.string = string;
}
if(context && typeof context === 'object') {
if(context instanceof Array){
for(var j = context.length; i<j; i++) {
if (data) { data.index = i; }
ret = ret + fn(context[i], { data: data });
}
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) { data.key = key; }
ret = ret + fn(context[key], {data: data});
i++;
SafeString.prototype.toString = function() {
return "" + this.string;
};
__exports__ = SafeString;
return __exports__;
})();
// handlebars/utils.js
var __module2__ = (function(__dependency1__) {
"use strict";
var __exports__ = {};
/*jshint -W004 */
var SafeString = __dependency1__;
var escape = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
function escapeChar(chr) {
return escape[chr];
}
function extend(obj /* , ...source */) {
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
obj[key] = arguments[i][key];
}
}
}
return obj;
}
if(i === 0){
ret = inverse(this);
__exports__.extend = extend;var toString = Object.prototype.toString;
__exports__.toString = toString;
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
var isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
/* istanbul ignore next */
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
};
}
var isFunction;
__exports__.isFunction = isFunction;
/* istanbul ignore next */
var isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
__exports__.isArray = isArray;
return ret;
});
Handlebars.registerHelper('if', function(conditional, options) {
var type = toString.call(conditional);
if(type === functionType) { conditional = conditional.call(this); }
if(!conditional || Handlebars.Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
Handlebars.registerHelper('unless', function(conditional, options) {
return Handlebars.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn});
});
Handlebars.registerHelper('with', function(context, options) {
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if (!Handlebars.Utils.isEmpty(context)) return options.fn(context);
});
Handlebars.registerHelper('log', function(context, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
Handlebars.log(level, context);
});
;
// lib/handlebars/utils.js
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
Handlebars.Exception = function(message) {
var tmp = Error.prototype.constructor.apply(this, arguments);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
};
Handlebars.Exception.prototype = new Error();
// Build out our basic SafeString type
Handlebars.SafeString = function(string) {
this.string = string;
};
Handlebars.SafeString.prototype.toString = function() {
return this.string.toString();
};
var escape = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
var escapeChar = function(chr) {
return escape[chr] || "&amp;";
};
Handlebars.Utils = {
extend: function(obj, value) {
for(var key in value) {
if(value.hasOwnProperty(key)) {
obj[key] = value[key];
}
}
},
escapeExpression: function(string) {
function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
if (string instanceof SafeString) {
return string.toString();
} else if (string == null || string === false) {
} else if (string == null) {
return "";
} else if (!string) {
return string + '';
}
// Force a string conversion as this will be done by the append regardless and
// the regex test will do this transparently behind the scenes, causing issues if
// an object's to string has escaped characters in it.
string = string.toString();
string = "" + string;
if(!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
},
}
isEmpty: function(value) {
__exports__.escapeExpression = escapeExpression;function isEmpty(value) {
if (!value && value !== 0) {
return true;
} else if(toString.call(value) === "[object Array]" && value.length === 0) {
} else if (isArray(value) && value.length === 0) {
return true;
} else {
return false;
}
}
};
;
// lib/handlebars/runtime.js
Handlebars.VM = {
template: function(templateSpec) {
__exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) {
return (contextPath ? contextPath + '.' : '') + id;
}
__exports__.appendContextPath = appendContextPath;
return __exports__;
})(__module3__);
// handlebars/exception.js
var __module4__ = (function() {
"use strict";
var __exports__;
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
function Exception(message, node) {
var line;
if (node && node.firstLine) {
line = node.firstLine;
message += ' - ' + line + ':' + node.firstColumn;
}
var tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
if (line) {
this.lineNumber = line;
this.column = node.firstColumn;
}
}
Exception.prototype = new Error();
__exports__ = Exception;
return __exports__;
})();
// handlebars/base.js
var __module1__ = (function(__dependency1__, __dependency2__) {
"use strict";
var __exports__ = {};
var Utils = __dependency1__;
var Exception = __dependency2__;
var VERSION = "2.0.0";
__exports__.VERSION = VERSION;var COMPILER_REVISION = 6;
__exports__.COMPILER_REVISION = COMPILER_REVISION;
var REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '== 1.x.x',
5: '== 2.0.0-alpha.x',
6: '>= 2.0.0-beta.1'
};
__exports__.REVISION_CHANGES = REVISION_CHANGES;
var isArray = Utils.isArray,
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
function HandlebarsEnvironment(helpers, partials) {
this.helpers = helpers || {};
this.partials = partials || {};
registerDefaultHelpers(this);
}
__exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = {
constructor: HandlebarsEnvironment,
logger: logger,
log: log,
registerHelper: function(name, fn) {
if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple helpers'); }
Utils.extend(this.helpers, name);
} else {
this.helpers[name] = fn;
}
},
unregisterHelper: function(name) {
delete this.helpers[name];
},
registerPartial: function(name, partial) {
if (toString.call(name) === objectType) {
Utils.extend(this.partials, name);
} else {
this.partials[name] = partial;
}
},
unregisterPartial: function(name) {
delete this.partials[name];
}
};
function registerDefaultHelpers(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
if(arguments.length === 1) {
// A missing field in a {{foo}} constuct.
return undefined;
} else {
// Someone is actually trying to call something, blow up.
throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'");
}
});
instance.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse,
fn = options.fn;
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if (isArray(context)) {
if(context.length > 0) {
if (options.ids) {
options.ids = [options.name];
}
return instance.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
if (options.data && options.ids) {
var data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name);
options = {data: data};
}
return fn(context, options);
}
});
instance.registerHelper('each', function(context, options) {
if (!options) {
throw new Exception('Must pass iterator to #each');
}
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
var contextPath;
if (options.data && options.ids) {
contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (isFunction(context)) { context = context.call(this); }
if (options.data) {
data = createFrame(options.data);
}
if(context && typeof context === 'object') {
if (isArray(context)) {
for(var j = context.length; i<j; i++) {
if (data) {
data.index = i;
data.first = (i === 0);
data.last = (i === (context.length-1));
if (contextPath) {
data.contextPath = contextPath + i;
}
}
ret = ret + fn(context[i], { data: data });
}
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) {
data.key = key;
data.index = i;
data.first = (i === 0);
if (contextPath) {
data.contextPath = contextPath + key;
}
}
ret = ret + fn(context[key], {data: data});
i++;
}
}
}
}
if(i === 0){
ret = inverse(this);
}
return ret;
});
instance.registerHelper('if', function(conditional, options) {
if (isFunction(conditional)) { conditional = conditional.call(this); }
// Default behavior is to render the positive path if the value is truthy and not empty.
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
instance.registerHelper('unless', function(conditional, options) {
return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
});
instance.registerHelper('with', function(context, options) {
if (isFunction(context)) { context = context.call(this); }
var fn = options.fn;
if (!Utils.isEmpty(context)) {
if (options.data && options.ids) {
var data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]);
options = {data:data};
}
return fn(context, options);
} else {
return options.inverse(this);
}
});
instance.registerHelper('log', function(message, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
instance.log(level, message);
});
instance.registerHelper('lookup', function(obj, field) {
return obj && obj[field];
});
}
var logger = {
methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' },
// State enum
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
level: 3,
// can be overridden in the host environment
log: function(level, message) {
if (logger.level <= level) {
var method = logger.methodMap[level];
if (typeof console !== 'undefined' && console[method]) {
console[method].call(console, message);
}
}
}
};
__exports__.logger = logger;
var log = logger.log;
__exports__.log = log;
var createFrame = function(object) {
var frame = Utils.extend({}, object);
frame._parent = object;
return frame;
};
__exports__.createFrame = createFrame;
return __exports__;
})(__module2__, __module4__);
// handlebars/runtime.js
var __module5__ = (function(__dependency1__, __dependency2__, __dependency3__) {
"use strict";
var __exports__ = {};
var Utils = __dependency1__;
var Exception = __dependency2__;
var COMPILER_REVISION = __dependency3__.COMPILER_REVISION;
var REVISION_CHANGES = __dependency3__.REVISION_CHANGES;
var createFrame = __dependency3__.createFrame;
function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) {
var runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").");
}
}
}
__exports__.checkRevision = checkRevision;// TODO: Remove this line and break up compilePartial
function template(templateSpec, env) {
/* istanbul ignore next */
if (!env) {
throw new Exception("No environment passed to template");
}
if (!templateSpec || !templateSpec.main) {
throw new Exception('Unknown template object: ' + typeof templateSpec);
}
// Note: Using env.VM references rather than local var references throughout this section to allow
// for external users to override these as psuedo-supported APIs.
env.VM.checkRevision(templateSpec.compiler);
var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) {
if (hash) {
context = Utils.extend({}, context, hash);
}
var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths);
if (result == null && env.compile) {
var options = { helpers: helpers, partials: partials, data: data, depths: depths };
partials[name] = env.compile(partial, { data: data !== undefined, compat: templateSpec.compat }, env);
result = partials[name](context, options);
}
if (result != null) {
if (indent) {
var lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) {
break;
}
lines[i] = indent + lines[i];
}
result = lines.join('\n');
}
return result;
} else {
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
}
};
// Just add water
var container = {
escapeExpression: Handlebars.Utils.escapeExpression,
invokePartial: Handlebars.VM.invokePartial,
lookup: function(depths, name) {
var len = depths.length;
for (var i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
return depths[i][name];
}
}
},
lambda: function(current, context) {
return typeof current === 'function' ? current.call(context) : current;
},
escapeExpression: Utils.escapeExpression,
invokePartial: invokePartialWrapper,
fn: function(i) {
return templateSpec[i];
},
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
programWrapper = Handlebars.VM.program(i, fn, data);
program: function(i, data, depths) {
var programWrapper = this.programs[i],
fn = this.fn(i);
if (data || depths) {
programWrapper = program(this, i, fn, data, depths);
} else if (!programWrapper) {
programWrapper = this.programs[i] = Handlebars.VM.program(i, fn);
programWrapper = this.programs[i] = program(this, i, fn);
}
return programWrapper;
},
data: function(data, depth) {
while (data && depth--) {
data = data._parent;
}
return data;
},
merge: function(param, common) {
var ret = param || common;
if (param && common) {
ret = {};
Handlebars.Utils.extend(ret, common);
Handlebars.Utils.extend(ret, param);
if (param && common && (param !== common)) {
ret = Utils.extend({}, common, param);
}
return ret;
},
programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop,
compilerInfo: null
noop: env.VM.noop,
compilerInfo: templateSpec.compiler
};
return function(context, options) {
var ret = function(context, options) {
options = options || {};
var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
var data = options.data;
var compilerInfo = container.compilerInfo || [],
compilerRevision = compilerInfo[0] || 1,
currentRevision = Handlebars.COMPILER_REVISION;
if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) {
var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision],
compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision];
throw "Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").";
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").";
}
ret._setup(options);
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
var depths;
if (templateSpec.useDepths) {
depths = options.depths ? [context].concat(options.depths) : [context];
}
return result;
return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths);
};
},
ret.isTop = true;
programWithDepth: function(i, fn, data /*, $depth */) {
var args = Array.prototype.slice.call(arguments, 3);
ret._setup = function(options) {
if (!options.partial) {
container.helpers = container.merge(options.helpers, env.helpers);
var program = function(context, options) {
if (templateSpec.usePartial) {
container.partials = container.merge(options.partials, env.partials);
}
} else {
container.helpers = options.helpers;
container.partials = options.partials;
}
};
ret._child = function(i, data, depths) {
if (templateSpec.useDepths && !depths) {
throw new Exception('must pass parent depths');
}
return program(container, i, templateSpec[i], data, depths);
};
return ret;
}
__exports__.template = template;function program(container, i, fn, data, depths) {
var prog = function(context, options) {
options = options || {};
return fn.apply(this, [context, options.data || data].concat(args));
return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths));
};
program.program = i;
program.depth = args.length;
return program;
},
program: function(i, fn, data) {
var program = function(context, options) {
options = options || {};
prog.program = i;
prog.depth = depths ? depths.length : 0;
return prog;
}
return fn(context, options.data || data);
};
program.program = i;
program.depth = 0;
return program;
},
noop: function() { return ""; },
invokePartial: function(partial, name, context, helpers, partials, data) {
var options = { helpers: helpers, partials: partials, data: data };
__exports__.program = program;function invokePartial(partial, name, context, helpers, partials, data, depths) {
var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths };
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
throw new Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, options);
} else if (!Handlebars.compile) {
throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
} else {
partials[name] = Handlebars.compile(partial, {data: data !== undefined});
return partials[name](context, options);
}
}
};
Handlebars.template = Handlebars.VM.template;
;
// lib/handlebars/browser-suffix.js
})(Handlebars);
;
__exports__.invokePartial = invokePartial;function noop() { return ""; }
__exports__.noop = noop;function initData(context, data) {
if (!data || !('root' in data)) {
data = data ? createFrame(data) : {};
data.root = context;
}
return data;
}
return __exports__;
})(__module2__, __module4__, __module1__);
// handlebars.runtime.js
var __module0__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__) {
"use strict";
var __exports__;
/*globals Handlebars: true */
var base = __dependency1__;
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
var SafeString = __dependency2__;
var Exception = __dependency3__;
var Utils = __dependency4__;
var runtime = __dependency5__;
// For compatibility and usage outside of module systems, make the Handlebars object a namespace
var create = function() {
var hb = new base.HandlebarsEnvironment();
Utils.extend(hb, base);
hb.SafeString = SafeString;
hb.Exception = Exception;
hb.Utils = Utils;
hb.escapeExpression = Utils.escapeExpression;
hb.VM = runtime;
hb.template = function(spec) {
return runtime.template(spec, hb);
};
return hb;
};
var Handlebars = create();
Handlebars.create = create;
Handlebars['default'] = Handlebars;
__exports__ = Handlebars;
return __exports__;
})(__module1__, __module3__, __module4__, __module2__, __module5__);
return __module0__;
}));
+113 -75
View File
@@ -1,5 +1,5 @@
/*!
* jQuery JavaScript Library v1.11.1
* jQuery JavaScript Library v1.11.2
* http://jquery.com/
*
* Includes Sizzle.js
@@ -9,7 +9,7 @@
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2014-05-01T17:42Z
* Date: 2014-12-17T15:27Z
*/
(function( global, factory ) {
@@ -64,7 +64,7 @@ var support = {};
var
version = "1.11.1",
version = "1.11.2",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
@@ -269,7 +269,8 @@ jQuery.extend({
// parseFloat NaNs numeric-cast false positives (null|true|false|"")
// ...but misinterprets leading-number strings, particularly hex literals ("0x...")
// subtraction forces infinities to NaN
return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0;
// adding 1 corrects loss of precision from parseFloat (#15100)
return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0;
},
isEmptyObject: function( obj ) {
@@ -584,14 +585,14 @@ function isArraylike( obj ) {
}
var Sizzle =
/*!
* Sizzle CSS Selector Engine v1.10.19
* Sizzle CSS Selector Engine v2.2.0-pre
* http://sizzlejs.com/
*
* Copyright 2013 jQuery Foundation, Inc. and other contributors
* Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: 2014-04-18
* Date: 2014-12-16
*/
(function( window ) {
@@ -618,7 +619,7 @@ var i,
contains,
// Instance-specific data
expando = "sizzle" + -(new Date()),
expando = "sizzle" + 1 * new Date(),
preferredDoc = window.document,
dirruns = 0,
done = 0,
@@ -633,7 +634,6 @@ var i,
},
// General-purpose constants
strundefined = typeof undefined,
MAX_NEGATIVE = 1 << 31,
// Instance methods
@@ -643,12 +643,13 @@ var i,
push_native = arr.push,
push = arr.push,
slice = arr.slice,
// Use a stripped-down indexOf if we can't use a native one
indexOf = arr.indexOf || function( elem ) {
// Use a stripped-down indexOf as it's faster than native
// http://jsperf.com/thor-indexof-vs-for/5
indexOf = function( list, elem ) {
var i = 0,
len = this.length;
len = list.length;
for ( ; i < len; i++ ) {
if ( this[i] === elem ) {
if ( list[i] === elem ) {
return i;
}
}
@@ -688,6 +689,7 @@ var i,
")\\)|)",
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
rwhitespace = new RegExp( whitespace + "+", "g" ),
rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
@@ -739,6 +741,14 @@ var i,
String.fromCharCode( high + 0x10000 ) :
// Supplemental Plane codepoint (surrogate pair)
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
},
// Used for iframes
// See setDocument()
// Removing the function wrapper causes a "Permission Denied"
// error in IE
unloadHandler = function() {
setDocument();
};
// Optimize for push.apply( _, NodeList )
@@ -781,19 +791,18 @@ function Sizzle( selector, context, results, seed ) {
context = context || document;
results = results || [];
nodeType = context.nodeType;
if ( typeof selector !== "string" || !selector ||
nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
if ( !selector || typeof selector !== "string" ) {
return results;
}
if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
return [];
}
if ( !seed && documentIsHTML ) {
if ( documentIsHTML && !seed ) {
// Shortcuts
if ( (match = rquickExpr.exec( selector )) ) {
// Try to shortcut find operations when possible (e.g., not under DocumentFragment)
if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) {
// Speed-up: Sizzle("#ID")
if ( (m = match[1]) ) {
if ( nodeType === 9 ) {
@@ -825,7 +834,7 @@ function Sizzle( selector, context, results, seed ) {
return results;
// Speed-up: Sizzle(".CLASS")
} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
} else if ( (m = match[3]) && support.getElementsByClassName ) {
push.apply( results, context.getElementsByClassName( m ) );
return results;
}
@@ -835,7 +844,7 @@ function Sizzle( selector, context, results, seed ) {
if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
nid = old = expando;
newContext = context;
newSelector = nodeType === 9 && selector;
newSelector = nodeType !== 1 && selector;
// qSA works strangely on Element-rooted queries
// We can work around this by specifying an extra ID on the root
@@ -1022,7 +1031,7 @@ function createPositionalPseudo( fn ) {
* @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
*/
function testContext( context ) {
return context && typeof context.getElementsByTagName !== strundefined && context;
return context && typeof context.getElementsByTagName !== "undefined" && context;
}
// Expose support vars for convenience
@@ -1046,9 +1055,8 @@ isXML = Sizzle.isXML = function( elem ) {
* @returns {Object} Returns the current document
*/
setDocument = Sizzle.setDocument = function( node ) {
var hasCompare,
doc = node ? node.ownerDocument || node : preferredDoc,
parent = doc.defaultView;
var hasCompare, parent,
doc = node ? node.ownerDocument || node : preferredDoc;
// If no document and documentElement is available, return
if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
@@ -1058,9 +1066,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// Set our document
document = doc;
docElem = doc.documentElement;
// Support tests
documentIsHTML = !isXML( doc );
parent = doc.defaultView;
// Support: IE>8
// If iframe document is assigned to "document" variable and if iframe has been reloaded,
@@ -1069,21 +1075,22 @@ setDocument = Sizzle.setDocument = function( node ) {
if ( parent && parent !== parent.top ) {
// IE11 does not have attachEvent, so all must suffer
if ( parent.addEventListener ) {
parent.addEventListener( "unload", function() {
setDocument();
}, false );
parent.addEventListener( "unload", unloadHandler, false );
} else if ( parent.attachEvent ) {
parent.attachEvent( "onunload", function() {
setDocument();
});
parent.attachEvent( "onunload", unloadHandler );
}
}
/* Support tests
---------------------------------------------------------------------- */
documentIsHTML = !isXML( doc );
/* Attributes
---------------------------------------------------------------------- */
// Support: IE<8
// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
// Verify that getAttribute really returns attributes and not properties
// (excepting IE8 booleans)
support.attributes = assert(function( div ) {
div.className = "i";
return !div.getAttribute("className");
@@ -1098,17 +1105,8 @@ setDocument = Sizzle.setDocument = function( node ) {
return !div.getElementsByTagName("*").length;
});
// Check if getElementsByClassName can be trusted
support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {
div.innerHTML = "<div class='a'></div><div class='a i'></div>";
// Support: Safari<4
// Catch class over-caching
div.firstChild.className = "i";
// Support: Opera<10
// Catch gEBCN failure to find non-leading classes
return div.getElementsByClassName("i").length === 2;
});
// Support: IE<9
support.getElementsByClassName = rnative.test( doc.getElementsByClassName );
// Support: IE<10
// Check if getElementById returns elements by name
@@ -1122,7 +1120,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// ID find and filter
if ( support.getById ) {
Expr.find["ID"] = function( id, context ) {
if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
var m = context.getElementById( id );
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
@@ -1143,7 +1141,7 @@ setDocument = Sizzle.setDocument = function( node ) {
Expr.filter["ID"] = function( id ) {
var attrId = id.replace( runescape, funescape );
return function( elem ) {
var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
return node && node.value === attrId;
};
};
@@ -1152,14 +1150,20 @@ setDocument = Sizzle.setDocument = function( node ) {
// Tag
Expr.find["TAG"] = support.getElementsByTagName ?
function( tag, context ) {
if ( typeof context.getElementsByTagName !== strundefined ) {
if ( typeof context.getElementsByTagName !== "undefined" ) {
return context.getElementsByTagName( tag );
// DocumentFragment nodes don't have gEBTN
} else if ( support.qsa ) {
return context.querySelectorAll( tag );
}
} :
function( tag, context ) {
var elem,
tmp = [],
i = 0,
// By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
results = context.getElementsByTagName( tag );
// Filter out possible comments
@@ -1177,7 +1181,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// Class
Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
if ( documentIsHTML ) {
return context.getElementsByClassName( className );
}
};
@@ -1206,13 +1210,15 @@ setDocument = Sizzle.setDocument = function( node ) {
// setting a boolean content attribute,
// since its presence should be enough
// http://bugs.jquery.com/ticket/12359
div.innerHTML = "<select msallowclip=''><option selected=''></option></select>";
docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" +
"<select id='" + expando + "-\f]' msallowcapture=''>" +
"<option selected=''></option></select>";
// Support: IE8, Opera 11-12.16
// Nothing should be selected when empty strings follow ^= or $= or *=
// The test attribute must be unknown in Opera but "safe" for WinRT
// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
if ( div.querySelectorAll("[msallowclip^='']").length ) {
if ( div.querySelectorAll("[msallowcapture^='']").length ) {
rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
}
@@ -1222,12 +1228,24 @@ setDocument = Sizzle.setDocument = function( node ) {
rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
}
// Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+
if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
rbuggyQSA.push("~=");
}
// Webkit/Opera - :checked should return selected option elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
// IE8 throws error here and will not see later tests
if ( !div.querySelectorAll(":checked").length ) {
rbuggyQSA.push(":checked");
}
// Support: Safari 8+, iOS 8+
// https://bugs.webkit.org/show_bug.cgi?id=136851
// In-page `selector#id sibing-combinator selector` fails
if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) {
rbuggyQSA.push(".#.+[+~]");
}
});
assert(function( div ) {
@@ -1344,7 +1362,7 @@ setDocument = Sizzle.setDocument = function( node ) {
// Maintain original order
return sortInput ?
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
0;
}
@@ -1371,7 +1389,7 @@ setDocument = Sizzle.setDocument = function( node ) {
aup ? -1 :
bup ? 1 :
sortInput ?
( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
0;
// If the nodes are siblings, we can do a quick check
@@ -1434,7 +1452,7 @@ Sizzle.matchesSelector = function( elem, expr ) {
elem.document && elem.document.nodeType !== 11 ) {
return ret;
}
} catch(e) {}
} catch (e) {}
}
return Sizzle( expr, document, null, [ elem ] ).length > 0;
@@ -1653,7 +1671,7 @@ Expr = Sizzle.selectors = {
return pattern ||
(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
classCache( className, function( elem ) {
return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" );
});
},
@@ -1675,7 +1693,7 @@ Expr = Sizzle.selectors = {
operator === "^=" ? check && result.indexOf( check ) === 0 :
operator === "*=" ? check && result.indexOf( check ) > -1 :
operator === "$=" ? check && result.slice( -check.length ) === check :
operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
false;
};
@@ -1795,7 +1813,7 @@ Expr = Sizzle.selectors = {
matched = fn( seed, argument ),
i = matched.length;
while ( i-- ) {
idx = indexOf.call( seed, matched[i] );
idx = indexOf( seed, matched[i] );
seed[ idx ] = !( matches[ idx ] = matched[i] );
}
}) :
@@ -1834,6 +1852,8 @@ Expr = Sizzle.selectors = {
function( elem, context, xml ) {
input[0] = elem;
matcher( input, null, xml, results );
// Don't keep the element (issue #299)
input[0] = null;
return !results.pop();
};
}),
@@ -1845,6 +1865,7 @@ Expr = Sizzle.selectors = {
}),
"contains": markFunction(function( text ) {
text = text.replace( runescape, funescape );
return function( elem ) {
return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
};
@@ -2266,7 +2287,7 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS
i = matcherOut.length;
while ( i-- ) {
if ( (elem = matcherOut[i]) &&
(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
(temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) {
seed[temp] = !(results[temp] = elem);
}
@@ -2301,13 +2322,16 @@ function matcherFromTokens( tokens ) {
return elem === checkContext;
}, implicitRelative, true ),
matchAnyContext = addCombinator( function( elem ) {
return indexOf.call( checkContext, elem ) > -1;
return indexOf( checkContext, elem ) > -1;
}, implicitRelative, true ),
matchers = [ function( elem, context, xml ) {
return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
(checkContext = context).nodeType ?
matchContext( elem, context, xml ) :
matchAnyContext( elem, context, xml ) );
// Avoid hanging onto element (issue #299)
checkContext = null;
return ret;
} ];
for ( ; i < len; i++ ) {
@@ -2557,7 +2581,7 @@ select = Sizzle.select = function( selector, context, results, seed ) {
// Sort stability
support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
// Support: Chrome<14
// Support: Chrome 14-35+
// Always assume duplicates if they aren't passed to the comparison function
support.detectDuplicates = !!hasDuplicate;
@@ -6115,7 +6139,14 @@ var getStyles, curCSS,
if ( window.getComputedStyle ) {
getStyles = function( elem ) {
return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
// Support: IE<=11+, Firefox<=30+ (#15098, #14150)
// IE throws on elements created in popups
// FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
if ( elem.ownerDocument.defaultView.opener ) {
return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
}
return window.getComputedStyle( elem, null );
};
curCSS = function( elem, name, computed ) {
@@ -6363,6 +6394,8 @@ function addGetHookIf( conditionFn, hookFn ) {
reliableMarginRightVal =
!parseFloat( ( window.getComputedStyle( contents, null ) || {} ).marginRight );
div.removeChild( contents );
}
// Support: IE8
@@ -9070,7 +9103,8 @@ jQuery.extend({
}
// We can fire global events as of now if asked to
fireGlobals = s.global;
// Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118)
fireGlobals = jQuery.event && s.global;
// Watch for a new set of requests
if ( fireGlobals && jQuery.active++ === 0 ) {
@@ -9329,13 +9363,6 @@ jQuery.each( [ "get", "post" ], function( i, method ) {
};
});
// Attach a bunch of functions for handling common AJAX events
jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) {
jQuery.fn[ type ] = function( fn ) {
return this.on( type, fn );
};
});
jQuery._evalUrl = function( url ) {
return jQuery.ajax({
@@ -9561,8 +9588,9 @@ var xhrId = 0,
// Support: IE<10
// Open requests must be manually aborted on unload (#5280)
if ( window.ActiveXObject ) {
jQuery( window ).on( "unload", function() {
// See https://support.microsoft.com/kb/2856746 for more info
if ( window.attachEvent ) {
window.attachEvent( "onunload", function() {
for ( var key in xhrCallbacks ) {
xhrCallbacks[ key ]( undefined, true );
}
@@ -9996,6 +10024,16 @@ jQuery.fn.load = function( url, params, callback ) {
// Attach a bunch of functions for handling common AJAX events
jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) {
jQuery.fn[ type ] = function( fn ) {
return this.on( type, fn );
};
});
jQuery.expr.filters.animated = function( elem ) {
return jQuery.grep(jQuery.timers, function( fn ) {
return elem === fn.elem;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+80 -178
View File
@@ -3,12 +3,32 @@
MIT license
*/
;(function(window, undefined) {
(function(window, undefined) {
var TraceKit = {};
var _oldTraceKit = window.TraceKit;
// global reference to slice
var _slice = [].slice;
var UNKNOWN_FUNCTION = '?';
/**
* _has, a better form of hasOwnProperty
* Example: _has(MainHostObject, property) === true/false
*
* @param {Object} host object to check property
* @param {string} key to check
*/
function _has(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function _isUndefined(what) {
return typeof what === 'undefined';
}
/**
* TraceKit.noConflict: Export TraceKit out to another variable
* Example: var TK = TraceKit.noConflict()
@@ -19,14 +39,22 @@ TraceKit.noConflict = function noConflict() {
};
/**
* TraceKit._has, a better form of hasOwnProperty
* Example: TraceKit._has(MainHostObject, property) === true/false
* TraceKit.wrap: Wrap any function in a TraceKit reporter
* Example: func = TraceKit.wrap(func);
*
* @param {Object} host object to check property
* @param {string} key to check
* @param {Function} func Function to be wrapped
* @return {Function} The wrapped func
*/
TraceKit._has = function _has(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
TraceKit.wrap = function traceKitWrapper(func) {
function wrapped() {
try {
return func.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
}
return wrapped;
};
/**
@@ -78,6 +106,7 @@ TraceKit.report = (function reportModuleWrapper() {
* @param {Function} handler
*/
function subscribe(handler) {
installGlobalHandler();
handlers.push(handler);
}
@@ -103,9 +132,9 @@ TraceKit.report = (function reportModuleWrapper() {
return;
}
for (var i in handlers) {
if (TraceKit._has(handlers, i)) {
if (_has(handlers, i)) {
try {
handlers[i].apply(null, [stack].concat(Array.prototype.slice.call(arguments, 2)));
handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2)));
} catch (inner) {
exception = inner;
}
@@ -117,7 +146,7 @@ TraceKit.report = (function reportModuleWrapper() {
}
}
var _oldOnerrorHandler = window.onerror;
var _oldOnerrorHandler, _onErrorHandlerInstalled;
/**
* Ensures all global unhandled exceptions are recorded.
@@ -127,7 +156,7 @@ TraceKit.report = (function reportModuleWrapper() {
* @param {(number|string)} lineNo The line number at which the error
* occurred.
*/
window.onerror = function traceKitWindowOnError(message, url, lineNo) {
function traceKitWindowOnError(message, url, lineNo) {
var stack = null;
if (lastExceptionStack) {
@@ -158,14 +187,24 @@ TraceKit.report = (function reportModuleWrapper() {
}
return false;
};
}
function installGlobalHandler ()
{
if (_onErrorHandlerInstalled === true) {
return;
}
_oldOnerrorHandler = window.onerror;
window.onerror = traceKitWindowOnError;
_onErrorHandlerInstalled = true;
}
/**
* Reports an unhandled Error to TraceKit.
* @param {Error} ex
*/
function report(ex) {
var args = Array.prototype.slice.call(arguments, 1);
var args = _slice.call(arguments, 1);
if (lastExceptionStack) {
if (lastException === ex) {
return; // already caught by an inner catch block, ignore
@@ -283,29 +322,16 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
return '';
}
try {
var XMLHttpRequestWrapper;
var getXHR = function() {
try {
return new window.XMLHttpRequest();
} catch (e) {
// explicitly bubble up the exception if not found
return new window.ActiveXObject('Microsoft.XMLHTTP');
}
};
if (typeof (XMLHttpRequest) === 'undefined') { // IE 5.x-6.x:
XMLHttpRequestWrapper = function IEXMLHttpRequestSub() {
try {
return new ActiveXObject('Msxml2.XMLHTTP.6.0');
} catch (e) {}
try {
return new ActiveXObject('Msxml2.XMLHTTP.3.0');
} catch (e) {}
try {
return new ActiveXObject('Msxml2.XMLHTTP');
} catch (e) {}
try {
return new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
throw new Error('No XHR.');
};
} else {
XMLHttpRequestWrapper = XMLHttpRequest;
}
var request = new XMLHttpRequestWrapper();
var request = getXHR();
request.open('GET', url, false);
request.send('');
return request.responseText;
@@ -320,16 +346,14 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
* @return {Array.<string>} Source contents.
*/
function getSource(url) {
if (!TraceKit._has(sourceCache, url)) {
if (!_has(sourceCache, url)) {
// URL needs to be able to fetched within the acceptable domain. Otherwise,
// cross-domain errors will be triggered.
var source;
var source = '';
if (url.indexOf(document.domain) !== -1) {
source = loadSource(url);
} else {
source = [];
}
sourceCache[url] = source.length ? source.split('\n') : [];
sourceCache[url] = source ? source.split('\n') : [];
}
return sourceCache[url];
@@ -352,7 +376,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
m;
if (!source.length) {
return '?';
return UNKNOWN_FUNCTION;
}
// Walk backwards from the first line in the function until we find the line which
@@ -360,7 +384,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
for (var i = 0; i < maxLines; ++i) {
line = source[lineNo - i] + line;
if (line !== undefined) {
if (!_isUndefined(line)) {
if ((m = reGuessFunction.exec(line))) {
return m[1];
} else if ((m = reFunctionArgNames.exec(line))) {
@@ -369,7 +393,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
}
}
return '?';
return UNKNOWN_FUNCTION;
}
/**
@@ -399,7 +423,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
line -= 1; // convert to 0-based index
for (var i = start; i < end; ++i) {
if (typeof (source[i]) !== 'undefined') {
if (!_isUndefined(source[i])) {
context.push(source[i]);
}
}
@@ -594,7 +618,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
return null;
}
var chrome = /^\s*at (?:((?:\[object object\])?\S+) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i,
var chrome = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i,
gecko = /^\s*(\S*)(?:\((.*?)\))?@((?:file|http|https).*?):(\d+)(?::(\d+))?\s*$/i,
lines = ex.stack.split('\n'),
stack = [],
@@ -606,7 +630,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
if ((parts = gecko.exec(lines[i]))) {
element = {
'url': parts[3],
'func': parts[1] || '?',
'func': parts[1] || UNKNOWN_FUNCTION,
'args': parts[2] ? parts[2].split(',') : '',
'line': +parts[4],
'column': parts[5] ? +parts[5] : null
@@ -614,7 +638,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
} else if ((parts = chrome.exec(lines[i]))) {
element = {
'url': parts[2],
'func': parts[1] || '?',
'func': parts[1] || UNKNOWN_FUNCTION,
'line': +parts[3],
'column': parts[4] ? +parts[4] : null
};
@@ -750,7 +774,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
source;
for (i in scripts) {
if (TraceKit._has(scripts, i) && !scripts[i].src) {
if (_has(scripts, i) && !scripts[i].src) {
inlineScriptBlocks.push(scripts[i]);
}
}
@@ -904,7 +928,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
item = {
'url': null,
'func': '?',
'func': UNKNOWN_FUNCTION,
'line': null,
'column': null
};
@@ -919,7 +943,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
item.url = source.url;
item.line = source.line;
if (item.func === '?') {
if (item.func === UNKNOWN_FUNCTION) {
item.func = guessFunctionName(item.url, item.line);
}
@@ -1029,8 +1053,6 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
} catch (ex) {
return computeStackTrace(ex, depth + 1);
}
return null;
}
computeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement;
@@ -1045,22 +1067,15 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
* Extends support for global error handling for asynchronous browser
* functions. Adopted from Closure Library's errorhandler.js
*/
(function extendToAsynchronousCallbacks(w) {
(function extendToAsynchronousCallbacks() {
var _helper = function _helper(fnName) {
var originalFn = w[fnName];
w[fnName] = function traceKitAsyncExtension() {
var originalFn = window[fnName];
window[fnName] = function traceKitAsyncExtension() {
// Make a copy of the arguments
var args = Array.prototype.slice.call(arguments, 0);
var args = _slice.call(arguments);
var originalCallback = args[0];
if (typeof (originalCallback) === 'function') {
args[0] = function traceKitArgsZero() {
try {
originalCallback.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
args[0] = TraceKit.wrap(originalCallback);
}
// IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
// also only supports 2 argument and doesn't care what "this" is, so we
@@ -1075,120 +1090,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
_helper('setTimeout');
_helper('setInterval');
}(window));
/**
* Extended support for backtraces and global error handling for most
* asynchronous jQuery functions.
*/
(function traceKitAsyncForjQuery($) {
// quit if jQuery isn't on the page
if (!$) {
return;
}
var _oldEventAdd = $.event.add;
$.event.add = function traceKitEventAdd(elem, types, handler, data, selector) {
var _handler;
if (handler.handler) {
_handler = handler.handler;
handler.handler = function traceKitHandler() {
try {
return _handler.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
} else {
_handler = handler;
handler = function apply_handler() {
try {
return _handler.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
}
// If the handler we are attaching doesn’t have the same guid as
// the original, it will never be removed when someone tries to
// unbind the original function later. Technically as a result of
// this our guids are no longer globally unique, but whatever, that
// never hurt anybody RIGHT?!
if (_handler.guid) {
handler.guid = _handler.guid;
} else {
handler.guid = _handler.guid = $.guid++;
}
return _oldEventAdd.call(this, elem, types, handler, data, selector);
};
var _oldReady = $.fn.ready;
$.fn.ready = function traceKitjQueryReadyWrapper(fn) {
var _fn = function () {
try {
return fn.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
return _oldReady.call(this, _fn);
};
var _oldAjax = $.ajax;
$.ajax = function traceKitAjaxWrapper(s) {
if ($.isFunction(s.complete)) {
var _oldComplete = s.complete;
s.complete = function traceKitjQueryComplete() {
try {
return _oldComplete.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
}
if ($.isFunction(s.error)) {
var _oldError = s.error;
s.error = function traceKitjQueryError() {
try {
return _oldError.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
}
if ($.isFunction(s.success)) {
var _oldSuccess = s.success;
s.success = function traceKitjQuerySuccess() {
try {
return _oldSuccess.apply(this, arguments);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
}
try {
return _oldAjax.call(this, s);
} catch (e) {
TraceKit.report(e);
throw e;
}
};
}(window.jQuery));
}());
//Default options:
if (!TraceKit.remoteFetching) {
-1
View File
@@ -1159,7 +1159,6 @@
return value;
};
// Predicate-generating functions. Often useful outside of Underscore.
_.constant = function(value) {
return function() {
return value;
@@ -75,13 +75,17 @@ var HistoryPanelEdit = _super.extend(
/** Override to handle history as drag-drop target */
_setUpListeners : function(){
_super.prototype._setUpListeners.call( this );
var panel = this;
_super.prototype._setUpListeners.call( panel );
this.on( 'drop', function( ev, data ){
this.dataDropped( data );
panel.on( 'drop', function( ev, data ){
panel.dataDropped( data );
// remove the drop target
this.dropTargetOff();
panel.dropTargetOff();
});
panel.on( 'view:attached view:removed', function(){
panel._renderCounts();
}, panel );
},
// ------------------------------------------------------------------------ listeners
@@ -332,7 +336,10 @@ var HistoryPanelEdit = _super.extend(
// override to control where the view is added, how/whether it's rendered
panel.views.unshift( view );
panel.$list().prepend( view.render( 0 ).$el.hide() );
view.$el.slideDown( panel.fxSpeed );
panel.trigger( 'view:attached', view );
view.$el.slideDown( panel.fxSpeed, function(){
panel.trigger( 'view:attached:rendered' );
});
},
/** In this override, add purgeAllowed and whether tags/annotation editors should be shown */
@@ -199,7 +199,7 @@ define([], function() {
*/
var Jstree = Backbone.Model.extend({
urlRoot: '/api/ftp_files'
urlRoot: '/api/remote_files'
});
return {
+10 -5
View File
@@ -477,7 +477,10 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
// override to control where the view is added, how/whether it's rendered
panel.views.push( view );
panel.$list().append( view.render( 0 ).$el.hide() );
view.$el.slideDown( panel.fxSpeed );
panel.trigger( 'view:attached', view );
view.$el.slideDown( panel.fxSpeed, function(){
panel.trigger( 'view:attached:rendered' );
});
},
/** Remove a view from the panel (if found) */
@@ -487,6 +490,7 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
view = panel.viewFromModel( model );
if( !view ){ return undefined; }
panel.views = _.without( panel.views, view );
panel.trigger( 'view:removed', view );
// potentially show the empty message if no views left
// use anonymous queue here - since remove can happen multiple times
@@ -494,6 +498,7 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
function( next ){ view.$el.fadeOut( panel.fxSpeed, next ); },
function( next ){
view.remove();
panel.trigger( 'view:removed:rendered' );
if( !panel.views.length ){
panel._renderEmptyMessage().fadeIn( panel.fxSpeed, next );
} else {
@@ -872,10 +877,10 @@ var ModelListPanel = ListPanel.extend({
this.log( this + '._setUpModelListeners', this.model );
// bounce model errors up to the panel
this.model.on( 'error', function(){
//TODO: namespace?
//var args = Array.prototype.slice.call( arguments, 0 );
//args[0] = 'model:' + args[0];
this.trigger.apply( panel, arguments );
var args = Array.prototype.slice.call( arguments, 0 );
//args.unshift( 'model:error' );
args.unshift( 'error' );
this.trigger.apply( this, args );
}, this );
return this;
},
+66 -61
View File
@@ -2,7 +2,7 @@
* Model, view, and controller objects for Galaxy tools and tool panel.
*/
define( ["libs/underscore", "viz/trackster/util", "mvc/data" ],
define( ["libs/underscore", "viz/trackster/util", "mvc/data", "libs/lunr" ],
function(_, util, data) {
/**
@@ -14,7 +14,7 @@ var VisibilityMixin = {
show: function() {
this.set("hidden", false);
},
hide: function() {
this.set("hidden", true);
},
@@ -22,7 +22,7 @@ var VisibilityMixin = {
toggle: function() {
this.set("hidden", !this.get("hidden"));
},
is_visible: function() {
return !this.attributes.hidden;
}
@@ -88,7 +88,7 @@ var FloatToolParameter = IntegerToolParameter.extend({
}
});
/**
/**
* A select tool parameter.
*/
var SelectToolParameter = ToolParameter.extend({
@@ -157,7 +157,7 @@ var Tool = Backbone.Model.extend({
});
tool.get('inputs').remove(incompatible_inputs);
},
/**
* Returns object copy, optionally including only inputs that can be sampled.
*/
@@ -177,12 +177,12 @@ var Tool = Backbone.Model.extend({
return copy;
},
apply_search_results: function(results) {
( _.indexOf(results, this.attributes.id) !== -1 ? this.show() : this.hide() );
return this.is_visible();
},
/**
* Set a tool input's value.
*/
@@ -191,24 +191,24 @@ var Tool = Backbone.Model.extend({
return input.get('name') === name;
}).set('value', value);
},
/**
* Set many input values at once.
*/
set_input_values: function(inputs_dict) {
var self = this;
_.each(_.keys(inputs_dict), function(input_name) {
self.set_input_value(input_name, inputs_dict[input_name]);
self.set_input_value(input_name, inputs_dict[input_name]);
});
},
/**
* Run tool; returns a Deferred that resolves to the tool's output(s).
*/
run: function() {
return this._run();
},
/**
* Rerun tool using regions and a target dataset.
*/
@@ -230,7 +230,7 @@ var Tool = Backbone.Model.extend({
});
return input_dict;
},
/**
* Run tool; returns a Deferred that resolves to the tool's output(s).
* NOTE: this method is a helper method and should not be called directly.
@@ -259,7 +259,7 @@ var Tool = Backbone.Model.extend({
return response !== "pending";
}
});
// Run job and resolve run_deferred to tool outputs.
$.when(ss_deferred.go()).then(function(result) {
run_deferred.resolve(new data.DatasetCollection().reset(result));
@@ -296,20 +296,20 @@ var ToolSection = Backbone.Model.extend({
elems: [],
open: false
},
clear_search_results: function() {
_.each(this.attributes.elems, function(elt) {
elt.show();
});
this.show();
this.set("open", false);
},
apply_search_results: function(results) {
var all_hidden = true,
cur_label;
_.each(this.attributes.elems, function(elt) {
_.each(this.attributes.elems, function(elt) {
if (elt instanceof ToolSectionLabel) {
cur_label = elt;
cur_label.hide();
@@ -323,7 +323,7 @@ var ToolSection = Backbone.Model.extend({
}
}
});
if (all_hidden) {
this.hide();
}
@@ -340,6 +340,11 @@ _.extend(ToolSection.prototype, VisibilityMixin);
* indicates that query was not run; if not null, results are from search using
* query.
*/
/**
* TODO: Integrate lunr search here with tools from API instead of making
* repeated requests.
*/
var ToolSearch = Backbone.Model.extend({
defaults: {
search_hint_string: "search tools",
@@ -353,23 +358,23 @@ var ToolSearch = Backbone.Model.extend({
// ESC (27) will clear the input field and tool search filters
clear_key: 27
},
initialize: function() {
this.on("change:query", this.do_search);
},
/**
* Do the search and update the results.
*/
do_search: function() {
var query = this.attributes.query;
// If query is too short, do not search.
if (query.length < this.attributes.min_chars_for_search) {
this.set("results", null);
return;
}
// Do search via AJAX.
var q = query + '*';
// Stop previous ajax-request
@@ -388,12 +393,12 @@ var ToolSearch = Backbone.Model.extend({
}, "json" );
}, 200 );
},
clear_search: function() {
this.set("query", "");
this.set("results", null);
}
});
_.extend(ToolSearch.prototype, VisibilityMixin);
@@ -433,7 +438,7 @@ var ToolPanel = Backbone.Model.extend({
return new ToolSectionLabel(elt_dict);
}
};
return _.map(response, parse_elt);
},
@@ -448,14 +453,14 @@ var ToolPanel = Backbone.Model.extend({
}
});
},
apply_search_results: function() {
var results = this.get('tool_search').get('results');
if (results === null) {
this.clear_search_results();
return;
}
var cur_label = null;
this.get('layout').each(function(panel_elt) {
if (panel_elt instanceof ToolSectionLabel) {
@@ -479,13 +484,13 @@ var ToolPanel = Backbone.Model.extend({
});
/**
* View classes for Galaxy tools and tool panel.
*
* View classes for Galaxy tools and tool panel.
*
* Views use precompiled Handlebars templates for rendering. Views update as needed
* based on (a) model/collection events and (b) user interactions; in this sense,
* they are controllers are well and the HTML is the real view in the MVC architecture.
*/
/**
* Base view that handles visibility based on model's hidden attribute.
*/
@@ -496,9 +501,9 @@ var BaseView = Backbone.View.extend({
},
update_visible: function() {
( this.model.attributes.hidden ? this.$el.hide() : this.$el.show() );
}
}
});
/**
* Link to a tool.
*/
@@ -509,7 +514,7 @@ var ToolLinkView = BaseView.extend({
// create element
var $link = $('<div/>');
$link.append(Handlebars.templates.tool_link(this.model.toJSON()));
// open upload dialog for upload tool
if (this.model.id === 'upload1') {
$link.find('a').on('click', function(e) {
@@ -517,7 +522,7 @@ var ToolLinkView = BaseView.extend({
Galaxy.upload.show();
});
}
// add element
this.$el.append($link);
return this;
@@ -543,7 +548,7 @@ var ToolSectionLabelView = BaseView.extend({
var ToolSectionView = BaseView.extend({
tagName: 'div',
className: 'toolSectionWrapper',
initialize: function() {
BaseView.prototype.initialize.call(this);
this.model.on("change:open", this.update_open, this);
@@ -552,7 +557,7 @@ var ToolSectionView = BaseView.extend({
render: function() {
// Build using template.
this.$el.append( Handlebars.templates.panel_section(this.model.toJSON()) );
// Add tools to section.
var section_body = this.$el.find(".toolSectionBody");
_.each(this.model.attributes.elems, function(elt) {
@@ -572,25 +577,25 @@ var ToolSectionView = BaseView.extend({
});
return this;
},
events: {
'click .toolSectionTitle > a': 'toggle'
},
/**
/**
* Toggle visibility of tool section.
*/
toggle: function() {
this.model.set("open", !this.model.attributes.open);
},
/**
* Update whether section is open or close.
*/
update_open: function() {
(this.model.attributes.open ?
this.$el.children(".toolSectionBody").slideDown("fast") :
this.$el.children(".toolSectionBody").slideUp("fast")
this.$el.children(".toolSectionBody").slideUp("fast")
);
}
});
@@ -599,13 +604,13 @@ var ToolSearchView = Backbone.View.extend({
tagName: 'div',
id: 'tool-search',
className: 'bar',
events: {
'click': 'focus_and_select',
'keyup :input': 'query_changed',
'click #search-clear-btn': 'clear'
},
render: function() {
this.$el.append( Handlebars.templates.tool_search(this.model.toJSON()) );
if (!this.model.is_visible()) {
@@ -614,18 +619,18 @@ var ToolSearchView = Backbone.View.extend({
this.$el.find('[title]').tooltip();
return this;
},
focus_and_select: function() {
this.$el.find(":input").focus().select();
},
clear: function() {
this.model.clear_search();
this.$el.find(":input").val(this.model.attributes.search_hint_string);
this.focus_and_select();
return false;
},
query_changed: function( evData ) {
// check for the 'clear key' (ESC) first
if( ( this.model.attributes.clear_key ) &&
@@ -644,22 +649,22 @@ var ToolSearchView = Backbone.View.extend({
var ToolPanelView = Backbone.View.extend({
tagName: 'div',
className: 'toolMenu',
/**
* Set up view.
*/
initialize: function() {
this.model.get('tool_search').on("change:results", this.handle_search_results, this);
},
render: function() {
var self = this;
// Render search.
var search_view = new ToolSearchView( { model: this.model.get('tool_search') } );
search_view.render();
self.$el.append(search_view.$el);
// Render panel.
this.model.get('layout').each(function(panel_elt) {
if (panel_elt instanceof ToolSection) {
@@ -678,20 +683,20 @@ var ToolPanelView = Backbone.View.extend({
self.$el.append(label_view.$el);
}
});
// Setup tool link click eventing.
self.$el.find("a.tool-link").click(function(e) {
// Tool id is always the first class.
var
var
tool_id = $(this).attr('class').split(/\s+/)[0],
tool = self.model.get('tools').get(tool_id);
self.trigger("tool_link_click", e, tool);
});
return this;
},
handle_search_results: function() {
var results = this.model.get('tool_search').get('results');
if (results && results.length === 0) {
@@ -708,7 +713,7 @@ var ToolPanelView = Backbone.View.extend({
*/
var ToolFormView = Backbone.View.extend({
className: 'toolForm',
render: function() {
this.$el.children().remove();
this.$el.append( Handlebars.templates.tool_form(this.model.toJSON()) );
@@ -720,22 +725,22 @@ var ToolFormView = Backbone.View.extend({
*/
var IntegratedToolMenuAndView = Backbone.View.extend({
className: 'toolMenuAndView',
initialize: function() {
this.tool_panel_view = new ToolPanelView({collection: this.collection});
this.tool_form_view = new ToolFormView();
},
render: function() {
// Render and append tool panel.
this.tool_panel_view.render();
this.tool_panel_view.$el.css("float", "left");
this.$el.append(this.tool_panel_view.$el);
// Append tool form view.
this.tool_form_view.$el.hide();
this.$el.append(this.tool_form_view.$el);
// On tool link click, show tool.
var self = this;
this.tool_panel_view.on("tool_link_click", function(e, tool) {
@@ -745,7 +750,7 @@ var IntegratedToolMenuAndView = Backbone.View.extend({
self.show_tool(tool);
});
},
/**
* Fetch and display tool.
*/
@@ -0,0 +1,347 @@
/**
This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules.
*/
define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
'mvc/citation/citation-model', 'mvc/citation/citation-view',
'mvc/tools', 'mvc/tools/tools-template', 'mvc/tools/tools-content', 'mvc/tools/tools-section', 'mvc/tools/tools-tree'],
function(Utils, Deferred, Portlet, Ui, CitationModel, CitationView,
Tools, ToolTemplate, ToolContent, ToolSection, ToolTree) {
// create form view
return Backbone.View.extend({
// initialize
initialize: function(options) {
// options
this.optionsDefault = {
// uses dynamic fields instead of text fields
is_dynamic : true,
// shows form in narrow view mode
narrow : false,
// shows errors on start
initial_errors : false
};
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// log options
console.debug(this.options);
// link galaxy modal or create one
var galaxy = parent.Galaxy;
if (galaxy && galaxy.modal) {
this.modal = galaxy.modal;
} else {
this.modal = new Ui.Modal.View();
}
// check if the user is an admin
if (galaxy && galaxy.currUser) {
this.is_admin = galaxy.currUser.get('is_admin');
} else {
this.is_admin = false;
}
// link container
this.container = this.options.container || 'body';
// create deferred processing queue handler
// this handler reduces the number of requests to the api by filtering redundant requests
this.deferred = new Deferred();
// set element
this.setElement('<div/>');
// add to main element
$(this.container).append(this.$el);
// build this form
this.build(this.options);
},
/** Main tool form build function. This function is called once a new model is available.
*/
build: function(options) {
// link this
var self = this;
// reset events
this.off('refresh');
this.off('reset');
// reset field list, which contains the input field elements
this.field_list = {};
// reset sequential input definition list, which contains the input definitions as provided from the api
this.input_list = {};
// reset input element list, which contains the dom elements of each input element (includes also the input field)
this.element_list = {};
// creates a tree/json data structure from the input form
this.tree = new ToolTree(this);
// request history content and build form
this.content = new ToolContent(this);
// update model data
self.options.inputs = options && options.inputs;
// create ui elements
this._renderForm(options);
// rebuild the underlying data structure
this.tree.finalize();
// show errors on startup
if (options.initial_errors) {
this._errors(options);
}
// add refresh listener
this.on('refresh', function() {
// by using/resetting the deferred ajax queue the number of redundant calls is reduced
self.deferred.reset();
self.deferred.execute(function(){self._updateModel()});
});
// add reset listener
this.on('reset', function() {
for (var i in this.element_list) {
this.element_list[i].reset();
}
});
// refresh
this.trigger('refresh');
},
/** Shows the final message (usually upon successful job submission)
*/
reciept: function($el) {
$(this.container).empty();
$(this.container).append($el);
},
/** Highlight and scroll to input element (currently only used for error notifications)
*/
highlight: function (input_id, message, silent) {
// get input field
var input_element = this.element_list[input_id];
// check input element
if (input_element) {
// mark error
input_element.error(message || 'Please verify this parameter.');
// scroll to first input element
if (!silent) {
$(this.container).animate({
scrollTop: input_element.$el.offset().top - 20
}, 500);
}
}
},
/** Highlights errors
*/
_errors: function(options) {
// hide previous error statements
this.trigger('reset');
// highlight all errors
if (options && options.errors) {
var error_messages = this.tree.matchResponse(options.errors);
for (var input_id in this.element_list) {
var input = this.element_list[input_id];
if (error_messages[input_id]) {
this.highlight(input_id, error_messages[input_id], true);
}
}
}
},
/** Renders the UI elements required for the form
*/
_renderForm: function(options) {
// link this
var self = this;
// create message view
this.message = new Ui.Message();
// button for version selection
var requirements_button = new Ui.ButtonIcon({
icon : 'fa-info-circle',
title : (!options.narrow && 'Requirements') || null,
tooltip : 'Display tool requirements',
onclick : function() {
if (!this.visible) {
this.visible = true;
self.message.update({
persistent : true,
message : ToolTemplate.requirements(options),
status : 'info'
});
} else {
this.visible = false;
self.message.update({
message : ''
});
}
}
});
if (!options.requirements || options.requirements.length == 0) {
requirements_button.$el.hide();
}
// button for version selection
var versions_button = new Ui.ButtonMenu({
icon : 'fa-cubes',
title : (!options.narrow && 'Versions') || null,
tooltip : 'Select another tool version'
});
if (options.versions && options.versions.length > 1) {
for (var i in options.versions) {
var version = options.versions[i];
if (version != options.version) {
versions_button.addMenu({
title : 'Switch to ' + version,
version : version,
icon : 'fa-cube',
onclick : function() {
// here we update the tool version (some tools encode the version also in the id)
self.options.id = self.options.id.replace(self.options.version, this.version);
self.options.version = this.version;
// rebuild the model and form
self.deferred.reset();
self.deferred.execute(function(){self._buildModel()});
}
});
}
}
} else {
versions_button.$el.hide();
}
// button menu
var menu_button = new Ui.ButtonMenu({
icon : 'fa-caret-down',
title : (!options.narrow && 'Options') || null,
tooltip : 'View available options'
});
// configure button selection
if(options.biostar_url) {
// add question option
menu_button.addMenu({
icon : 'fa-question-circle',
title : 'Question?',
tooltip : 'Ask a question about this tool (Biostar)',
onclick : function() {
window.open(options.biostar_url + '/p/new/post/');
}
});
// create search button
menu_button.addMenu({
icon : 'fa-search',
title : 'Search',
tooltip : 'Search help for this tool (Biostar)',
onclick : function() {
window.open(options.biostar_url + '/t/' + options.id + '/');
}
});
};
// create share button
menu_button.addMenu({
icon : 'fa-share',
title : 'Share',
tooltip : 'Share this tool',
onclick : function() {
prompt('Copy to clipboard: Ctrl+C, Enter', window.location.origin + galaxy_config.root + 'root?tool_id=' + options.id);
}
});
// add admin operations
if (this.is_admin) {
// create download button
menu_button.addMenu({
icon : 'fa-download',
title : 'Download',
tooltip : 'Download this tool',
onclick : function() {
window.location.href = galaxy_config.root + 'api/tools/' + options.id + '/download';
}
});
}
// create tool form section
this.section = new ToolSection.View(self, {
inputs : options.inputs
});
// switch to classic tool form mako if the form definition is incompatible
if (this.incompatible) {
this.$el.hide();
$('#tool-form-classic').show();
return;
}
// create portlet
this.portlet = new Portlet.View({
icon : 'fa-wrench',
title : '<b>' + options.name + '</b> ' + options.description + ' (Galaxy Tool Version ' + options.version + ')',
cls : 'ui-portlet-slim',
operations: {
requirements : requirements_button,
menu : menu_button,
versions : versions_button
},
buttons : this.buttons
});
// remove padding
if (options.narrow) {
this.portlet.$content.css('padding', '0px');
}
// append message
this.portlet.append(this.message.$el, true);
// append tool section
this.portlet.append(this.section.$el);
// start form
this.$el.empty();
this.$el.append(this.portlet.$el);
// append help
if (options.help != '') {
this.$el.append(ToolTemplate.help(options.help));
}
// append citations
if (options.citations) {
var $citations = $('<div/>');
var citations = new CitationModel.ToolCitationCollection();
citations.tool_id = options.id;
var citation_list_view = new CitationView.CitationListView({ el: $citations, collection: citations } );
citation_list_view.render();
citations.fetch();
this.$el.append($citations);
}
// show message if available in model
if (options.message) {
this.message.update({
persistent : true,
status : 'warning',
message : options.message
});
}
}
});
});
@@ -0,0 +1,325 @@
/**
This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules.
*/
define(['utils/utils', 'mvc/tools/tools-form-base'],
function(Utils, ToolFormBase) {
// create form view
var View = ToolFormBase.extend({
initialize: function(options) {
// link with node representation in workflow module
this.node = workflow.active_node;
if (!this.node) {
console.debug('FAILED - tools-form-workflow:initialize() - Node not found in workflow.');
return;
}
// link actions
this.post_job_actions = this.node.post_job_actions || {};
// initialize parameters
this.options = options;
// set labels
this.options.text_enable = 'In Advance';
this.options.text_disable = 'At Runtime';
// configure workflow style
this.options.is_dynamic = false;
this.options.narrow = true;
this.options.initial_errors = true;
// declare fields as optional
Utils.deepeach(options.inputs, function(item) {
if (item.type) {
item.optional = (['data', 'data_hidden', 'hidden', 'drill_down', 'repeat', 'conditional']).indexOf(item.type) == -1;
}
});
// declare conditional fields as not optional
Utils.deepeach(options.inputs, function(item) {
if (item.type) {
if (item.type == 'conditional') {
item.test_param.optional = false;
}
}
});
// load extension
var self = this;
Utils.get({
url : galaxy_config.root + 'api/datatypes',
cache : true,
success : function(datatypes) {
self.datatypes = datatypes;
self._makeSections(options.inputs);
ToolFormBase.prototype.initialize.call(self, options);
}
});
},
/** Builds all sub sections
*/
_makeSections: function(inputs){
// for annotation
inputs[Utils.uuid()] = {
label : 'Annotation / Notes',
name : 'annotation',
type : 'text',
area : true,
help : 'Add an annotation or note for this step. It will be shown with the workflow.',
value : this.node.annotation
}
// get first output id
var output_id = this.node.output_terminals && Object.keys(this.node.output_terminals)[0];
if (output_id) {
// send email on job completion
inputs[Utils.uuid()] = {
name : 'pja__' + output_id + '__EmailAction',
label : 'Email notification',
type : 'boolean',
value : String(Boolean(this.post_job_actions['EmailAction' + output_id])),
ignore : 'false',
help : 'An email notification will be send when the job has completed.',
payload : {
'host' : window.location.host
}
};
// delete non-output files
inputs[Utils.uuid()] = {
name : 'pja__' + output_id + '__DeleteIntermediatesAction',
label : 'Output cleanup',
type : 'boolean',
value : String(Boolean(this.post_job_actions['DeleteIntermediatesAction' + output_id])),
ignore : 'false',
help : 'Delete intermediate outputs if they are not used as input for another job.'
};
// add output specific actions
for (var i in this.node.output_terminals) {
inputs[Utils.uuid()] = this._makeSection(i);
}
}
},
/** Builds sub section with step actions/annotation
*/
_makeSection: function(output_id){
// format datatypes
var extensions = [];
for (key in this.datatypes) {
extensions.push({
0 : this.datatypes[key],
1 : this.datatypes[key]
});
}
// sort extensions
extensions.sort(function(a, b) {
return a.label > b.label ? 1 : a.label < b.label ? -1 : 0;
});
// add additional options
extensions.unshift({
0 : 'Sequences',
1 : 'Sequences'
});
extensions.unshift({
0 : 'Roadmaps',
1 : 'Roadmaps'
});
extensions.unshift({
0 : 'Leave unchanged',
1 : ''
});
// create custom sub section
var input_config = {
label : 'Add Actions: \'' + output_id + '\'',
type : 'section',
inputs : [{
action : 'RenameDatasetAction',
argument : 'newname',
label : 'Rename dataset',
type : 'text',
value : '',
ignore : '',
help : 'This action will rename the result dataset. Click <a href="https://wiki.galaxyproject.org/Learn/AdvancedWorkflow/Variables">here</a> for more information.'
},{
action : 'ChangeDatatypeAction',
argument : 'newtype',
label : 'Change datatype',
type : 'select',
ignore : '',
options : extensions,
help : 'This action will change the datatype of the output to the indicated value.'
},{
action : 'TagDatasetAction',
argument : 'tags',
label : 'Tags',
type : 'text',
value : '',
ignore : '',
help : 'This action will set tags for the dataset.'
},{
label : 'Assign columns',
type : 'section',
inputs : [{
action : 'ColumnSetAction',
argument : 'chromCol',
label : 'Chrom column',
type : 'integer',
value : '',
ignore : ''
},{
action : 'ColumnSetAction',
argument : 'startCol',
label : 'Start column',
type : 'integer',
value : '',
ignore : ''
},{
action : 'ColumnSetAction',
argument : 'endCol',
label : 'End column',
type : 'integer',
value : '',
ignore : ''
},{
action : 'ColumnSetAction',
argument : 'strandCol',
label : 'Strand column',
type : 'integer',
value : '',
ignore : ''
},{
action : 'ColumnSetAction',
argument : 'nameCol',
label : 'Name column',
type : 'integer',
value : '',
ignore : ''
}],
help : 'This action will set column assignments in the output dataset. Blank fields are ignored.'
}]
};
// visit input nodes and enrich by name/value pairs from server data
var self = this;
function visit (head, head_list) {
head_list = head_list || [];
head_list.push(head);
for (var i in head.inputs) {
var input = head.inputs[i];
if (input.action) {
// construct identifier as expected by backend
input.name = 'pja__' + output_id + '__' + input.action;
if (input.argument) {
input.name += '__' + input.argument;
}
// modify names of payload arguments
if (input.payload) {
for (var p_id in input.payload) {
var p = input.payload[p_id];
input.payload[input.name + '__' + p_id] = p;
delete p;
}
}
// access/verify existence of value
var d = self.post_job_actions[input.action + output_id];
if (d) {
// mark as expanded
for (var j in head_list) {
head_list[j].expand = true;
}
// update input field value
if (input.argument) {
input.value = d.action_arguments && d.action_arguments[input.argument] || input.value;
} else {
input.value = 'true';
}
}
}
// continue with sub section
if (input.inputs) {
visit(input, head_list.slice(0));
}
}
}
visit(input_config);
// return final configuration
return input_config;
},
/** Builds a new model through api call and recreates the entire form
*/
_buildModel: function() {
Galaxy.modal.show({
title : 'Coming soon...',
body : 'This feature has not been implemented yet.',
buttons : {
'Close' : function() {
Galaxy.modal.hide();
}
}
});
},
/** Request a new model for an already created tool form and updates the form inputs
*/
_updateModel: function() {
// create the request dictionary
var self = this;
var current_state = this.tree.finalize();
// log tool state
console.debug('tools-form-workflow::_refreshForm() - Refreshing states.');
console.debug(current_state);
// register process
var process_id = this.deferred.register();
// build model url for request
var model_url = galaxy_config.root + 'workflow/editor_form_post?tool_id=' + this.options.id + '&__is_dynamic__=False';
// post job
Utils.request({
type : 'GET',
url : model_url,
data : current_state,
success : function(data) {
// update node in workflow module
self.node.update_field_data(data);
// highlight errors
self._errors(data && data.tool_model);
// process completed
self.deferred.done(process_id);
// log success
console.debug('tools-form::_refreshForm() - States refreshed.');
console.debug(data);
},
error : function(response) {
// process completed
self.deferred.done(process_id);
// log error
console.debug('tools-form::_refreshForm() - Refresh request failed.');
console.debug(response);
}
});
}
});
return {
View: View
};
});
+36 -340
View File
@@ -1,81 +1,30 @@
/**
This is the main class of the tool form plugin. It is referenced as 'app' in all lower level modules.
*/
define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
'mvc/citation/citation-model', 'mvc/citation/citation-view',
'mvc/tools', 'mvc/tools/tools-template', 'mvc/tools/tools-content', 'mvc/tools/tools-section', 'mvc/tools/tools-tree', 'mvc/tools/tools-jobs'],
function(Utils, Deferred, Portlet, Ui, CitationModel, CitationView,
Tools, ToolTemplate, ToolContent, ToolSection, ToolTree, ToolJobs) {
define(['utils/utils', 'mvc/ui/ui-misc', 'mvc/tools/tools-form-base', 'mvc/tools/tools-jobs'],
function(Utils, Ui, ToolFormBase, ToolJobs) {
// create form view
var View = Backbone.View.extend({
// base element
container: 'body',
var View = ToolFormBase.extend({
// initialize
initialize: function(options) {
// log options
console.debug(options);
// link galaxy modal or create one
var galaxy = parent.Galaxy;
if (galaxy && galaxy.modal) {
this.modal = galaxy.modal;
} else {
this.modal = new Ui.Modal.View();
var self = this;
this.job_handler = new ToolJobs(this);
this.buttons = {
execute : new Ui.Button({
icon : 'fa-check',
tooltip : 'Execute: ' + options.name,
title : 'Execute',
cls : 'btn btn-primary',
floating : 'clear',
onclick : function() {
self.job_handler.submit();
}
})
}
// check if the user is an admin
if (galaxy && galaxy.currUser) {
this.is_admin = galaxy.currUser.get('is_admin')
} else {
this.is_admin = false;
}
// link options
this.options = options;
// create deferred processing queue handler
// this handler reduces the number of requests to the api by filtering redundant requests
this.deferred = new Deferred();
// set element
this.setElement('<div/>');
// add to main element
$(this.container).append(this.$el);
// build this form
this._buildForm();
ToolFormBase.prototype.initialize.call(this, options);
},
/** Shows the final message (usually upon successful job submission)
*/
reciept: function($el) {
$(this.container).empty();
$(this.container).append($el);
},
/** Highlight and scroll to input element (currently only used for error notifications)
*/
highlight: function (input_id, message, silent) {
// get input field
var input_element = this.element_list[input_id];
// check input element
if (input_element) {
// mark error
input_element.error(message || 'Please verify this parameter.');
// scroll to first input element
if (!silent) {
$(this.container).animate({
scrollTop: input_element.$el.offset().top - 20
}, 500);
}
}
},
/** Builds a new model through api call and recreates the entire form
*/
_buildModel: function() {
@@ -98,31 +47,28 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
}
}
}
// register process
var process_id = this.deferred.register();
// get initial model
Utils.request({
type : 'GET',
url : model_url,
success : function(response) {
// link model data update options
self.options = response;
// build form
self._buildForm();
// build new tool form
self.build(response);
// notification
self.message.update({
status : 'success',
message : 'Now you are using \'' + self.options.name + '\' version ' + self.options.version + '.',
persistent : false
});
// process completed
self.deferred.done(process_id);
// log success
console.debug('tools-form::initialize() - Initial tool model ready.');
console.debug(response);
@@ -130,11 +76,11 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
error : function(response) {
// process completed
self.deferred.done(process_id);
// log error
console.debug('tools-form::initialize() - Initial tool model request failed.');
console.debug(response);
// show error
var error_message = response.error || 'Uncaught error.';
self.modal.show({
@@ -149,7 +95,7 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
}
});
},
/** Request a new model for an already created tool form and updates the form inputs
*/
_updateModel: function() {
@@ -163,11 +109,11 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
return null;
}
});
// log tool state
console.debug('tools-form::_refreshForm() - Refreshing states.');
console.debug(current_state);
// activates/disables spinner for dynamic fields to indicate that they are currently being updated
function wait(active) {
for (var i in self.input_list) {
@@ -182,10 +128,10 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
}
}
}
// set wait mode
wait(true);
// register process
var process_id = this.deferred.register();
@@ -230,13 +176,13 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
}
}
});
// unset wait mode
wait(false);
// process completed
self.deferred.done(process_id);
// log success
console.debug('tools-form::_refreshForm() - States refreshed.');
console.debug(new_model);
@@ -244,262 +190,12 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
error : function(response) {
// process completed
self.deferred.done(process_id);
// log error
console.debug('tools-form::_refreshForm() - Refresh request failed.');
console.debug(response);
}
});
},
/** Main tool form build function. This function is called once a new model is available.
*/
_buildForm: function() {
// link this
var self = this;
// reset events
this.off('refresh');
this.off('reset');
// reset field list, which contains the input field elements
this.field_list = {};
// reset sequential input definition list, which contains the input definitions as provided from the api
this.input_list = {};
// reset input element list, which contains the dom elements of each input element (includes also the input field)
this.element_list = {};
// creates a tree/json data structure from the input form
this.tree = new ToolTree(this);
// creates the job handler
this.job_handler = new ToolJobs(this);
// request history content and build form
this.content = new ToolContent(this);
// link model options
var options = this.options;
// create ui elements
this._renderForm(options);
// rebuild the underlying data structure
this.tree.finalize();
// show errors
if (options.errors) {
var error_messages = this.tree.matchResponse(options.errors);
for (var input_id in error_messages) {
this.highlight(input_id, error_messages[input_id], true);
}
}
// add refresh listener
this.on('refresh', function() {
// by using/reseting the deferred ajax queue the number of redundant calls is reduced
self.deferred.reset();
self.deferred.execute(function(){self._updateModel()});
});
// add reset listener
this.on('reset', function() {
for (var i in this.element_list) {
this.element_list[i].reset();
}
});
},
/** Renders the UI elements required for the form
*/
_renderForm: function(options) {
// link this
var self = this;
// create message view
this.message = new Ui.Message();
// button for version selection
var requirements_button = new Ui.ButtonIcon({
icon : 'fa-info-circle',
title : 'Requirements',
tooltip : 'Display tool requirements',
onclick : function() {
if (!this.visible) {
this.visible = true;
self.message.update({
persistent : true,
message : ToolTemplate.requirements(options),
status : 'info'
});
} else {
this.visible = false;
self.message.update({
message : ''
});
}
}
});
if (!options.requirements || options.requirements.length == 0) {
requirements_button.$el.hide();
}
// button for version selection
var versions_button = new Ui.ButtonMenu({
icon : 'fa-cubes',
title : 'Versions',
tooltip : 'Select another tool version'
});
if (options.versions && options.versions.length > 1) {
for (var i in options.versions) {
var version = options.versions[i];
if (version != options.version) {
versions_button.addMenu({
title : 'Switch to ' + version,
version : version,
icon : 'fa-cube',
onclick : function() {
// here we update the tool version (some tools encode the version also in the id)
options.id = options.id.replace(options.version, this.version);
options.version = this.version;
// rebuild the model and form
self.deferred.reset();
self.deferred.execute(function(){self._buildModel()});
}
});
}
}
} else {
versions_button.$el.hide();
}
// button menu
var menu_button = new Ui.ButtonMenu({
icon : 'fa-caret-down',
title : 'Options',
tooltip : 'View available options'
});
// configure button selection
if(options.biostar_url) {
// add question option
menu_button.addMenu({
icon : 'fa-question-circle',
title : 'Question?',
tooltip : 'Ask a question about this tool (Biostar)',
onclick : function() {
window.open(options.biostar_url + '/p/new/post/');
}
});
// create search button
menu_button.addMenu({
icon : 'fa-search',
title : 'Search',
tooltip : 'Search help for this tool (Biostar)',
onclick : function() {
window.open(options.biostar_url + '/t/' + options.id + '/');
}
});
};
// create share button
menu_button.addMenu({
icon : 'fa-share',
title : 'Share',
tooltip : 'Share this tool',
onclick : function() {
prompt('Copy to clipboard: Ctrl+C, Enter', window.location.origin + galaxy_config.root + 'root?tool_id=' + options.id);
}
});
// add admin operations
if (this.is_admin) {
// create download button
menu_button.addMenu({
icon : 'fa-download',
title : 'Download',
tooltip : 'Download this tool',
onclick : function() {
window.location.href = galaxy_config.root + 'api/tools/' + options.id + '/download';
}
});
}
// create tool form section
this.section = new ToolSection.View(self, {
inputs : options.inputs,
cls : 'ui-table-plain'
});
// switch to classic tool form mako if the form definition is incompatible
if (this.incompatible) {
this.$el.hide();
$('#tool-form-classic').show();
return;
}
// create portlet
this.portlet = new Portlet.View({
icon : 'fa-wrench',
title : '<b>' + options.name + '</b> ' + options.description + ' (Galaxy Tool Version ' + options.version + ')',
cls : 'ui-portlet-slim',
operations: {
requirements : requirements_button,
menu : menu_button,
versions : versions_button
},
buttons: {
execute : new Ui.Button({
icon : 'fa-check',
tooltip : 'Execute: ' + options.name,
title : 'Execute',
cls : 'btn btn-primary',
floating : 'clear',
onclick : function() {
self.job_handler.submit();
}
})
}
});
// append message
this.portlet.append(this.message.$el, true);
// append tool section
this.portlet.append(this.section.$el);
// start form
this.$el.empty();
this.$el.append(this.portlet.$el);
// append help
if (options.help != '') {
this.$el.append(ToolTemplate.help(options.help));
}
// append citations
if (options.citations) {
var $citations = $('<div/>');
var citations = new CitationModel.ToolCitationCollection();
citations.tool_id = options.id;
var citation_list_view = new CitationView.CitationListView({ el: $citations, collection: citations } );
citation_list_view.render();
citations.fetch();
this.$el.append($citations);
}
// show message if available in model
if (options.message) {
this.message.update({
persistent : true,
status : 'warning',
message : options.message
});
}
}
});
+20 -7
View File
@@ -10,8 +10,13 @@ define([], function() {
// link app
this.app = app;
// set text labels
this.text_enable = app.options.text_enable || 'Enable';
this.text_disable = app.options.text_disable || 'Disable';
// link field
this.field = options.field;
this.default_value = options.default_value;
// set element
this.setElement(this._template(options));
@@ -25,9 +30,15 @@ define([], function() {
// add field element
this.$field.prepend(this.field.$el);
// start with enabled optional fields
// decide wether to expand or collapse optional fields
this.field.skip = false;
var v = this.field.value && this.field.value();
this.field.skip = Boolean(options.optional &&
((this.default_value === undefined) ||
((this.field.validate && !this.field.validate()) || !v ||
(v == this.default_value) || (Number(v) == Number(this.default_value)) ||
(JSON.stringify(v) == JSON.stringify(this.default_value)))));
// refresh view
this._refresh();
@@ -46,7 +57,7 @@ define([], function() {
*/
error: function(text) {
this.$error_text.html(text);
this.$error.fadeIn();
this.$error.show();
this.$el.addClass('ui-error');
},
@@ -62,12 +73,14 @@ define([], function() {
_refresh: function() {
if (!this.field.skip) {
this.$field.fadeIn('fast');
this.$title_optional.html('Disable');
this.app.trigger('refresh');
this.$title_optional.html(this.text_disable);
} else {
this.reset();
this.$field.hide();
this.$title_optional.html('Enable');
this.$title_optional.html(this.text_enable);
this.field.value && this.field.value(this.default_value);
}
this.app.trigger('refresh');
},
/** Main Template
@@ -82,7 +95,7 @@ define([], function() {
// is optional
if (options.optional) {
tmp += 'Optional: ' + options.label +
tmp += options.label +
'<span>&nbsp[<span class="ui-table-form-title-optional"/>]</span>';
} else {
tmp += options.label;
@@ -98,7 +98,7 @@ return Backbone.Model.extend({
}
// validate non-optional fields
if (!input_def.optional && input_field.validate && !input_field.validate()) {
if (!input_def.optional && input_value == null) {
this.app.highlight(input_id);
return false;
}
+227 -103
View File
@@ -1,8 +1,8 @@
/**
This class creates a tool form section and populates it with input elements. It also handles repeat blocks and conditionals by recursively creating new sub sections. New input elements can be plugged in by adding cases to the switch block defined in the _createField() function.
*/
define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-repeat', 'mvc/tools/tools-select-content', 'mvc/tools/tools-input'],
function(Utils, Table, Ui, Repeat, SelectContent, InputElement) {
define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/ui/ui-portlet', 'mvc/tools/tools-repeat', 'mvc/tools/tools-select-content', 'mvc/tools/tools-input'],
function(Utils, Table, Ui, Portlet, Repeat, SelectContent, InputElement) {
// create form view
var View = Backbone.View.extend({
@@ -10,51 +10,54 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
initialize: function(app, options) {
// link app
this.app = app;
// link inputs
this.inputs = options.inputs;
// fix table style
options.cls = 'ui-table-plain';
// add table class for tr tag
// this assist in transforming the form into a json structure
options.cls_tr = 'section-row';
// create table
this.table = new Table.View(options);
// configure portlet and form table
this.setElement(this.table.$el);
// render tool section
this.render();
},
/** Render section view
*/
render: function() {
// reset table
this.table.delAll();
// load settings elements into table
for (var i in this.inputs) {
this._add(this.inputs[i]);
this.add(this.inputs[i]);
}
},
/** Add a new input element
*/
_add: function(input) {
add: function(input) {
// link this
var self = this;
// clone definition
var input_def = jQuery.extend(true, {}, input);
// create unique id
input_def.id = input.id = Utils.uuid();
// add to sequential list of inputs
this.app.input_list[input_def.id] = input_def;
// identify field type
var type = input_def.type;
switch(type) {
@@ -66,50 +69,53 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
case 'repeat':
this._addRepeat(input_def);
break;
// customized section
case 'section':
this._addSection(input_def);
break;
// default single element row
default:
this._addRow(input_def);
}
},
/** Add a conditional block
*/
_addConditional: function(input_def) {
// link this
var self = this;
// copy identifier
input_def.test_param.id = input_def.id;
// build test parameter
var field = this._addRow(input_def.test_param);
// set onchange event for test parameter
field.options.onchange = function(value) {
// identify the selected case
var selectedCase = self.app.tree.matchCase(input_def, value);
// check value in order to hide/show options
for (var i in input_def.cases) {
// get case
var case_def = input_def.cases[i];
// identify subsection name
var section_id = input_def.id + '-section-' + i;
// identify row
var section_row = self.table.get(section_id);
// check if non-hidden elements exist
var nonhidden = false;
for (var j in case_def.inputs) {
var type = case_def.inputs[j].type;
if (type && type !== 'hidden') {
if (!case_def.inputs[j].hidden) {
nonhidden = true;
break;
}
}
// show/hide sub form
if (i == selectedCase && nonhidden) {
section_row.fadeIn('fast');
@@ -117,71 +123,69 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
section_row.hide();
}
}
// refresh form inputs
self.app.trigger('refresh');
};
// add conditional sub sections
for (var i in input_def.cases) {
// create id tag
var sub_section_id = input_def.id + '-section-' + i;
// create sub section
var sub_section = new View(this.app, {
inputs : input_def.cases[i].inputs,
cls : 'ui-table-plain'
inputs : input_def.cases[i].inputs
});
// displays as grouped subsection
sub_section.$el.addClass('ui-table-form-section');
// create table row
this.table.add(sub_section.$el);
// append to table
this.table.append(sub_section_id);
}
// trigger refresh on conditional input field after all input elements have been created
field.trigger('change');
},
/** Add a repeat block
*/
_addRepeat: function(input_def) {
// link this
var self = this;
// block index
var block_index = 0;
// helper function to create new repeat blocks
function create (inputs, deleteable) {
// create id tag
var sub_section_id = input_def.id + '-section-' + (block_index++);
// enable/disable repeat delete button
var ondel = null;
if (deleteable) {
ondel = function() {
// delete repeat block
repeat.del(sub_section_id);
// retitle repeat block
repeat.retitle(input_def.title);
// trigger refresh
self.app.trigger('refresh');
}
}
// create sub section
var sub_section = new View(self.app, {
inputs : inputs,
cls : 'ui-table-plain'
inputs : inputs
});
// add tab
repeat.add({
id : sub_section_id,
@@ -189,11 +193,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
$el : sub_section.$el,
ondel : ondel
});
// retitle repeat block
repeat.retitle(input_def.title);
}
//
// create repeat block element
//
@@ -208,7 +212,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
self.app.trigger('refresh');
}
});
//
// add parsed/minimum number of repeat blocks
//
@@ -222,28 +226,85 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
} else {
inputs = input_def.inputs;
}
// create repeat block
create(inputs, i >= n_min);
}
// create input field wrapper
var input_element = new InputElement(this.app, {
label : input_def.title,
help : input_def.help,
field : repeat
label : input_def.title,
help : input_def.help,
field : repeat
});
// displays as grouped subsection
input_element.$el.addClass('ui-table-form-section');
// create table row
this.table.add(input_element.$el);
// append row to table
this.table.append(input_def.id);
},
/** Add a customized section
*/
_addSection: function(input_def) {
// link this
var self = this;
// create sub section
var sub_section = new View(self.app, {
inputs : input_def.inputs
});
// delete button
var button_visible = new Ui.ButtonIcon({
icon : 'fa-eye-slash',
tooltip : 'Show/hide section',
cls : 'ui-button-icon-plain'
});
// create portlet for sub section
var portlet = new Portlet.View({
title : input_def.label,
cls : 'ui-portlet-section',
operations : {
button_visible: button_visible
}
});
portlet.append(sub_section.$el);
portlet.append($('<div/>').addClass('ui-table-form-info').html(input_def.help));
// add event handler visibility button
var visible = false;
portlet.$content.hide();
portlet.$header.css('cursor', 'pointer');
portlet.$header.on('click', function() {
if (visible) {
visible = false;
portlet.$content.hide();
button_visible.setIcon('fa-eye-slash');
} else {
visible = true;
portlet.$content.fadeIn('fast');
button_visible.setIcon('fa-eye');
}
});
// show sub section if requested
if (input_def.expand) {
portlet.$header.trigger('click');
}
// create table row
this.table.add(portlet.$el);
// append row to table
this.table.append(input_def.id);
},
/** Add a single input field element
*/
_addRow: function(input_def) {
@@ -252,70 +313,86 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
// create input field
var field = this._createField(input_def);
// add to field list
this.app.field_list[id] = field;
// fix default value if missing
if (input_def.default_value === undefined) {
input_def.default_value = input_def.value;
}
// create input field wrapper
var input_element = new InputElement(this.app, {
label : input_def.label,
optional : input_def.optional,
help : input_def.help,
field : field
label : input_def.label,
default_value : input_def.default_value,
optional : input_def.optional,
help : input_def.help,
field : field
});
// add to element list
this.app.element_list[id] = input_element;
// create table row
this.table.add(input_element.$el);
// append to table
this.table.append(id);
// hide row if neccessary
if (input_def.hidden) {
this.table.get(id).hide();
}
// return created field
return field;
},
/** Returns an input field for a given field type
*/
_createField: function(input_def) {
// field wrapper
var field = null;
// identify field type
switch(input_def.type) {
// text input field
case 'text' :
field = this._fieldText(input_def);
break;
// select field
case 'select' :
field = this._fieldSelect(input_def);
break;
// data selector
case 'data':
field = this._fieldData(input_def);
break;
// collection selector
case 'data_collection':
field = this._fieldData(input_def);
break;
// data column
case 'data_column':
input_def.error_text = 'Missing columns in referenced dataset.';
field = this._fieldSelect(input_def);
break;
// hidden field
case 'hidden':
field = this._fieldHidden(input_def);
break;
// hidden data field
case 'hidden_data':
field = this._fieldHidden(input_def);
break;
// integer field
case 'integer':
field = this._fieldSlider(input_def);
@@ -325,33 +402,33 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
case 'float':
field = this._fieldSlider(input_def);
break;
// boolean field
case 'boolean':
field = this._fieldBoolean(input_def);
break;
// genome field
case 'genomebuild':
input_def.searchable = true;
field = this._fieldSelect(input_def);
break;
// drill down field
case 'drill_down':
field = this._fieldDrilldown(input_def);
break;
// base url field
case 'baseurl':
field = this._fieldHidden(input_def);
break;
// field not found
default:
// flag
this.app.incompatible = true;
// with or without options
if (input_def.options) {
// assign select field
@@ -360,27 +437,33 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
// assign text field
field = this._fieldText(input_def);
}
// log
console.debug('tools-form::_addRow() : Auto matched field type (' + input_def.type + ').');
}
// set field value
if (input_def.value !== undefined) {
field.value(input_def.value);
}
// return field element
return field;
},
/** Data input field
*/
_fieldData : function(input_def) {
if (!this.app.options.is_dynamic) {
input_def.info = 'Data input \'' + input_def.name + '\' (' + Utils.textify(input_def.extensions.toString()) + ')';
input_def.value = null;
return this._fieldHidden(input_def);
}
var self = this;
return new SelectContent.View(this.app, {
id : 'field-' + input_def.id,
extensions : input_def.extensions,
optional : input_def.optional,
multiple : input_def.multiple,
type : input_def.type,
data : input_def.options,
@@ -389,10 +472,15 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
}
});
},
/** Select/Checkbox/Radio options field
*/
_fieldSelect : function (input_def) {
// show text field in if dynamic fields are disabled e.g. in workflow editor
if (!this.app.options.is_dynamic && input_def.is_dynamic) {
return this._fieldText(input_def);
}
// configure options fields
var options = [];
for (var i in input_def.options) {
@@ -402,7 +490,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
value: option[1]
});
}
// identify display type
var SelectClass = Ui.Select;
switch (input_def.display) {
@@ -413,7 +501,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
SelectClass = Ui.Radio;
break;
}
// select field
var self = this;
return new SelectClass.View({
@@ -427,10 +515,16 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
}
});
},
/** Drill down options field
*/
_fieldDrilldown : function (input_def) {
// show text field in if dynamic fields are disabled e.g. in workflow editor
if (!this.app.options.is_dynamic && input_def.is_dynamic) {
return this._fieldText(input_def);
}
// create drill down field
var self = this;
return new Ui.Drilldown.View({
id : 'field-' + input_def.id,
@@ -445,42 +539,72 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep
/** Text input field
*/
_fieldText : function(input_def) {
// field replaces e.g. a select field
if (input_def.options) {
// show text area if selecting multiple entries is allowed
input_def.area = input_def.multiple;
// validate value
if (!Utils.validate(input_def.value)) {
input_def.value = '';
} else {
if (input_def.value instanceof Array) {
input_def.value = value.toString();
} else {
input_def.value = String(input_def.value).replace(/[\[\]'"\s]/g, '');
if (input_def.multiple) {
input_def.value = input_def.value.replace(/,/g, '\n');
}
}
}
}
// create input element
var self = this;
return new Ui.Input({
id : 'field-' + input_def.id,
area : input_def.area,
onchange: function() {
id : 'field-' + input_def.id,
area : input_def.area,
onchange : function() {
self.app.trigger('refresh');
}
});
},
/** Slider field
*/
_fieldSlider: function(input_def) {
var self = this;
return new Ui.Slider.View({
id : 'field-' + input_def.id,
precise : input_def.type == 'float',
min : input_def.min,
max : input_def.max
id : 'field-' + input_def.id,
precise : input_def.type == 'float',
min : input_def.min,
max : input_def.max,
onchange : function() {
self.app.trigger('refresh');
}
});
},
/** Hidden field
*/
_fieldHidden : function(input_def) {
return new Ui.Hidden({
id : 'field-' + input_def.id
id : 'field-' + input_def.id,
info : input_def.info
});
},
/** Boolean field
*/
_fieldBoolean : function(input_def) {
var self = this;
return new Ui.RadioButton.View({
id : 'field-' + input_def.id,
data : [ { label : 'Yes', value : 'true' },
{ label : 'No', value : 'false' }]
id : 'field-' + input_def.id,
data : [ { label : 'Yes', value : 'true' },
{ label : 'No', value : 'false' }],
onchange : function() {
self.app.trigger('refresh');
}
});
}
});
@@ -36,14 +36,7 @@ var View = Backbone.View.extend({
this.list = {};
// error messages
var extensions = options.extensions.toString();
if (extensions) {
extensions = extensions.replace(/,/g, ', ');
var pos = extensions.lastIndexOf(', ');
if (pos != -1) {
extensions = extensions.substr(0, pos) + ' or ' + extensions.substr(pos+1);
}
}
var extensions = Utils.textify(options.extensions);
var hda_error = 'No dataset available.';
if (extensions) {
hda_error = 'No ' + extensions + ' dataset available.';
@@ -57,6 +50,7 @@ var View = Backbone.View.extend({
if (this.mode == 'single') {
radio_buttons.push({icon: 'fa-file-o', label : 'Single dataset', value : 'single'});
this.select_single = new Ui.Select.View({
optional : options.optional,
error_text : hda_error,
onchange : function() {
self.trigger('change');
@@ -89,6 +83,7 @@ var View = Backbone.View.extend({
radio_buttons.push({icon: 'fa-folder-o', label : 'Dataset collection', value : 'collection' });
this.select_collection = new Ui.Select.View({
error_text : hdca_error,
optional : options.optional,
onchange : function() {
self.trigger('change');
}
@@ -187,39 +182,39 @@ var View = Backbone.View.extend({
},
/** Return the currently selected dataset values */
value : function (dict) {
value : function (new_value) {
// update current value
if (dict && dict.values) {
try {
// create list
var list = [];
for (var i in dict.values) {
list.push(dict.values[i].id);
}
// identify suitable select field
if (dict && dict.values.length > 0 && dict.values[0].src == 'hcda') {
this.current = 'collection';
this.select_collection.value(list[0]);
} else {
if (this.mode == 'multiple') {
this.current = 'multiple';
this.select_multiple.value(list);
} else {
this.current = 'single';
this.select_single.value(list[0]);
if (new_value !== undefined) {
if (new_value && new_value.values) {
try {
// create list with values
var list = [];
for (var i in new_value.values) {
list.push(new_value.values[i].id);
}
// identify suitable select field
if (new_value && new_value.values.length > 0 && new_value.values[0].src == 'hcda') {
this.current = 'collection';
this.select_collection.value(list[0]);
} else {
if (this.mode == 'multiple') {
this.current = 'multiple';
this.select_multiple.value(list);
} else {
this.current = 'single';
this.select_single.value(list[0]);
}
}
} catch (err) {
console.debug('tools-select-content::value() - Skipped.');
}
this.refresh();
// check if value has been set
var select = this._select();
if (!select.validate()) {
select.value(select.first());
}
} catch (err) {
console.debug('tools-select-content::value() - Skipped.');
} else {
this.select_single && this.select_single.value('__null__');
this.select_multiple && this.select_multiple.value('__null__');
this.select_collection && this.select_collection.value('__null__');
}
this.refresh();
}
// transform into an array
@@ -254,6 +249,7 @@ var View = Backbone.View.extend({
/** Refreshes data selection view */
refresh: function() {
this.button_type.value(this.current);
for (var i in this.list) {
var $el = this.list[i].field.$el;
if (this.current == i) {
@@ -4,7 +4,7 @@ define([], function() {
// tool form templates
return {
help: function(content) {
return '<div class="toolHelp">' +
return '<div class="toolHelp" style="overflow: auto;">' +
'<div class="toolHelpBody">' +
content +
'</div>' +
+32 -12
View File
@@ -2,7 +2,7 @@
This class maps the tool form dom to an api compatible javascript dictionary.
*/
// dependencies
define([], function() {
define(['utils/utils'], function(Utils) {
// tool form tree
return Backbone.Model.extend({
@@ -108,22 +108,42 @@ return Backbone.Model.extend({
convert(job_input_id, head[input.id + '-section-' + selectedCase]);
}
break;
// handle custom sub sections
case 'section':
convert('', node);
break;
default:
// get field
var field = self.app.field_list[input.id];
// get and patch field value
var value = field.value();
if (patch[input.type]) {
value = patch[input.type](value);
}
// handle default value
if (!field.skip) {
if (input.optional && field.validate && !field.validate()) {
if (field && field.value) {
// get and patch field value
var value = field.value();
if (patch[input.type]) {
value = patch[input.type](value);
}
// validate field value
if (field.skip) {
continue;
}
// validate field value
if (field.validate && !field.validate()) {
value = null;
}
add (job_input_id, input.id, value);
// ignore certain values
if (input.ignore === undefined || (value !== null && input.ignore != value)) {
// add value to submission
add (job_input_id, input.id, value);
// add payload to submission
if (input.payload) {
for (var p_id in input.payload) {
add (p_id, input.id, input.payload[p_id]);
}
}
}
}
}
}
+65 -4
View File
@@ -193,6 +193,12 @@ var ButtonIcon = Backbone.View.extend({
this.disabled = false;
},
// change icon
setIcon: function(icon_cls) {
this.$('i').removeClass(this.options.icon).addClass(icon_cls);
this.options.icon = icon_cls;
},
// template
_template: function(options) {
// width
@@ -412,7 +418,7 @@ var Hidden = Backbone.View.extend({
initialize : function(options) {
// configure options
this.options = options;
// create new element
this.setElement(this._template(this.options));
@@ -425,14 +431,68 @@ var Hidden = Backbone.View.extend({
// value
value : function (new_val) {
if (new_val !== undefined) {
this.$el.val(new_val);
this.$('hidden').val(new_val);
}
return this.$el.val();
return this.$('hidden').val();
},
// element
_template: function(options) {
return '<hidden id="' + options.id + '" value="' + options.value + '"/>';
var tmpl = '<div id="' + options.id + '" >';
if (options.info) {
tmpl += '<label>' + options.info + '</label>';
}
tmpl += '<hidden value="' + options.value + '"/>' +
'</div>';
return tmpl;
}
});
// plugin
var CheckButton = Backbone.View.extend({
// default options
optionsDefault: {
class_add : 'fa fa-square-o',
class_remove : 'fa fa-check-square-o',
class_partial : 'fa fa-minus-square-o',
value : false
},
// initialize
initialize : function(options) {
// configure options
this.options = Utils.merge(options, this.optionsDefault);
// create new element
this.setElement($('<div/>'));
// set initial value
this.value(Boolean(this.options.value));
// add event handler
var self = this;
this.$el.on('click', function() {
self.value(!self.current);
});
},
// value
value : function (new_val) {
if (new_val !== undefined) {
this.current = new_val;
if (new_val) {
this.$el.removeClass()
.addClass('ui-checkbutton')
.addClass(this.options.class_remove);
} else {
this.$el.removeClass()
.addClass('ui-checkbutton')
.addClass(this.options.class_add);
}
this.options.onchange && this.options.onchange(new_val);
}
return this.current;
}
});
@@ -451,6 +511,7 @@ return {
RadioButton : Options.RadioButton,
Checkbox : Options.Checkbox,
Radio : Options.Radio,
CheckButton : CheckButton,
Searchbox : Searchbox,
Select : Select,
Hidden : Hidden,
+2 -11
View File
@@ -137,16 +137,7 @@ var Base = Backbone.View.extend({
/** Validate the selected option/options
*/
validate: function() {
var current = this.value();
if (!(current instanceof Array)) {
current = [current];
}
for (var i in current) {
if ([null, 'null', undefined].indexOf(current[i]) > -1) {
return false;
}
}
return true;
return Utils.validate(this.value());
},
/** Wait message during request processing
@@ -191,7 +182,7 @@ var Base = Backbone.View.extend({
// get selected values
var selected = this.$el.find(':checked');
if (selected.length == 0) {
return 'null';
return '__null__';
}
// return multiple or single value
+17 -22
View File
@@ -25,6 +25,7 @@ var View = Backbone.View.extend({
$content : null,
$buttons : null,
$operations : null,
$header : null,
// initialize
initialize : function(options) {
@@ -35,22 +36,16 @@ var View = Backbone.View.extend({
this.setElement(this._template(this.options));
// link content
this.$content = this.$el.find('#content');
this.$content = this.$el.find('.content');
// link title
this.$title = this.$el.find('#portlet-title-text');
this.$title = this.$el.find('.portlet-title-text');
// set content format
var $portlet_content = this.$el.find('#portlet-content');
if (!this.options.scrollable) {
if (this.options.title) {
$portlet_content.addClass('no-scroll');
} else {
$portlet_content.addClass('no-scroll-no-title');
}
} else {
$portlet_content.addClass('scroll');
}
// link header
this.$header = this.$el.find('.portlet-header');
// link portlet content (wraps 'content')
var $portlet_content = this.$el.find('.portlet-content');
// set content padding
if (this.options.nopadding) {
@@ -59,7 +54,7 @@ var View = Backbone.View.extend({
}
// append buttons
this.$buttons = $(this.el).find('#buttons');
this.$buttons = $(this.el).find('.buttons');
if (this.options.buttons) {
// link functions
var self = this;
@@ -72,7 +67,7 @@ var View = Backbone.View.extend({
}
// append operations
this.$operations = $(this.el).find('#operations');
this.$operations = $(this.el).find('.operations');
if (this.options.operations) {
// link functions
var self = this;
@@ -161,27 +156,27 @@ var View = Backbone.View.extend({
var tmpl = '<div id="' + options.id + '" class="' + options.cls + '">';
if (options.title) {
tmpl += '<div id="portlet-header" class="portlet-header">' +
'<div id="operations" style="float: ' + options.operations_flt + ';"></div>' +
tmpl += '<div class="portlet-header">' +
'<div class="operations" style="float: ' + options.operations_flt + ';"></div>' +
'<div class="portlet-title">';
if (options.icon)
tmpl += '<i class="icon fa ' + options.icon + '">&nbsp;</i>';
tmpl += '<span id="portlet-title-text" class="portlet-title-text">' + options.title + '</span>' +
tmpl += '<span class="portlet-title-text">' + options.title + '</span>' +
'</div>' +
'</div>';
}
tmpl += '<div id="portlet-content" class="portlet-content">';
tmpl += '<div class="portlet-content">';
if (options.placement == 'top') {
tmpl += '<div id="buttons" class="buttons"></div>';
tmpl += '<div class="buttons"></div>';
}
tmpl += '<div id="content" class="content"></div>';
tmpl += '<div class="content"></div>';
if (options.placement == 'bottom') {
tmpl += '<div id="buttons" class="buttons"></div>';
tmpl += '<div class="buttons"></div>';
}
tmpl += '</div>' +
@@ -10,10 +10,12 @@ var View = Backbone.View.extend({
id : '',
cls : '',
error_text : 'No data available',
empty_text : 'No selection',
visible : true,
wait : false,
multiple : false,
searchable : false
searchable : false,
optional : false
},
// initialize
@@ -96,16 +98,7 @@ var View = Backbone.View.extend({
/** Validate the current selection
*/
validate: function() {
var current = this.value();
if (!(current instanceof Array)) {
current = [current];
}
for (var i in current) {
if ([null, 'null', undefined].indexOf(current[i]) > -1) {
return false;
}
}
return true;
return Utils.validate(this.value());
},
/** Return the label/text of the current selection
@@ -190,6 +183,11 @@ var View = Backbone.View.extend({
// remove all options
this.$select.find('option').remove();
// add optional field
if (this.options.optional && !this.options.multiple) {
this.$select.append(this._templateOption({value : '__null__', label : this.options.empty_text}));
}
// add new options
for (var key in options) {
this.$select.append(this._templateOption(options[key]));
@@ -238,7 +236,7 @@ var View = Backbone.View.extend({
*/
_refresh: function() {
// remove placeholder
this.$select.find('option[value=null]').remove();
this.$select.find('option[value="__undefined__"]').remove();
// count remaining entries
var remaining = this.$select.find('option').length;
@@ -247,7 +245,7 @@ var View = Backbone.View.extend({
this.disable();
// append placeholder
this.$select.append(this._templateOption({value : 'null', label : this.options.error_text}));
this.$select.append(this._templateOption({value : '__undefined__', label : this.options.error_text}));
} else {
// enable select field
this.enable();
+1 -1
View File
@@ -54,7 +54,7 @@ var View = Backbone.View.extend({
}
// add text field event
this.$text.on('change', function () {
this.$text.on('input', function () {
self.value($(this).val());
});
@@ -20,24 +20,24 @@ var View = Backbone.View.extend({
initialize : function(model) {
// link this
var self = this;
// create model
this.model = model;
// get options
this.options = this.model.attributes;
// create new element
this.setElement(this._template(this.options));
// add event
$(this.el).on('click', this.options.onclick);
// add tooltip
if (this.options.tooltip) {
$(this.el).tooltip({title: this.options.tooltip, placement: 'bottom'});
}
// events
this.model.on('change:percentage', function() {
self._percentage(self.model.get('percentage'));
@@ -45,7 +45,7 @@ var View = Backbone.View.extend({
this.model.on('change:status', function() {
self._status(self.model.get('status'));
});
// unload event
var self = this;
$(window).on('beforeunload', function() {
@@ -58,7 +58,7 @@ var View = Backbone.View.extend({
}
});
},
// set status
_status: function(value) {
var $el = this.$el.find('.progress-bar');
@@ -69,13 +69,13 @@ var View = Backbone.View.extend({
$el.addClass('progress-bar-' + value);
}
},
// set percentage
_percentage: function(value) {
var $el = this.$el.find('.progress-bar');
$el.css({ width : value + '%' });
},
// template
_template: function(options) {
return '<div style="float: right">' +
+85 -44
View File
@@ -6,72 +6,94 @@ return Backbone.View.extend({
// options
options: {
class_add : 'upload-icon-button fa fa-square-o',
class_remove : 'upload-icon-button fa fa-check-square-o'
class_remove : 'upload-icon-button fa fa-check-square-o',
class_partial : 'upload-icon-button fa fa-minus-square-o'
},
// render
initialize: function(app) {
// link app
this.app = app;
// link this
var self = this;
// set template
this.setElement(this._template());
// list of rows
this.rows = [];
// load extension
Utils.get({
url : galaxy_config.root + 'api/ftp_files',
url : galaxy_config.root + 'api/remote_files',
success : function(ftp_files) { self._fill(ftp_files); },
error : function() { self._fill(); }
});
},
// events
events: {
'mousedown' : function(e) { e.preventDefault(); }
},
// fill table
_fill: function(ftp_files) {
if (ftp_files && ftp_files.length > 0) {
// add table
this.$el.find('#upload-ftp-content').html($(this._templateTable()));
// add files to table
var size = 0;
for (key in ftp_files) {
this.add(ftp_files[key]);
this.rows.push(this._add(ftp_files[key]));
size += ftp_files[key].size;
}
// update stats
this.$el.find('#upload-ftp-number').html(ftp_files.length + ' files');
this.$el.find('#upload-ftp-disk').html(Utils.bytesToString (size, true));
// add event handler to select/unselect all
this.$select_all = $('#upload-selectall');
this.$select_all.addClass(this.options.class_add);
var self = this;
this.$select_all.on('click', function() {
var add = self.$select_all.hasClass(self.options.class_add);
for (key in ftp_files) {
var ftp_file = ftp_files[key];
var model_index = self._find(ftp_file);
if(!model_index && add || model_index && !add) {
self.rows[key].trigger('click');
}
}
});
// refresh
self._refresh();
} else {
// add info
this.$el.find('#upload-ftp-content').html($(this._templateInfo()));
}
// hide spinner
this.$el.find('#upload-ftp-wait').hide();
},
// add
add: function(ftp_file) {
_add: function(ftp_file) {
// link this
var self = this;
// create new item
var $it = $(this._templateRow(ftp_file));
// identify icon
var $icon = $it.find('.icon');
// append to table
$(this.el).find('tbody').append($it);
// find model and set initial 'add' icon class
var icon_class = '';
if (this._find(ftp_file)) {
@@ -79,7 +101,7 @@ return Backbone.View.extend({
} else {
icon_class = this.options.class_add;
}
// add icon class
$icon.addClass(icon_class);
@@ -87,10 +109,10 @@ return Backbone.View.extend({
$it.on('click', function() {
// find model
var model_index = self._find(ftp_file);
// update icon
$icon.removeClass();
// add model
if (!model_index) {
// add to uploadbox
@@ -100,43 +122,63 @@ return Backbone.View.extend({
size : ftp_file.size,
path : ftp_file.path
}]);
// add new icon class
$icon.addClass(self.options.class_remove);
} else {
// remove
self.app.collection.remove(model_index);
// add new icon class
$icon.addClass(self.options.class_add);
}
// update select all checkbox
self._refresh();
});
// return dom handler
return $it;
},
// get model index
_find: function(ftp_file) {
// check if exists already
var filtered = this.app.collection.where({file_path : ftp_file.path});
var model_index = null;
for (var key in filtered) {
var item = filtered[key];
if (item.get('status') == 'init' && item.get('file_mode') == 'ftp') {
model_index = item.get('id');
// refresh
_refresh: function() {
var filtered = this.app.collection.where({file_mode : 'ftp'});
this.$select_all.removeClass();
if (filtered.length == 0) {
this.$select_all.addClass(this.options.class_add);
} else {
if (filtered.length == this.rows.length) {
this.$select_all.addClass(this.options.class_remove);
} else {
this.$select_all.addClass(this.options.class_partial);
}
}
return model_index;
},
// get model index
_find: function(ftp_file) {
var item = this.app.collection.findWhere({
file_path : ftp_file.path,
status : 'init',
file_mode : 'ftp'
});
if (item) {
return item.get('id');
}
return null;
},
// template row
_templateRow: function(options) {
return '<tr class="upload-ftp-row" style="cursor: pointer;">' +
return '<tr class="upload-ftp-row">' +
'<td><div class="icon"/></td>' +
'<td style="width: 200px"><p style="width: inherit; word-wrap: break-word;">' + options.path + '</p></td>' +
'<td style="white-space: nowrap;">' + Utils.bytesToString(options.size) + '</td>' +
'<td style="white-space: nowrap;">' + options.ctime + '</td>' +
'<td class="label"><p>' + options.path + '</p></td>' +
'<td class="nonlabel">' + Utils.bytesToString(options.size) + '</td>' +
'<td class="nonlabel">' + options.ctime + '</td>' +
'</tr>';
},
// load table template
_templateTable: function() {
return '<span style="whitespace: nowrap; float: left;">Available files: </span>' +
@@ -149,7 +191,7 @@ return Backbone.View.extend({
'<table class="grid" style="float: left;">' +
'<thead>' +
'<tr>' +
'<th></th>' +
'<th><div id="upload-selectall"></th>' +
'<th>Name</th>' +
'<th>Size</th>' +
'<th>Created</th>' +
@@ -158,14 +200,14 @@ return Backbone.View.extend({
'<tbody></tbody>' +
'</table>';
},
// load table template
_templateInfo: function() {
return '<div class="upload-ftp-warning warningmessage">' +
'Your FTP directory does not contain any files.' +
'</div>';
},
// load html template
_template: function() {
return '<div class="upload-ftp">' +
@@ -174,7 +216,6 @@ return Backbone.View.extend({
'<div id="upload-ftp-content"></div>' +
'<div>';
}
});
});
@@ -14,8 +14,6 @@ var Model = Backbone.Model.extend({
file_type : null,
file_path : '',
percentage : 0,
// settings
space_to_tabs : false,
to_posix_lines : true
}
+85 -105
View File
@@ -4,7 +4,7 @@ define(['utils/utils',
'mvc/upload/upload-settings',
'mvc/ui/ui-popover',
'mvc/ui/ui-select'],
function( Utils,
UploadModel,
UploadSettings,
@@ -18,7 +18,7 @@ return Backbone.View.extend({
options: {
padding : 8
},
// states
status_classes : {
init : 'upload-icon-button fa fa-trash-o',
@@ -27,33 +27,33 @@ return Backbone.View.extend({
success : 'upload-icon-button fa fa-check',
error : 'upload-icon-button fa fa-exclamation-triangle'
},
// handle for settings popover
settings: null,
// genome selector
select_genome : null,
// extension selector
select_extension : null,
// render
initialize: function(app, options) {
// link app
this.app = app;
// link this
var self = this;
// create model
this.model = new UploadModel.Model(options);
// add upload item
this.setElement(this._template(options));
// link item
var it = this.$el;
// append popup to settings icon
this.settings = new Popover.View({
title : 'Upload configuration',
@@ -61,44 +61,57 @@ return Backbone.View.extend({
placement : 'bottom'
});
// initialize default genome through default select field
var default_genome = this.app.select_genome.value();
// select genomes
this.select_genome = new Select.View({
css: 'genome',
onchange : function() {
self.model.set('genome', self.select_genome.value());
onchange : function(genome) {
self.model.set('genome', genome);
self.app.updateGenome(genome, true);
},
data: self.app.list_genomes,
container: it.find('#genome'),
value: self.model.get('genome')
value: default_genome
});
// initialize genome
this.model.set('genome', self.select_genome.value());
// initialize genome
this.model.set('genome', default_genome);
// initialize default extension through default select field
var default_extension = this.app.select_extension.value();
// select extension
this.select_extension = new Select.View({
css: 'extension',
onchange : function() {
self.model.set('extension', self.select_extension.value());
onchange : function(extension) {
self.model.set('extension', extension);
self.app.updateExtension(extension, true);
},
data: self.app.list_extensions,
container: it.find('#extension'),
value: self.model.get('extension')
value: default_extension
});
// initialize extension
this.model.set('extension', self.select_extension.value());
this.model.set('extension', default_extension);
//
// ui events
//
// handle click event
it.find('#symbol').on('click', function() { self._removeRow(); });
// handle extension info popover
it.find('#extension-info').on('click' , function(e) { self._showExtensionInfo(); })
.on('mousedown', function(e) { e.preventDefault(); });
it.find('#extension-info').on('click' , function(e) {
self.app.showExtensionInfo({
$el : $(e.target),
title : self.select_extension.text(),
extension : self.select_extension.value()
});
}).on('mousedown', function(e) { e.preventDefault(); });
// handle settings popover
it.find('#settings').on('click' , function(e) { self._showSettings(); })
@@ -109,12 +122,12 @@ return Backbone.View.extend({
self.model.set('url_paste', $(e.target).val());
self.model.set('file_size', $(e.target).val().length);
});
// handle space to tabs button
it.find('#space_to_tabs').on('change', function(e) {
self.model.set('space_to_tabs', $(e.target).prop('checked'));
});
//
// model events
//
@@ -130,6 +143,9 @@ return Backbone.View.extend({
this.model.on('change:genome', function() {
self._refreshGenome();
});
this.model.on('change:extension', function() {
self._refreshExtension();
});
this.model.on('change:file_size', function() {
self._refreshFileSize();
});
@@ -140,57 +156,58 @@ return Backbone.View.extend({
self.remove();
});
},
// render
render: function() {
// read model
var file_name = this.model.get('file_name');
var file_size = this.model.get('file_size');
var file_mode = this.model.get('file_mode');
// link item
var it = this.$el;
// update title
it.find('#title').html(file_name);
// update info
it.find('#size').html(Utils.bytesToString (file_size));
// remove mode class
it.find('#mode').removeClass()
.addClass('mode');
it.find('#mode')
.removeClass()
.addClass('mode');
// activate text field if file is new
if (file_mode == 'new') {
// get text component
var text = it.find('#text');
// get padding
var padding = this.options.padding;
// get dimensions
var width = it.width() - 2 * padding;
var height = it.height() - padding;
// set dimensions
text.css('width', width + 'px');
text.css('top', height + 'px');
it.height(height + text.height() + 2 * padding);
// show text field
text.show();
// update icon
it.find('#mode').addClass('fa fa-pencil');
}
// file from local disk
if (file_mode == 'local') {
// update icon
it.find('#mode').addClass('fa fa-laptop');
}
// file from ftp
if (file_mode == 'ftp') {
// update icon
@@ -203,7 +220,7 @@ return Backbone.View.extend({
// trigger remove event
this.select_genome.remove();
this.select_extension.remove();
// call the base class remove method
Backbone.View.prototype.remove.apply(this);
},
@@ -211,14 +228,17 @@ return Backbone.View.extend({
//
// handle model events
//
// genome
_refreshGenome: function() {
// update genome info on screen
var genome = this.model.get('genome');
this.select_genome.value(genome);
// update extension
_refreshExtension: function() {
this.select_extension.value(this.model.get('extension'));
},
// update genome
_refreshGenome: function() {
this.select_genome.value(this.model.get('genome'));
},
// progress
_refreshInfo: function() {
// write error message
@@ -229,7 +249,7 @@ return Backbone.View.extend({
this.$el.find('#info').hide();
}
},
// progress
_refreshPercentage : function() {
var percentage = parseInt(this.model.get('percentage'));
@@ -239,29 +259,29 @@ return Backbone.View.extend({
else
this.$el.find('#percentage').html('Adding to history...');
},
// status
_refreshStatus : function() {
// get element
var it = this.$el;
// identify new status
var status = this.model.get('status');
var status_class = this.status_classes[status];
// identify symbol and reset classes
var sy = this.$el.find('#symbol');
sy.removeClass();
// set new status class
sy.addClass(status_class);
// enable form fields
if (status == 'init') {
// select fields
this.select_genome.enable();
this.select_extension.enable();
// default fields
it.find('#text-content').attr('disabled', false);
it.find('#space_to_tabs').attr('disabled', false);
@@ -269,18 +289,18 @@ return Backbone.View.extend({
// select fields
this.select_genome.disable();
this.select_extension.disable();
// default fields
it.find('#text-content').attr('disabled', true);
it.find('#space_to_tabs').attr('disabled', true);
}
// success
if (status == 'success') {
it.addClass('success');
it.find('#percentage').html('100%');
}
// error
if (status == 'error') {
it.addClass('danger');
@@ -297,44 +317,17 @@ return Backbone.View.extend({
//
// handle ui events
//
// remove row
_removeRow: function() {
// get current status
var status = this.model.get('status');
// only remove from queue if not in processing line
if (status == 'init' || status == 'success' || status == 'error') {
this.app.collection.remove(this.model);
}
},
// attach file info popup
_showExtensionInfo : function() {
// initialize
var $el = $(this.el).find('#extension-info');
var extension = this.model.get('extension');
var title = this.select_extension.text();
var description = _.findWhere(this.app.list_extensions, {'id': extension});
// create popup
if (!this.extension_popup) {
this.extension_popup = new Popover.View({
placement: 'bottom',
container: $el
});
}
// show / hide popup
if (!this.extension_popup.visible) {
this.extension_popup.title(title);
this.extension_popup.empty();
this.extension_popup.append(this._templateDescription(description));
this.extension_popup.show();
} else {
this.extension_popup.hide();
}
},
// attach file info popup
_showSettings : function() {
@@ -349,25 +342,12 @@ return Backbone.View.extend({
this.settings.hide();
}
},
// template
_templateDescription: function(options) {
if (options.description) {
var tmpl = options.description;
if (options.description_url) {
tmpl += '&nbsp;(<a href="' + options.description_url + '" target="_blank">read more</a>)';
}
return tmpl;
} else {
return 'There is no description available for this file extension.';
}
},
// template
_template: function(options) {
return '<tr id="upload-item-' + options.id + '" class="upload-item">' +
'<td>' +
'<div style="position: relative;">' +
'<div class="name-column">' +
'<div id="mode"></div>' +
'<div id="title" class="title"></div>' +
'<div id="text" class="text">' +
@@ -400,7 +380,7 @@ return Backbone.View.extend({
'</td>' +
'</tr>';
}
});
});
@@ -8,35 +8,35 @@ return Backbone.View.extend({
class_check : 'upload-icon-button fa fa-check-square-o',
class_uncheck : 'upload-icon-button fa fa-square-o'
},
// render
initialize: function(app) {
// link app
this.app = app;
// link this
var self = this;
// set template
this.setElement(this._template());
// link model
this.model = this.app.model;
// ui event: space to tabs
this.$el.find('#upload-space-to-tabs').on('click', function() {
self._switchState('#upload-space-to-tabs', 'space_to_tabs');
});
// ui event: to posix
this.$el.find('#upload-to-posix-lines').on('click', function() {
self._switchState('#upload-to-posix-lines', 'to_posix_lines');
});
// render
this.render();
},
// events
events: {
'mousedown' : function(e) { e.preventDefault(); }
@@ -47,7 +47,7 @@ return Backbone.View.extend({
// render states
this._renderState('#upload-space-to-tabs', this.model.get('space_to_tabs'));
this._renderState('#upload-to-posix-lines', this.model.get('to_posix_lines'));
// disable options
var $cover = this.$el.find('#upload-settings-cover');
if (this.model.get('status') != 'init') {
@@ -56,7 +56,7 @@ return Backbone.View.extend({
$cover.hide();
}
},
// switch state
_switchState: function (element_id, parameter_id) {
if (this.model.get('status') == 'init') {
@@ -65,7 +65,7 @@ return Backbone.View.extend({
this._renderState(element_id, checked);
}
},
// render state
_renderState: function (element_id, checked) {
// swith icon class
@@ -77,7 +77,7 @@ return Backbone.View.extend({
$it.addClass(this.options.class_uncheck);
}
},
// load template
_template: function() {
return '<div class="upload-settings" style="position: relative;">' +
+223 -116
View File
@@ -6,58 +6,70 @@ define(["utils/utils",
"mvc/upload/upload-ftp",
"mvc/ui/ui-popover",
"mvc/ui/ui-modal",
"mvc/ui/ui-select",
"utils/uploadbox"],
function( Utils,
UploadButton,
UploadModel,
UploadItem,
UploadFtp,
Popover,
Modal
Modal,
Select
) {
// galaxy upload
return Backbone.View.extend({
// options
options : {
nginx_upload_path : ''
nginx_upload_path : ''
},
// default for selection fields
default_extension : 'auto',
default_genome : '?',
// own modal
modal : null,
// button
ui_button : null,
// jquery uploadbox plugin
uploadbox: null,
// current history
current_history: null,
// extension selector
select_extension : null,
// genome selector
select_genome: null,
// current upload size
upload_size: 0,
// extension types
list_extensions :[],
// genomes
list_genomes : [],
// datatype placeholder for auto-detection
auto: {
id : 'auto',
text : 'Auto-detect',
description : 'This system will try to detect the file type automatically. If your file is not detected properly as one of the known formats, it most likely means that it has some format problems (e.g., different number of columns on different rows). You can still coerce the system to set your data to the format you think it should be. You can also upload compressed files, which will automatically be decompressed.'
},
// collection
collection : new UploadModel.Collection(),
// ftp file viewer
ftp : null,
// counter
counter : {
// stats
@@ -65,13 +77,13 @@ return Backbone.View.extend({
success : 0,
error : 0,
running : 0,
// reset stats
reset : function() {
this.announce = this.success = this.error = this.running = 0;
}
},
// initialize
initialize : function(options) {
// link this
@@ -81,7 +93,7 @@ return Backbone.View.extend({
if (options) {
this.options = _.defaults(options, this.options);
}
// create model
this.ui_button = new UploadButton.Model({
icon : 'fa-upload',
@@ -91,7 +103,7 @@ return Backbone.View.extend({
if (e) {
// prevent default
e.preventDefault();
// show
self.show()
}
@@ -102,10 +114,10 @@ return Backbone.View.extend({
}
}
});
// define location
$('#left .unified-panel-header-inner').append((new UploadButton.View(this.ui_button)).$el);
$('.with-upload-button').append((new UploadButton.View(this.ui_button)).$el);
// load extension
var self = this;
Utils.get({
@@ -119,19 +131,19 @@ return Backbone.View.extend({
description_url : datatypes[key].description_url
});
}
// sort
self.list_extensions.sort(function(a, b) {
return a.id > b.id ? 1 : a.id < b.id ? -1 : 0;
return a.text > b.text ? 1 : a.text < b.text ? -1 : 0;
});
// add auto field
if (!self.options.datatypes_disable_auto) {
self.list_extensions.unshift(self.auto);
}
}
});
// load genomes
Utils.get({
url : galaxy_config.root + "api/genomes",
@@ -142,32 +154,26 @@ return Backbone.View.extend({
text : genomes[key][0]
});
}
// sort
self.list_genomes.sort(function(a, b) {
return a.id > b.id ? 1 : a.id < b.id ? -1 : 0;
if (a.id == self.default_genome) { return -1; }
if (b.id == self.default_genome) { return 1; }
return a.text > b.text ? 1 : a.text < b.text ? -1 : 0;
});
}
});
// events
this.collection.on('remove', function(item) {
self._eventRemove(item);
});
this.collection.on('change:genome', function(item) {
var genome = item.get('genome');
self.collection.each(function(item) {
if (item.get('status') == 'init' && item.get('genome') == '?') {
item.set('genome', genome);
}
});
});
},
//
// event triggered by upload button
//
// show/hide upload frame
show: function () {
// wait for galaxy history panel
@@ -176,7 +182,7 @@ return Backbone.View.extend({
window.setTimeout(function() { self.show() }, 500)
return;
}
// create modal
if (!this.modal) {
// make modal
@@ -197,10 +203,10 @@ return Backbone.View.extend({
width : '900',
closing_events : true
});
// set element
this.setElement('#upload-box');
// file upload
var self = this;
this.uploadbox = this.$el.uploadbox({
@@ -211,34 +217,89 @@ return Backbone.View.extend({
error : function(index, file, message) { self._eventError(index, file, message) },
complete : function() { self._eventComplete() }
});
// add ftp file viewer
var button = this.modal.getButton('Choose FTP file');
this.ftp = new Popover.View({
title : 'FTP files',
container : button
});
// select extension
this.select_extension = new Select.View({
css : 'header-selection',
data : this.list_extensions,
container : this.$el.parent().find('#header-extension'),
value : this.default_extension,
onchange : function(extension) {
self.updateExtension(extension);
}
});
// handle extension info popover
self.$el.parent().find('#header-extension-info').on('click' , function(e) {
self.showExtensionInfo({
$el : $(e.target),
title : self.select_extension.text(),
extension : self.select_extension.value(),
placement : 'top'
});
}).on('mousedown', function(e) { e.preventDefault(); });
// genome extension
this.select_genome = new Select.View({
css : 'header-selection',
data : this.list_genomes,
container : this.$el.parent().find('#header-genome'),
value : this.default_genome,
onchange : function(genome) {
self.updateGenome(genome);
}
});
}
// show modal
this.modal.show();
// refresh
this._updateUser();
// setup info
this._updateScreen();
},
showExtensionInfo : function(options) {
// initialize
var self = this;
var $el = options.$el;
var extension = options.extension;
var title = options.title;
var description = _.findWhere(self.list_extensions, {'id': extension});
// create popup
this.extension_popup && this.extension_popup.remove();
this.extension_popup = new Popover.View({
placement: options.placement || 'bottom',
container: $el,
destroy: true
});
// add content and show popup
this.extension_popup.title(title);
this.extension_popup.empty();
this.extension_popup.append(this._templateDescription(description));
this.extension_popup.show();
},
//
// events triggered by collection
//
// remove item from upload list
_eventRemove : function(item) {
// update status
var status = item.get('status');
// reduce counter
if (status == 'success') {
this.counter.success--;
@@ -247,26 +308,26 @@ return Backbone.View.extend({
} else {
this.counter.announce--;
}
// show on screen info
this._updateScreen();
// remove from queue
this.uploadbox.remove(item.id);
},
//
// events triggered by the upload box plugin
//
// a new file has been dropped/selected through the uploadbox plugin
_eventAnnounce : function(index, file, message) {
// update counter
this.counter.announce++;
// update screen
this._updateScreen();
// create view/model
var upload_item = new UploadItem(this, {
id : index,
@@ -275,25 +336,25 @@ return Backbone.View.extend({
file_mode : file.mode,
file_path : file.path
});
// add to collection
this.collection.add(upload_item.model);
// add upload item element to table
$(this.el).find('tbody:first').append(upload_item.$el);
// render
upload_item.render();
},
// the uploadbox plugin is initializing the upload for this file
_eventInitialize : function(index, file, message) {
// get element
var it = this.collection.get(index);
// update status
it.set('status', 'running');
// get configuration
var file_name = it.get('file_name');
var file_path = it.get('file_path');
@@ -303,113 +364,113 @@ return Backbone.View.extend({
var url_paste = it.get('url_paste');
var space_to_tabs = it.get('space_to_tabs');
var to_posix_lines = it.get('to_posix_lines');
// validate
if (!url_paste && !(file.size > 0))
return null;
// configure uploadbox
this.uploadbox.configure({url : this.options.nginx_upload_path});
// local files
if (file_mode == 'local') {
this.uploadbox.configure({paramname : 'files_0|file_data'});
} else {
this.uploadbox.configure({paramname : null});
}
// configure tool
tool_input = {};
// new files
if (file_mode == 'new') {
tool_input['files_0|url_paste'] = url_paste;
}
// files from ftp
if (file_mode == 'ftp') {
tool_input['files_0|ftp_files'] = file_path;
}
// add common configuration
tool_input['dbkey'] = genome;
tool_input['file_type'] = extension;
tool_input['files_0|type'] = 'upload_dataset';
tool_input['space_to_tabs'] = space_to_tabs;
tool_input['to_posix_lines'] = to_posix_lines;
tool_input['files_0|space_to_tab'] = space_to_tabs && 'Yes' || null;
tool_input['files_0|to_posix_lines'] = to_posix_lines && 'Yes' || null;
// setup data
data = {};
data['history_id'] = this.current_history;
data['tool_id'] = 'upload1';
data['inputs'] = JSON.stringify(tool_input);
// return additional data to be send with file
return data;
},
// progress
_eventProgress : function(index, file, percentage) {
// set progress for row
var it = this.collection.get(index);
it.set('percentage', percentage);
// update ui button
this.ui_button.set('percentage', this._upload_percentage(percentage, file.size));
this.ui_button.set('percentage', this._uploadPercentage(percentage, file.size));
},
// success
_eventSuccess : function(index, file, message) {
// update status
var it = this.collection.get(index);
it.set('percentage', 100);
it.set('status', 'success');
// file size
var file_size = it.get('file_size');
// update ui button
this.ui_button.set('percentage', this._upload_percentage(100, file_size));
this.ui_button.set('percentage', this._uploadPercentage(100, file_size));
// update completed
this.upload_completed += file_size * 100;
// update counter
this.counter.announce--;
this.counter.success++;
// update on screen info
this._updateScreen();
// update galaxy history
Galaxy.currHistoryPanel.refreshContents();
},
// error
_eventError : function(index, file, message) {
// get element
var it = this.collection.get(index);
// update status
it.set('percentage', 100);
it.set('status', 'error');
it.set('info', message);
// update ui button
this.ui_button.set('percentage', this._upload_percentage(100, file.size));
this.ui_button.set('percentage', this._uploadPercentage(100, file.size));
this.ui_button.set('status', 'danger');
// update completed
this.upload_completed += file.size * 100;
// update counter
this.counter.announce--;
this.counter.error++;
// update on screen info
this._updateScreen();
},
// queue is done
_eventComplete: function() {
// reset queued upload to initial status
@@ -418,16 +479,16 @@ return Backbone.View.extend({
item.set('status', 'init');
}
});
// update running
this.counter.running = 0;
this._updateScreen();
},
//
// events triggered by this view
//
_eventFtp: function() {
// check if popover is visible
if (!this.ftp.visible) {
@@ -456,7 +517,7 @@ return Backbone.View.extend({
if (this.counter.announce == 0 || this.counter.running > 0) {
return;
}
// reset current size
var self = this;
this.upload_size = 0;
@@ -468,19 +529,19 @@ return Backbone.View.extend({
self.upload_size += item.get('file_size');
}
});
// reset progress
this.ui_button.set('percentage', 0);
this.ui_button.set('status', 'success');
// update running
this.counter.running = this.counter.announce;
this._updateScreen();
// initiate upload procedure in plugin
this.uploadbox.start();
},
// pause upload process
_eventStop : function() {
// check
@@ -493,32 +554,56 @@ return Backbone.View.extend({
// request pause
this.uploadbox.stop();
// set html content
$('#upload-info').html('Queue will pause after completing the current file...');
},
// remove all
_eventReset : function() {
// make sure queue is not running
if (this.counter.running == 0){
// reset collection
this.collection.reset();
// reset counter
this.counter.reset();
// show on screen info
this._updateScreen();
// remove from queue
this.uploadbox.reset();
// reset value for universal type drop-down
this.select_extension.value(this.default_extension);
this.select_genome.value(this.default_genome);
// reset button
this.ui_button.set('percentage', 0);
}
},
// update extension for all models
updateExtension: function(extension, defaults_only) {
var self = this;
this.collection.each(function(item) {
if (item.get('status') == 'init' && (item.get('extension') == self.default_extension || !defaults_only)) {
item.set('extension', extension);
}
});
},
// update genome for all models
updateGenome: function(genome, defaults_only) {
var self = this;
this.collection.each(function(item) {
if (item.get('status') == 'init' && (item.get('genome') == self.default_genome || !defaults_only)) {
item.set('genome', genome);
}
});
},
// update uset
_updateUser: function() {
// backup current history
@@ -528,13 +613,13 @@ return Backbone.View.extend({
this.current_history = Galaxy.currHistoryPanel.model.get('id');
}
},
// set screen
_updateScreen: function () {
/*
update on screen info
*/
// check default message
if(this.counter.announce == 0){
if (this.uploadbox.compatible())
@@ -547,20 +632,20 @@ return Backbone.View.extend({
else
message = 'Please wait...' + this.counter.announce + ' out of ' + this.counter.running + ' remaining.';
}
// set html content
$('#upload-info').html(message);
/*
update button status
*/
// update reset button
if (this.counter.running == 0 && this.counter.announce + this.counter.success + this.counter.error > 0)
this.modal.enableButton('Reset');
else
this.modal.disableButton('Reset');
// update upload button
if (this.counter.running == 0 && this.counter.announce > 0)
this.modal.enableButton('Start');
@@ -572,7 +657,7 @@ return Backbone.View.extend({
this.modal.enableButton('Pause');
else
this.modal.disableButton('Pause');
// select upload button
if (this.counter.running == 0){
this.modal.enableButton('Choose local file');
@@ -583,14 +668,14 @@ return Backbone.View.extend({
this.modal.disableButton('Choose FTP file');
this.modal.disableButton('Paste/Fetch data');
}
// ftp button
if (this.current_user && this.options.ftp_upload_dir && this.options.ftp_upload_site) {
this.modal.showButton('Choose FTP file');
} else {
this.modal.hideButton('Choose FTP file');
}
// table visibility
if (this.counter.announce + this.counter.success + this.counter.error > 0) {
$(this.el).find('#upload-table').show();
@@ -600,13 +685,29 @@ return Backbone.View.extend({
},
// calculate percentage of all queued uploads
_upload_percentage: function(percentage, size) {
_uploadPercentage: function(percentage, size) {
return (this.upload_completed + (percentage * size)) / this.upload_size;
},
// template for extensions description
_templateDescription: function(options) {
if (options.description) {
var tmpl = options.description;
if (options.description_url) {
tmpl += '&nbsp;(<a href="' + options.description_url + '" target="_blank">read more</a>)';
}
return tmpl;
} else {
return 'There is no description available for this file extension.';
}
},
// load html template
_template: function(id, idInfo) {
return '<div id="' + id + '" class="upload-box">' +
return '<div class="upload-top">' +
'<h6 id="' + idInfo + '" class="upload-info"></h6>' +
'</div>' +
'<div id="' + id + '" class="upload-box">' +
'<table id="upload-table" class="table table-striped" style="display: none;">' +
'<thead>' +
'<tr>' +
@@ -622,7 +723,13 @@ return Backbone.View.extend({
'<tbody></tbody>' +
'</table>' +
'</div>' +
'<h6 id="' + idInfo + '" class="upload-info"></h6>';
'<div id="upload-header" class="upload-header">' +
'<span class="header-title">Type (set all):</span>' +
'<span id="header-extension"/>' +
'<span id="header-extension-info" class="upload-icon-button fa fa-search"/> ' +
'<span class="header-title">Genome (set all):</span>' +
'<span id="header-genome"/>' +
'</div>';
}
});
@@ -1,24 +1,13 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['panel_section'] = template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression;
buffer += "<div class=\"toolSectionTitle\" id=\"title_";
if (stack1 = helpers.id) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.id; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\">\n <a href=\"javascript:void(0)\"><span>";
if (stack1 = helpers.name) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.name; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "</span></a>\n</div>\n<div id=\"";
if (stack1 = helpers.id) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.id; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
templates['panel_section'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return "<div class=\"toolSectionTitle\" id=\"title_"
+ escapeExpression(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"id","hash":{},"data":data}) : helper)))
+ "\">\n <a href=\"javascript:void(0)\"><span>"
+ escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"name","hash":{},"data":data}) : helper)))
+ "</span></a>\n</div>\n<div id=\""
+ escapeExpression(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"id","hash":{},"data":data}) : helper)))
+ "\" class=\"toolSectionBody\" style=\"display: none; \">\n <div class=\"toolSectionBg\"></div>\n<div>";
return buffer;
});
},"useData":true});
})();
@@ -1,49 +1,26 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['tool_form'] = template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
function program1(depth0,data) {
var buffer = "", stack1;
buffer += "\n <div class=\"form-row\">\n <label for=\"";
if (stack1 = helpers.name) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.name; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\">";
if (stack1 = helpers.label) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.label; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
templates['tool_form'] = template({"1":function(depth0,helpers,partials,data) {
var stack1, helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, buffer = " <div class=\"form-row\">\n <label for=\""
+ escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"name","hash":{},"data":data}) : helper)))
+ "\">"
+ escapeExpression(((helper = (helper = helpers.label || (depth0 != null ? depth0.label : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"label","hash":{},"data":data}) : helper)))
+ ":</label>\n <div class=\"form-row-input\">\n ";
if (stack1 = helpers.html) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.html; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\n </div>\n <div class=\"toolParamHelp\" style=\"clear: both;\">\n ";
if (stack1 = helpers.help) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.help; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\n </div>\n <div style=\"clear: both;\"></div>\n </div>\n ";
return buffer;
}
buffer += "<div class=\"toolFormTitle\">";
if (stack1 = helpers.name) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.name; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ " (version ";
if (stack1 = helpers.version) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.version; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ ")</div>\n <div class=\"toolFormBody\">\n ";
stack1 = helpers.each.call(depth0, depth0.inputs, {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
if(stack1 || stack1 === 0) { buffer += stack1; }
buffer += "\n </div>\n <div class=\"form-row form-actions\">\n <input type=\"submit\" class=\"btn btn-primary\" name=\"runtool_btn\" value=\"Execute\">\n</div>\n<div class=\"toolHelp\">\n <div class=\"toolHelpBody\">";
if (stack1 = helpers.help) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.help; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
stack1 = ((helper = (helper = helpers.html || (depth0 != null ? depth0.html : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"html","hash":{},"data":data}) : helper));
if (stack1 != null) { buffer += stack1; }
return buffer + "\n </div>\n <div class=\"toolParamHelp\" style=\"clear: both;\">\n "
+ escapeExpression(((helper = (helper = helpers.help || (depth0 != null ? depth0.help : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"help","hash":{},"data":data}) : helper)))
+ "\n </div>\n <div style=\"clear: both;\"></div>\n </div>\n";
},"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
var stack1, helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, buffer = "<div class=\"toolFormTitle\">"
+ escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"name","hash":{},"data":data}) : helper)))
+ " (version "
+ escapeExpression(((helper = (helper = helpers.version || (depth0 != null ? depth0.version : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"version","hash":{},"data":data}) : helper)))
+ ")</div>\n <div class=\"toolFormBody\">\n";
stack1 = helpers.each.call(depth0, (depth0 != null ? depth0.inputs : depth0), {"name":"each","hash":{},"fn":this.program(1, data),"inverse":this.noop,"data":data});
if (stack1 != null) { buffer += stack1; }
return buffer + " </div>\n <div class=\"form-row form-actions\">\n <input type=\"submit\" class=\"btn btn-primary\" name=\"runtool_btn\" value=\"Execute\">\n</div>\n<div class=\"toolHelp\">\n <div class=\"toolHelpBody\">"
+ escapeExpression(((helper = (helper = helpers.help || (depth0 != null ? depth0.help : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"help","hash":{},"data":data}) : helper)))
+ "</div>\n</div>";
return buffer;
});
},"useData":true});
})();
@@ -1,35 +1,18 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['tool_link'] = template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression;
buffer += "<a class=\"";
if (stack1 = helpers.id) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.id; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ " tool-link\" href=\"";
if (stack1 = helpers.link) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.link; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\" target=\"";
if (stack1 = helpers.target) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.target; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\" minsizehint=\"";
if (stack1 = helpers.min_width) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.min_width; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\">";
if (stack1 = helpers.name) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.name; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "</a> ";
if (stack1 = helpers.description) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.description; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1);
return buffer;
});
templates['tool_link'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return "<a class=\""
+ escapeExpression(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"id","hash":{},"data":data}) : helper)))
+ " tool-link\" href=\""
+ escapeExpression(((helper = (helper = helpers.link || (depth0 != null ? depth0.link : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"link","hash":{},"data":data}) : helper)))
+ "\" target=\""
+ escapeExpression(((helper = (helper = helpers.target || (depth0 != null ? depth0.target : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"target","hash":{},"data":data}) : helper)))
+ "\" minsizehint=\""
+ escapeExpression(((helper = (helper = helpers.min_width || (depth0 != null ? depth0.min_width : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"min_width","hash":{},"data":data}) : helper)))
+ "\">"
+ escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"name","hash":{},"data":data}) : helper)))
+ "</a> "
+ escapeExpression(((helper = (helper = helpers.description || (depth0 != null ? depth0.description : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"description","hash":{},"data":data}) : helper)));
},"useData":true});
})();
@@ -1,20 +1,11 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['tool_search'] = template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression;
buffer += "<input type=\"text\" name=\"query\" value=\"";
if (stack1 = helpers.search_hint_string) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.search_hint_string; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
+ "\" id=\"tool-search-query\" autocomplete=\"off\" class=\"search-query parent-width\" />\n<a id=\"search-clear-btn\" title=\"clear search (esc)\"> </a>\n<img src=\"";
if (stack1 = helpers.spinner_url) { stack1 = stack1.call(depth0, {hash:{},data:data}); }
else { stack1 = depth0.spinner_url; stack1 = typeof stack1 === functionType ? stack1.apply(depth0) : stack1; }
buffer += escapeExpression(stack1)
templates['tool_search'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
var helper, functionType="function", helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression;
return "<input type=\"text\" name=\"query\" value=\""
+ escapeExpression(((helper = (helper = helpers.search_hint_string || (depth0 != null ? depth0.search_hint_string : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"search_hint_string","hash":{},"data":data}) : helper)))
+ "\" id=\"tool-search-query\" autocomplete=\"off\" class=\"search-query parent-width\" />\n<a id=\"search-clear-btn\" title=\"clear search (esc)\"> </a>\n<img src=\""
+ escapeExpression(((helper = (helper = helpers.spinner_url || (depth0 != null ? depth0.spinner_url : depth0)) != null ? helper : helperMissing),(typeof helper === functionType ? helper.call(depth0, {"name":"spinner_url","hash":{},"data":data}) : helper)))
+ "\" id=\"search-spinner\" class=\"search-spinner\"/>";
return buffer;
});
},"useData":true});
})();
-4
View File
@@ -12,7 +12,6 @@
url : '',
paramname : 'content',
maxfilesize : 2048,
maxfilenumber : 50,
dragover : function() {},
dragleave : function() {},
announce : function() {},
@@ -129,9 +128,6 @@
// add files to queue
for (var i = 0; i < files.length; i++)
{
// check
if(queue_length >= opts.maxfilenumber)
break;
// new identifier
var index = String(queue_index++);
+51 -2
View File
@@ -6,6 +6,18 @@
// dependencies
define(["libs/underscore"], function(_) {
/** Traverse through json
*/
function deepeach(dict, callback) {
for (var i in dict) {
var d = dict[i];
if (d && typeof(d) == "object") {
callback(d);
deepeach(d, callback);
}
}
}
/**
* Sanitize/escape a string
* @param{String} content - Content to be sanitized
@@ -14,6 +26,40 @@ function sanitize(content) {
return $('<div/>').text(content).html();
};
/**
* Validate atomic values or list of values
* usually used for selectable options
* @param{String} value - Value or list to be validated
*/
function validate (value) {
if (!(value instanceof Array)) {
value = [value];
}
for (var i in value) {
if (['__null__', '__undefined__', 'None', null, undefined].indexOf(value[i]) > -1) {
return false;
}
}
return true;
};
/**
* Convert list to pretty string
* @param{String} lst - List of strings to be converted in human readable list sentence
*/
function textify(lst) {
var lst = lst.toString();
if (lst) {
lst = lst.replace(/,/g, ', ');
var pos = lst.lastIndexOf(', ');
if (pos != -1) {
lst = lst.substr(0, pos) + ' or ' + lst.substr(pos+1);
}
return lst;
}
return '';
};
/**
* Request handler for GET
* @param{String} url - Url request is made to
@@ -65,7 +111,7 @@ function request (options) {
} else {
ajaxConfig.url += '&';
}
ajaxConfig.url = ajaxConfig.url + $.param(ajaxConfig.data);
ajaxConfig.url = ajaxConfig.url + $.param(ajaxConfig.data, true);
ajaxConfig.data = null;
} else {
ajaxConfig.dataType = 'json';
@@ -212,7 +258,10 @@ return {
time: time,
wrap: wrap,
request: request,
sanitize: sanitize
sanitize: sanitize,
textify: textify,
validate: validate,
deepeach: deepeach
};
});
+2 -4
View File
@@ -12,7 +12,6 @@ require(
[
// load js libraries
'utils/utils',
'mvc/ui/icon-button',
'libs/jquery/jquery.event.drag',
'libs/jquery/jquery.event.hover',
'libs/jquery/jquery.mousewheel',
@@ -32,9 +31,8 @@ require(
});
// trackster viewer
define( ["libs/underscore", "base", "viz/trackster/tracks", "viz/visualization"],
function(_, base, tracks, visualization) {
define( ["libs/underscore", "base", "viz/trackster/tracks", "viz/visualization", "mvc/ui/icon-button" ],
function(_, base, tracks, visualization, mod_icon_btn) {
/**
* User interface controls for trackster
*/
+6 -1
View File
@@ -7,8 +7,13 @@
],
"devDependencies": {
"grunt": "^0.4.5",
"grunt-bower-install-simple": "^1.1.0",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-exec": "^0.4.6"
"grunt-exec": "^0.4.6",
"grunt-webpack": "^1.0.8",
"i18n-webpack-plugin": "^0.2.7",
"webpack": "^1.5.1",
"webpack-dev-server": "^1.7.0"
}
}
+23 -10
View File
@@ -3,6 +3,7 @@
<registration converters_path="lib/galaxy/datatypes/converters" display_path="display_applications">
<datatype extension="ab1" type="galaxy.datatypes.binary:Ab1" mimetype="application/octet-stream" display_in_upload="true" description="A binary sequence file in 'ab1' format with a '.ab1' file extension. You must manually select this 'File Format' when uploading the file." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Ab1"/>
<datatype extension="afg" type="galaxy.datatypes.assembly:Amos" display_in_upload="false" />
<datatype extension="arff" type="galaxy.datatypes.text:Arff" mimetype="text/plain" display_in_upload="True" />
<datatype extension="asn1" type="galaxy.datatypes.data:GenericAsn1" mimetype="text/plain" display_in_upload="true" />
<datatype extension="asn1-binary" type="galaxy.datatypes.binary:GenericAsn1Binary" mimetype="application/octet-stream" display_in_upload="true" />
<datatype extension="axt" type="galaxy.datatypes.sequence:Axt" display_in_upload="true" description="blastz pairwise alignment format. Each alignment block in an axt file contains three lines: a summary line and 2 sequence lines. Blocks are separated from one another by blank lines. The summary line contains chromosomal position and size information about the alignment. It consists of 9 required fields." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Axt"/>
@@ -112,7 +113,7 @@
<converter file="interval_to_bed12_converter.xml" target_datatype="bed12"/>
<converter file="interval_to_bgzip_converter.xml" target_datatype="bgzip"/>
<converter file="interval_to_tabix_converter.xml" target_datatype="tabix" depends_on="bgzip"/>
<converter file="interval_to_bigwig_converter.xml" target_datatype="bigwig"/>
<converter file="interval_to_bigwig_converter.xml" target_datatype="bigwig"/>
<!-- <display file="ucsc/interval_as_bed.xml" inherit="True" /> -->
<display file="ensembl/ensembl_interval_as_bed.xml" inherit="True"/>
<display file="gbrowse/gbrowse_interval_as_bed.xml" inherit="True"/>
@@ -139,6 +140,9 @@
<datatype extension="rgb" type="galaxy.datatypes.images:Rgb" mimetype="image/rgb"/>
<datatype extension="pbm" type="galaxy.datatypes.images:Pbm" mimetype="image/pbm"/>
<datatype extension="pgm" type="galaxy.datatypes.images:Pgm" mimetype="image/pgm"/>
<datatype extension="rna_eps" type="galaxy.datatypes.sequence:RNADotPlotMatrix" mimetype="image/eps" display_in_upload="True"/>
<datatype extension="searchgui_archive" type="galaxy.datatypes.binary:CompressedArchive" subclass="True" display_in_upload="True"/>
<datatype extension="peptideshaker_archive" type="galaxy.datatypes.binary:CompressedArchive" subclass="True" display_in_upload="True"/>
<datatype extension="eps" type="galaxy.datatypes.images:Eps" mimetype="image/eps"/>
<datatype extension="rast" type="galaxy.datatypes.images:Rast" mimetype="image/rast"/>
<datatype extension="laj" type="galaxy.datatypes.images:Laj"/>
@@ -150,11 +154,11 @@
<datatype extension="mafcustomtrack" type="galaxy.datatypes.sequence:MafCustomTrack">
<display file="ucsc/maf_customtrack.xml" />
</datatype>
<datatype extension="encodepeak" type="galaxy.datatypes.interval:ENCODEPeak" display_in_upload="True">
<converter file="encodepeak_to_tabix_converter.xml" target_datatype="tabix" depends_on="bgzip"/>
<converter file="encodepeak_to_bgzip_converter.xml" target_datatype="bgzip"/>
<converter file="bed_gff_or_vcf_to_bigwig_converter.xml" target_datatype="bigwig"/>
</datatype>
<datatype extension="encodepeak" type="galaxy.datatypes.interval:ENCODEPeak" display_in_upload="True">
<converter file="encodepeak_to_tabix_converter.xml" target_datatype="tabix" depends_on="bgzip"/>
<converter file="encodepeak_to_bgzip_converter.xml" target_datatype="bgzip"/>
<converter file="bed_gff_or_vcf_to_bigwig_converter.xml" target_datatype="bigwig"/>
</datatype>
<datatype extension="pdf" type="galaxy.datatypes.images:Pdf" mimetype="application/pdf"/>
<datatype extension="pileup" type="galaxy.datatypes.tabular:Pileup" display_in_upload="true">
<converter file="interval_to_bgzip_converter.xml" target_datatype="bgzip"/>
@@ -170,17 +174,23 @@
<datatype extension="qual454" type="galaxy.datatypes.qualityscore:QualityScore454" display_in_upload="true"/>
<datatype extension="Roadmaps" type="galaxy.datatypes.assembly:Roadmaps" display_in_upload="false"/>
<datatype extension="sam" type="galaxy.datatypes.tabular:Sam" display_in_upload="true">
<converter file="sam_to_bam.xml" target_datatype="bam"/>
<converter file="sam_to_bigwig_converter.xml" target_datatype="bigwig"/>
</datatype>
<converter file="sam_to_bam.xml" target_datatype="bam"/>
<converter file="sam_to_bigwig_converter.xml" target_datatype="bigwig"/>
</datatype>
<datatype extension="scf" type="galaxy.datatypes.binary:Scf" mimetype="application/octet-stream" display_in_upload="true" description="A binary sequence file in 'scf' format with a '.scf' file extension. You must manually select this 'File Format' when uploading the file." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Scf"/>
<datatype extension="Sequences" type="galaxy.datatypes.assembly:Sequences" display_in_upload="false"/>
<datatype extension="snpeffdb" type="galaxy.datatypes.text:SnpEffDb" display_in_upload="True"/>
<datatype extension="snpsiftdbnsfp" type="galaxy.datatypes.txt:SnpSiftDbNSFP" display_in_upload="True"/>
<datatype extension="dbnsfp.tabular" type="galaxy.datatypes.tabular:Tabular" subclass="True" display_in_upload="True">
<converter file="tabular_to_dbnsfp.xml" target_datatype="snpsiftdbnsfp"/>
</datatype>
<datatype extension="sff" type="galaxy.datatypes.binary:Sff" mimetype="application/octet-stream" display_in_upload="true" description="A binary file in 'Standard Flowgram Format' with a '.sff' file extension." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Sff"/>
<datatype extension="svg" type="galaxy.datatypes.images:Image" mimetype="image/svg+xml"/>
<datatype extension="taxonomy" type="galaxy.datatypes.tabular:Taxonomy" display_in_upload="true"/>
<datatype extension="tabular" type="galaxy.datatypes.tabular:Tabular" display_in_upload="true" description="Any data in tab delimited format (tabular)." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Tabular_.28tab_delimited.29"/>
<datatype extension="twobit" type="galaxy.datatypes.binary:TwoBit" mimetype="application/octet-stream" display_in_upload="true"/>
<datatype extension="sqlite" type="galaxy.datatypes.binary:SQlite" mimetype="application/octet-stream" display_in_upload="true"/>
<datatype extension="sqlite" type="galaxy.datatypes.binary:SQlite" mimetype="application/octet-stream" display_in_upload="true"/>
<datatype extension="gemini.sqlite" type="galaxy.datatypes.binary:GeminiSQLite" mimetype="application/octet-stream" display_in_upload="True" />
<datatype extension="txt" type="galaxy.datatypes.data:Text" display_in_upload="true" description="Any text file." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Plain_text"/>
<datatype extension="linecount" type="galaxy.datatypes.data:LineCount" display_in_upload="false"/>
<datatype extension="memexml" type="galaxy.datatypes.xml:MEMEXml" mimetype="application/xml" display_in_upload="true"/>
@@ -270,6 +280,7 @@
-->
<sniffer type="galaxy.datatypes.tabular:Vcf"/>
<sniffer type="galaxy.datatypes.binary:TwoBit"/>
<sniffer type="galaxy.datatypes.binary:GeminiSQLite"/>
<sniffer type="galaxy.datatypes.binary:SQlite"/>
<sniffer type="galaxy.datatypes.binary:Bam"/>
<sniffer type="galaxy.datatypes.binary:Sff"/>
@@ -298,8 +309,10 @@
<sniffer type="galaxy.datatypes.data:Newick"/>
<sniffer type="galaxy.datatypes.data:Nexus"/>
<sniffer type="galaxy.datatypes.text:Obo"/>
<sniffer type="galaxy.datatypes.text:Arff"/>
<sniffer type="galaxy.datatypes.text:Ipynb"/>
<sniffer type="galaxy.datatypes.text:Json"/>
<sniffer type="galaxy.datatypes.sequence:RNADotPlotMatrix"/>
<sniffer type="galaxy.datatypes.images:Jpg"/>
<sniffer type="galaxy.datatypes.images:Png"/>
<sniffer type="galaxy.datatypes.images:Tiff"/>
+199 -125
View File
@@ -8,7 +8,7 @@
# Throughout this sample configuration file, except where stated otherwise,
# uncommented values override the default if left unset, whereas commented
# values are set to the default value. Relative paths are relative to the root
# values are set to the default value. Relative paths are relative to the root
# Galaxy directory.
#
# Examples of many of these options are explained in more detail in the wiki:
@@ -42,7 +42,8 @@ use_threadpool = True
# Number of threads in the web server thread pool.
#threadpool_workers = 10
# Set the number of seconds a thread can work before you should kill it (assuming it will never finish) to 3 hours.
# Set the number of seconds a thread can work before you should kill it
# (assuming it will never finish) to 3 hours. Default is 600 (10 minutes).
threadpool_kill_thread_limit = 10800
# ---- Filters --------------------------------------------------------------
@@ -117,10 +118,10 @@ paste.app_factory = galaxy.web.buildapp:app_factory
#database_query_profiling_proxy = False
# By default, Galaxy will use the same database to track user data and
# tool shed install data. There are many situtations in which it is
# tool shed install data. There are many situtations in which it is
# valuable to seperate these - for instance bootstrapping fresh Galaxy
# instances with pretested installs. The following optin can be used to
# separate the tool shed install database (all other options listed above
# instances with pretested installs. The following optin can be used to
# separate the tool shed install database (all other options listed above
# but prefixed with install_ are also available).
#install_database_connection = sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE
@@ -138,26 +139,29 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# config/tool_conf.xml does not exist).
#tool_config_file = config/tool_conf.xml,config/shed_tool_conf.xml
# Enable / disable checking if any tools defined in the above non-shed tool_config_files
# (i.e., tool_conf.xml) have been migrated from the Galaxy code distribution to the Tool
# Shed. This setting should generally be set to False only for development Galaxy environments
# that are often rebuilt from scratch where migrated tools do not need to be available in the
# Galaxy tool panel. If the following setting remains commented, the default setting will be True.
# Enable / disable checking if any tools defined in the above non-shed
# tool_config_files (i.e., tool_conf.xml) have been migrated from the Galaxy
# code distribution to the Tool Shed. This setting should generally be set to
# False only for development Galaxy environments that are often rebuilt from
# scratch where migrated tools do not need to be available in the Galaxy tool
# panel. If the following setting remains commented, the default setting will
# be True.
#check_migrate_tools = True
# Tool config maintained by tool migration scripts. If you use the migration
# Tool config maintained by tool migration scripts. If you use the migration
# scripts to install tools that have been migrated to the tool shed upon a new
# release, they will be added to this tool config file.
#migrated_tools_config = migrated_tools_conf.xml
#migrated_tools_config = config/migrated_tools_conf.xml
# File that contains the XML section and tool tags from all tool panel config
# files integrated into a single file that defines the tool panel layout. This
# file can be changed by the Galaxy administrator to alter the layout of the
# tool panel. If not present, Galaxy will create it.
# tool panel. If not present, Galaxy will create it.
#integrated_tool_panel_config = integrated_tool_panel.xml
# Default path to the directory containing the tools defined in tool_conf.xml.
# Other tool config files must include the tool_path as an attribute in the <toolbox> tag.
# Other tool config files must include the tool_path as an attribute in the
# <toolbox> tag.
#tool_path = tools
# Path to the directory in which tool dependencies are placed. This is used by
@@ -172,33 +176,36 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# install from in the admin interface (.sample used if default does not exist).
#tool_sheds_config_file = config/tool_sheds_conf.xml
# If the following option is set to True - Galaxy will monitor individual tools
# and tool directories specified in tool_conf.xml for changes and reload these
# tools. Watchdog must be installed and available to Galaxy to use this option.
# See https://pypi.python.org/pypi/watchdog.
# Enable monitoring of tools and tool directories listed in any tool config file
# specified in tool_config_file option. If changes are found, tools are
# automatically reloaded.
# Watchdog ( https://pypi.python.org/pypi/watchdog ) must be installed and
# available to Galaxy to use this option.
#watch_tools = False
# Enable automatic polling of relative tool sheds to see if any updates
# are available for installed repositories. Ideally only one Galaxy
# are available for installed repositories. Ideally only one Galaxy
# server process should be able to check for repository updates. The
# setting for hours_between_check should be an integer between 1 and 24.
#enable_tool_shed_check = False
#hours_between_check = 12
# Enable use of an in-memory registry with bi-directional relationships
# between repositories (i.e., in addition to lists of dependencies for a
# repository, keep an in-memory registry of dependent items for each repository.
# Enable use of an in-memory registry with bi-directional relationships between
# repositories (i.e., in addition to lists of dependencies for a repository,
# keep an in-memory registry of dependent items for each repository.
#manage_dependency_relationships = False
# XML config file that contains data table entries for the
# ToolDataTableManager. This file is manually # maintained by the Galaxy
# ToolDataTableManager. This file is manually # maintained by the Galaxy
# administrator (.sample used if default does not exist).
#tool_data_table_config_path = config/tool_data_table_conf.xml
# XML config file that contains additional data table entries for the ToolDataTableManager. This file
# is automatically generated based on the current installed tool shed repositories that contain valid
# tool_data_table_conf.xml.sample files. At the time of installation, these entries are automatically
# added to the following file, which is parsed and applied to the ToolDataTableManager at server start up.
# XML config file that contains additional data table entries for the
# ToolDataTableManager. This file is automatically generated based on the
# current installed tool shed repositories that contain valid
# tool_data_table_conf.xml.sample files. At the time of installation, these
# entries are automatically added to the following file, which is parsed and
# applied to the ToolDataTableManager at server start up.
#shed_tool_data_table_config = config/shed_tool_data_table_conf.xml
# Directory where data used by tools is located, see the samples in that
@@ -213,24 +220,26 @@ paste.app_factory = galaxy.web.buildapp:app_factory
#len_file_path = tool-data/shared/ucsc/chrom
# Datatypes config file(s), defines what data (file) types are available in
# Galaxy (.sample is used if default does not exist). If a datatype appears in
# multiple files - the last definition is used (though the first sniffer is used
# Galaxy (.sample is used if default does not exist). If a datatype appears in
# multiple files, the last definition is used (though the first sniffer is used
# so limit sniffer definitions to one file).
#datatypes_config_file = config/datatypes_conf.xml
# Disable the 'Auto-detect' option for file uploads
#datatypes_disable_auto = False
# Visualizations config directory: where to look for individual visualization plugins.
# The path is relative to the Galaxy root dir. To use an absolute path begin the path
# with '/'. This is a comma separated list. Defaults to "config/plugins/visualizations".
# Visualizations config directory: where to look for individual visualization
# plugins. The path is relative to the Galaxy root dir. To use an absolute
# path begin the path with '/'. This is a comma separated list.
# Defaults to "config/plugins/visualizations".
#visualization_plugins_directory = config/plugins/visualizations
# Interactive environment plugins root directory: where to look for interactive environment
# plugins. By default none will be loaded. Set to config/plugins/interactive_environments
# to load Galaxy's stock plugins (currently just IPython). These will require Docker
# to be configured and have security considerations so proceed with caution.
#interactive_environment_plugins_directory =
# Interactive environment plugins root directory: where to look for interactive
# environment plugins. By default none will be loaded. Set to
# config/plugins/interactive_environments to load Galaxy's stock plugins
# (currently just IPython). These will require Docker to be configured and
# have security considerations, so proceed with caution.
#interactive_environment_plugins_directory =
# Each job is given a unique empty directory as its current working directory.
# This option defines in what parent directory those directories will be
@@ -245,18 +254,20 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# used for the cache
#template_cache_path = database/compiled_templates
# Citation related caching. Tool citations information maybe fetched from
# external sources such as http://dx.doi.org/ by Galaxy - the following parameters
# can be used to control the caching used to store this information.
# Citation related caching. Tool citations information maybe fetched from
# external sources such as http://dx.doi.org/ by Galaxy - the following
# parameters can be used to control the caching used to store this information.
#citation_cache_type = file
#citation_cache_data_dir = database/citations/data
#citation_cache_lock_dir = database/citations/lock
# External service types config file, defines what types of external_services configurations
# are available in Galaxy (.sample is used if default does not exist).
# External service types config file, defining what types of external_services
# configurations are available in Galaxy (.sample is used if default does not
# exist).
#external_service_type_config_file = config/external_service_types_conf.xml
# Path to the directory containing the external_service_types defined in the config.
# Path to the directory containing the external_service_types defined in the
# config.
#external_service_type_path = external_service_types
# Tools with a number of outputs not known until runtime can write these
@@ -314,10 +325,11 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# -- Mail and notification
# Galaxy sends mail for various things: Subscribing users to the mailing list
# if they request it, emailing password resets, notification from the Galaxy
# Sample Tracking system, reporting dataset errors, and sending activation emails.
# To do this, it needs to send mail through an SMTP server, which you may define here (host:port).
# Galaxy sends mail for various things: subscribing users to the mailing list
# if they request it, password resets, notifications from the Galaxy Sample
# Tracking system, reporting dataset errors, and sending activation emails.
# To do this, it needs to send mail through an SMTP server, which you may
# define here (host:port).
# Galaxy will automatically try STARTTLS but will continue upon failure.
#smtp_server = None
@@ -335,41 +347,44 @@ paste.app_factory = galaxy.web.buildapp:app_factory
#mailing_join_addr = galaxy-announce-join@bx.psu.edu
# Datasets in an error state include a link to report the error. Those reports
# will be sent to this address. Error reports are disabled if no address is set.
# Also this email is shown as a contact to user in case of Galaxy misconfiguration and other events user may encounter.
# will be sent to this address. Error reports are disabled if no address is
# set. Also this email is shown as a contact to user in case of Galaxy
# misconfiguration and other events user may encounter.
#error_email_to = None
# Activation email is used as a sender ('from' field) for the account activation mail.
# We recommend using string in the following format: Galaxy Project <galaxy-no-reply@example.com>
# Activation email is used as a sender ('from' field) for the account
# activation mail. We recommend using string in the following format:
# Galaxy Project <galaxy-no-reply@example.com>
#activation_email = None
# URL of the support resource for the galaxy instance. Used in activation emails.
# URL of the support resource for the galaxy instance. Used in activation
# emails.
#instance_resource_url = http://wiki.galaxyproject.org/
# E-mail domains blacklist is used for filtering out users that are using disposable email address
# during the registration. If their address domain matches any domain in the BL they are refused the registration.
# E-mail domains blacklist is used for filtering out users that are using
# disposable email address during the registration. If their address domain
# matches any domain in the blacklist, they are refused the registration.
#blacklist_file = config/disposable_email_blacklist.conf
# Registration warning message is used to discourage people from registering multiple accounts. Applies mostly for the main Galaxy instance.
# Registration warning message is used to discourage people from registering
# multiple accounts. Applies mostly for the main Galaxy instance.
# If no message specified the warning box will not be shown.
#registration_warning_message = Please register only one account - we provide this service free of charge and have limited computational resources. Multi-accounts are tracked and will be subjected to account termination and data deletion.
# When users opt to reset passwords, new ones are created, this option
# specifies the length of these passwords.
#reset_password_length = 15
# -- Account activation
# This is user account activation feature global flag. If set to "False" the rest of the Account
# activation configuration is ignored and user activation is disabled (a.k.a. accounts are active since registration).
# Note the activation is also not working in case the smtp server is not defined.
# User account activation feature global flag. If set to "False", the rest of
# the Account activation configuration is ignored and user activation is
# disabled (i.e. accounts are active since registration).
# The activation is also not working in case the SMTP server is not defined.
#user_activation_on = False
# Activation grace period. Activation is not forced (login is not disabled) until
# grace period has passed. Users under grace period can't run jobs (see inactivity_box_content).
# In hours. Default is 3. Enter 0 to disable grace period.
# Users with OpenID logins have grace period forever.
# Activation grace period (in hours). Activation is not forced (login is not
# disabled) until grace period has passed. Users under grace period can't run
# jobs (see inactivity_box_content). Default is 3. Enter 0 to disable grace
# period.
# Users with OpenID logins have grace period forever.
#activation_grace_period = 0
# Used for warning box for inactive accounts (unable to run jobs).
@@ -377,10 +392,17 @@ paste.app_factory = galaxy.web.buildapp:app_factory
#inactivity_box_content = Your account has not been activated yet. Feel free to browse around and see what's available, but you won't be able to upload data or run jobs until you have verified your email address.
# Galaxy Session Timeout
# This provides a timeout (in minutes) after which a user will have to log back in.
# A duration of 0 disables this feature.
#session_duration = 0
# -- Analytics
# You can enter tracking code here to track visitor's behavior
# through your Google Analytics account. Example: UA-XXXXXXXX-Y
# through your Google Analytics account. Example: UA-XXXXXXXX-Y
#ga_code = None
# -- Display sites
@@ -403,17 +425,19 @@ paste.app_factory = galaxy.web.buildapp:app_factory
# them).
#display_servers = hgw1.cse.ucsc.edu,hgw2.cse.ucsc.edu,hgw3.cse.ucsc.edu,hgw4.cse.ucsc.edu,hgw5.cse.ucsc.edu,hgw6.cse.ucsc.edu,hgw7.cse.ucsc.edu,hgw8.cse.ucsc.edu,lowepub.cse.ucsc.edu
# To disable the old-style display applications that are hardcoded into datatype classes,
# set enable_old_display_applications = False.
# This may be desirable due to using the new-style, XML-defined, display applications that
# have been defined for many of the datatypes that have the old-style.
# To disable the old-style display applications that are hardcoded into
# datatype classes, set enable_old_display_applications = False.
# This may be desirable due to using the new-style, XML-defined, display
# applications that have been defined for many of the datatypes that have the
# old-style.
# There is also a potential security concern with the old-style applications,
# where a malicious party could provide a link that appears to reference the Galaxy server,
# but contains a redirect to a third-party server, tricking a Galaxy user to access said
# site.
# where a malicious party could provide a link that appears to reference the
# Galaxy server, but contains a redirect to a third-party server, tricking a
# Galaxy user to access said site.
#enable_old_display_applications = True
# -- Next gen LIMS interface on top of existing Galaxy Sample/Request management code.
# -- Next gen LIMS interface on top of existing Galaxy Sample/Request
# management code.
use_nglims = False
nglims_config_file = tool-data/nglims.yaml
@@ -430,18 +454,20 @@ nglims_config_file = tool-data/nglims.yaml
# Format string used when showing date and time information.
# The string may contain:
# - the directives used by Python time.strftime() function (see http://docs.python.org/2/library/time.html#time.strftime ),
# - the directives used by Python time.strftime() function (see
# http://docs.python.org/2/library/time.html#time.strftime ),
# - $locale (complete format string for the server locale),
# - $iso8601 (complete format string as specified by ISO 8601 international standard).
# - $iso8601 (complete format string as specified by ISO 8601 international
# standard).
# pretty_datetime_format = $locale (UTC)
# URL (with schema http/https) of the Galaxy instance as accessible within your local
# network - if specified used as a default by pulsar file staging and IPython docker
# container for communicating back with Galaxy via the API.
# URL (with schema http/https) of the Galaxy instance as accessible within your
# local network - if specified used as a default by pulsar file staging and
# IPython Docker container for communicating back with Galaxy via the API.
#galaxy_infrastructure_url = http://localhost:8080
# The URL of the page to display in Galaxy's middle pane when loaded. This can be
# an absolute or relative URL.
# The URL of the page to display in Galaxy's middle pane when loaded. This can
# be an absolute or relative URL.
#welcome_url = /static/welcome.html
# The URL linked by the "Galaxy/brand" text.
@@ -472,7 +498,7 @@ nglims_config_file = tool-data/nglims.yaml
# The URL linked by the "Galaxy Q&A" link in the "Help" menu
# The Galaxy Q&A site is under development; when the site is done, this URL
# will be set and uncommented.
#qa_url =
#qa_url =
# Serve static content, which must be enabled if you're not serving it via a
# proxy server. These options should be self explanatory and so are not
@@ -529,9 +555,9 @@ nglims_config_file = tool-data/nglims.yaml
# will cause modern web browsers to not allow Galaxy to be embedded in
# the frames of web applications hosted at other hosts - this can help
# prevent a class of attack called clickjacking
# (https://www.owasp.org/index.php/Clickjacking). If you configure a
# (https://www.owasp.org/index.php/Clickjacking). If you configure a
# proxy in front of Galaxy - please ensure this header remains intact
# to protect your users. Uncomment and leave empty to not set the
# to protect your users. Uncomment and leave empty to not set the
# `X-Frame-Options` header.
#x_frame_options = SAMEORIGIN
@@ -547,10 +573,10 @@ nglims_config_file = tool-data/nglims.yaml
#nginx_upload_path = False
# Have Galaxy manage dynamic proxy component for routing requests to other
# services based on Galaxy's session cookie. It will attempt to do this by
# services based on Galaxy's session cookie. It will attempt to do this by
# default though you do need to install node+npm and do an npm install from
# `lib/galaxy/web/proxy/js`. It is generally more robust to configure this
# externally managing it however Galaxy is managed. If True Galaxy will only
# `lib/galaxy/web/proxy/js`. It is generally more robust to configure this
# externally managing it however Galaxy is managed. If True Galaxy will only
# launch the proxy if it is actually going to be used (e.g. for IPython).
#dynamic_proxy_manage=True
@@ -566,7 +592,8 @@ nglims_config_file = tool-data/nglims.yaml
# Enable verbose debugging of Galaxy-managed dynamic proxy.
#dynamic_proxy_debug=False
# The dynamic proxy is proxied by an external proxy (e.g. apache frontend to nodejs to wrap connections in SSL)
# The dynamic proxy is proxied by an external proxy (e.g. apache frontend to
# nodejs to wrap connections in SSL).
#dynamic_proxy_external_proxy=False
# -- Logging and Debugging
@@ -584,28 +611,28 @@ nglims_config_file = tool-data/nglims.yaml
# Turn on logging of application events and some user events to the database.
#log_events = True
# Turn on logging of user actions to the database. Actions currently logged are
# grid views, tool searches, and use of "recently" used tools menu. The
# Turn on logging of user actions to the database. Actions currently logged
# are grid views, tool searches, and use of "recently" used tools menu. The
# log_events and log_actions functionality will eventually be merged.
#log_actions = True
# Sanitize All HTML Tool Output By default, all tool output served as
# 'text/html' will be sanitized thoroughly. This can be disabled if
# you have special tools that require unaltered output. WARNING:
# Disabling this does make the Galxy instance susceptible to XSS
# attacks initiated by your users.
# Sanitize all HTML tool output. By default, all tool output served as
# 'text/html' will be sanitized thoroughly. This can be disabled if you have
# special tools that require unaltered output. WARNING: disabling this does
# make the Galaxy instance susceptible to XSS attacks initiated by your users.
#sanitize_all_html = True
# By default Galaxy will serve non-HTML tool output that may potentially
# contain browser executable JavaScript content as plain text. This will for
# contain browser executable JavaScript content as plain text. This will for
# instance cause SVG datasets to not render properly and so may be disabled
# by setting the following option to True.
#serve_xss_vulnerable_mimetypes = False
# Set the following to True to use ipython nbconvert to build HTML from IPython
# notebooks in Galaxy histories. This process may allow users to execute arbitrary
# code or serve arbitrary HTML. If enabled ipython must be available and on Galaxy's
# PATH, to do this run `pip install jinja2 pygments ipython` in Galaxy's virtualenv.
# Set the following to True to use IPython nbconvert to build HTML from IPython
# notebooks in Galaxy histories. This process may allow users to execute
# arbitrary code or serve arbitrary HTML. If enabled, IPython must be
# available and on Galaxy's PATH, to do this run
# `pip install jinja2 pygments ipython` in Galaxy's virtualenv.
#trust_ipython_notebook_conversion = False
# Debug enables access to various config options useful for development and
@@ -633,10 +660,6 @@ use_interactive = True
# lot of CPU.
#use_heartbeat = False
# Enable the memory debugging interface (careful, negatively impacts server
# performance).
#use_memdump = False
# -- Data Libraries
# These library upload options are described in much more detail in the wiki:
@@ -665,7 +688,7 @@ use_interactive = True
# if testing shows that Galaxy is able to create files using these formats.
# Specific formats can be disabled with this option, separate more than one
# format with commas. Available formats are currently 'zip', 'gz', and 'bz2'.
#disable_library_comptypes =
#disable_library_comptypes =
# Some sequencer integration features in beta allow you to automatically
# transfer datasets. This is done using a lightweight transfer manager which
@@ -693,6 +716,8 @@ use_interactive = True
# used by the algorithm that encodes and decodes these values. It can be any
# string. If left unchanged, anyone could construct a cookie that would grant
# them access to others' sessions.
# One simple way to generate a value for this is with the shell command:
# python -c 'import time; print time.time()' | md5sum | cut -f 1 -d ' '
#id_secret = USING THE DEFAULT IS NOT SECURE!
# User authentication can be delegated to an upstream proxy server (usually
@@ -713,6 +738,14 @@ use_interactive = True
# *not* include 'HTTP_' at the beginning of the header name.
#remote_user_header = HTTP_REMOTE_USER
# If use_remote_user is enabled, anyone who can log in to the Galaxy host may
# impersonate any other user by simply sending the appropriate header. Thus a
# secret shared between the upstream proxy server, and Galaxy is required.
# If anyone other than the Galaxy user is using the server, then apache/nginx
# should pass a value in the header 'GX_SECRET' that is identical to the one
# below.
#remote_user_secret = USING THE DEFAULT IS NOT SECURE!
# If use_remote_user is enabled, you can set this to a URL that will log your
# users out.
#remote_user_logout_href = None
@@ -764,11 +797,38 @@ use_interactive = True
# -- Beta features
# Use new tool form
#toolform_upgrade = False
#toolform_upgrade = True
# Use new tool form in workflow editor
#workflow_toolform_upgrade = False
# Enable Galaxy to communicate directly with a sequencer
#enable_sequencer_communication = False
# Separate tool command from rest of job script so tool dependencies
# don't interfer with metadata generation (this will be the default
# in a future release).
#enable_beta_tool_command_isolation = False
# Enable beta workflow modules that should not yet be considered part of Galaxy's
# stable API.
#enable_beta_workflow_modules = False
# Force usage of Galaxy's beta workflow scheduler under certain circumstances -
# this workflow scheduling forces Galaxy to schedule workflows in the background
# so initial submission of the workflows is signficantly sped up. This does
# however force the user to refresh their history manually to see newly scheduled
# steps (for "normal" workflows - steps are still scheduled far in advance of
# them being queued and scheduling here doesn't refer to actual cluster job
# scheduling).
# Workflows containing more than the specified number of steps will always use
# the Galaxy's beta workflow scheduling.
#force_beta_workflow_scheduled_min_steps=250
# Switch to using Galaxy's beta workflow scheduling for all workflows involving
# ccollections.
#force_beta_workflow_scheduled_for_collections=False
# Enable authentication via OpenID. Allows users to log in to their Galaxy
# account by authenticating with an OpenID provider.
#enable_openid = False
@@ -781,8 +841,9 @@ use_interactive = True
#api_allow_run_as = None
# Master key that allows many API admin actions to be used without actually
# having a defined admin user in the database/config. Only set this if you need
# to bootstrap Galaxy, you probably do not want to set this on public servers.
# having a defined admin user in the database/config. Only set this if you
# need to bootstrap Galaxy, you probably do not want to set this on public
# servers.
#master_api_key = changethis
# Enable tool tags (associating tools with tags). This has its own option
@@ -790,7 +851,7 @@ use_interactive = True
# large servers.
#enable_tool_tags = False
# Enable a feature when running workflows. When enabled, default datasets
# Enable a feature when running workflows. When enabled, default datasets
# are selected for "Set at Runtime" inputs from the history such that the
# same input will not be selected twice, unless there are more inputs than
# compatible datasets in the history.
@@ -826,7 +887,7 @@ use_interactive = True
# File where Data Managers are configured (.sample used if default does not
# exist)
#data_manager_config_file = config/data_manager_conf.xml
# File where Tool Shed based Data Managers are configured
# File where Tool Shed based Data Managers are configured
#shed_data_manager_config_file = config/shed_data_manager_conf.xml
# Directory to store Data Manager based tool-data; defaults to tool_data_path
#galaxy_data_manager_data_path = tool-data
@@ -853,7 +914,8 @@ use_interactive = True
# following option to True or False.
#track_jobs_in_database = None
# This enables splitting of jobs into tasks, if specified by the particular tool config.
# This enables splitting of jobs into tasks, if specified by the particular tool
# config.
# This is a new feature and not recommended for production servers yet.
#use_tasked_jobs = False
#local_task_queue_workers = 2
@@ -904,7 +966,8 @@ use_interactive = True
# Optional file containing job resource data entry fields definition.
# These fields will be presented to users in the tool forms and allow them to
# overwrite default job resources such as number of processors, memory, and walltime.
# overwrite default job resources such as number of processors, memory and
# walltime.
#job_resource_params_file = config/job_resource_params_conf.xml
# If using job concurrency limits (configured in job_config_file), several
@@ -918,17 +981,27 @@ use_interactive = True
#cache_user_job_count = False
# ToolBox filtering
# Modules from lib/galaxy/tools/filters/ can be specified in the following lines.
# tool_* filters will be applied for all users and can not be changed by them.
# user_tool_* filters will be shown under user preferences and can be toogled
# on and off by runtime
#tool_filters
#tool_label_filters
#tool_section_filters
# Modules from lib/galaxy/tools/toolbox/filters/ can be specified in
# the following lines. tool_* filters will be applied for all users
# and can not be changed by them. user_tool_* filters will be shown
# under user preferences and can be toogled on and off by
# runtime. Example shown below are not real defaults (no custom
# filters applied by defualt) but can be enabled with by renaming the
# example.py.sample in the filters directory to example.py.
#tool_filters =
#tool_label_filters =
#tool_section_filters =
#user_tool_filters = examples:restrict_upload_to_admins, examples:restrict_encode
#user_tool_section_filters = examples:restrict_text
#user_tool_label_filters = examples:restrict_upload_to_admins, examples:restrict_encode
# The base modules that are searched for modules as described above
# can be modified and modules external to Galaxy can be searched by
# modifying the following option.
#toolbox_filter_base_modules = galaxy.tools.toolbox.filters,galaxy.tools.filters
# Galaxy Application Internal Message Queue
# Galaxy uses AMQP internally TODO more documentation on what for.
@@ -945,11 +1018,12 @@ use_interactive = True
# ---- Galaxy External Message Queue -------------------------------------------------
# Galaxy uses AMQ protocol to receive messages from external sources like
# bar code scanners. Galaxy has been tested against RabbitMQ AMQP implementation.
# For Galaxy to receive messages from a message queue the RabbitMQ server has
# to be set up with a user account and other parameters listed below. The 'host'
# and 'port' fields should point to where the RabbitMQ server is running.
# Galaxy uses Advanced Message Queuing Protocol (AMQP) to receive messages from
# external sources like barcode scanners. Galaxy has been tested against
# RabbitMQ AMQP implementation. For Galaxy to receive messages from a message
# queue, the RabbitMQ server has to be set up with a user account and other
# parameters listed below. The 'host' and 'port' fields should point to where
# the RabbitMQ server is running.
[galaxy_amqp]
File diff suppressed because one or more lines are too long
@@ -83,7 +83,7 @@ return Backbone.View.extend({
Tools.addZoom({
xAxis : this.xAxis,
yAxis : this.yAxis,
redraw : function() { self.redraw() },
redraw : function() { self.redraw(); },
svg : d3.select('#' + this.canvas_id)
});
},
@@ -108,7 +108,9 @@ return Backbone.View.extend({
// append element
var $main = $('<div/>');
$main.append(Utils.wrap((new Ui.Label({ title : 'Provide a chart title:'})).$el));
$main.append(Utils.wrap(this.title.$el));
$main.append(this.title.$el);
$main.append($('<div/>').addClass('ui-table-form-info').html('This title will appear in the list of \'Saved Visualizations\'. Charts are saved upon creation.'));
$main.append(Utils.wrap());
$main.append(Utils.wrap(this.types.$el));
// add tab
@@ -189,7 +191,9 @@ return Backbone.View.extend({
_refreshTitle: function() {
var title = this.chart.get('title');
this.portlet.title(title);
this.title.value(title);
if (this.title.value() != title) {
this.title.value(title);
}
},
// refresh group
@@ -254,7 +254,9 @@ return Backbone.View.extend({
if (key_text === undefined) {
key_text = '';
}
this.group_key.value(key_text);
if (key_text != this.group_key.value()) {
this.group_key.value(key_text);
}
}
});
@@ -99,7 +99,6 @@ return Backbone.View.extend({
this.portlet = new Portlet.View({
icon : 'fa-bar-chart-o',
title: 'Viewport',
scrollable: false,
operations: {
edit_button: new Ui.ButtonIcon({
icon : 'fa-edit',
@@ -30,6 +30,14 @@ return Backbone.View.extend({
// use full screen for viewer
this._fullscreen(this.$el, 100);
// prevent window scrolling
var initial_overflow = $('body').css('overflow');
this.$el.on('mouseover', function() {
$('body').css('overflow', 'hidden');
}).on('mouseout', function() {
$('body').css('overflow', initial_overflow);
});
// create container element
this._createContainer('div');
@@ -31,9 +31,8 @@ ${h.js( 'libs/jquery/jquery',
'libs/backbone/backbone',
'libs/d3',
'libs/handlebars.runtime',
'mvc/ui',
'jq-plugins/ui/peek-column-selector',
'jq-plugins/ui/pagination',
'ui/peek-column-selector',
'ui/pagination',
'mvc/visualization/visualization-model' )}
${h.javascript_link( root + 'plugins/visualizations/scatterplot/static/scatterplot-edit.js' )}
+13 -9
View File
@@ -22,9 +22,13 @@ log_level = DEBUG
# database_connection = postgres:///galaxy_test?user=postgres&password=postgres
# Where dataset files are saved
file_path = database/files
#file_path = database/files
# Temporary storage for additional datasets, this should be shared through the cluster
new_file_path = database/tmp
#new_file_path = database/tmp
# Mako templates are compiled as needed and cached for reuse, this directory is
# used for the cache
#template_cache_path = database/compiled_templates/reports
# Session support (beaker)
use_beaker_session = True
@@ -60,10 +64,10 @@ mailing_join_addr = galaxy-user-join@bx.psu.edu
use_new_layout = true
# Serving static files (needed if running standalone)
static_enabled = True
static_cache_time = 360
static_dir = %(here)s/static/
static_images_dir = %(here)s/static/images
static_favicon_dir = %(here)s/static/favicon.ico
static_scripts_dir = %(here)s/static/scripts/
static_style_dir = %(here)s/static/june_2007_style/blue
# static_enabled = True
# static_cache_time = 360
# static_dir = %(here)s/static/
# static_images_dir = %(here)s/static/images
# static_favicon_dir = %(here)s/static/favicon.ico
# static_scripts_dir = %(here)s/static/scripts/
# static_style_dir = %(here)s/static/june_2007_style/blue
+1
View File
@@ -15,6 +15,7 @@
<tool file="data_source/yeastmine.xml" />
<tool file="data_source/worm_modencode.xml" />
<tool file="data_source/wormbase.xml" />
<tool file="data_source/zebrafishmine.xml" />
<tool file="data_source/eupathdb.xml" />
<tool file="genomespace/genomespace_file_browser_prod.xml" />
<tool file="genomespace/genomespace_importer.xml" />
+1
View File
@@ -22,6 +22,7 @@
<tool file="data_source/worm_modencode.xml" />
<tool file="data_source/wormbase.xml" />
<tool file="data_source/wormbase_test.xml" />
<tool file="data_source/zebrafishmine.xml" />
<tool file="data_source/eupathdb.xml" />
<tool file="data_source/hbvar.xml" />
<tool file="genomespace/genomespace_file_browser_prod.xml" />
+5
View File
@@ -60,4 +60,9 @@
<columns>value, dbkey, name, path</columns>
<file path="tool-data/mosaik_index.loc" />
</table>
<!-- Locations of indexes in the 2bit format -->
<table name="twobit" comment_char="#">
<columns>value, path</columns>
<file path="tool-data/twobit.loc" />
</table>
</tables>
+10
View File
@@ -67,6 +67,13 @@ id_secret = changethisinproductiontoo
# https://wiki.galaxyproject.org/Admin/Config/ApacheProxy
#use_remote_user = False
# If use_remote_user is enabled, anyone who can log in to the Galaxy host may
# impersonate any other user by simply sending the appropriate header. Thus a
# secret shared between the upstream proxy server, and Galaxy is required.
# If anyone other than the Galaxy user is using the server, then apache/nginx should
# pass a value in the header 'GX_SECRET' that is identical the one below
#remote_user_secret = changethisinproductiontoo
# Configuration for debugging middleware
debug = true
use_lint = false
@@ -118,3 +125,6 @@ use_new_layout = true
#static_favicon_dir = static/favicon.ico
#static_scripts_dir = static/scripts/
#static_style_dir = static/style/blue
# Sentry (getsentry.com) DSN for catching bugs.
#sentry_dsn = None
+2 -7
View File
@@ -8,12 +8,7 @@ build description
import sys
import urllib
if sys.version_info[:2] >= ( 2, 5 ):
import xml.etree.ElementTree as ElementTree
else:
from galaxy import eggs
import pkg_resources; pkg_resources.require( "elementtree" )
from elementtree import ElementTree
import xml.etree.ElementTree as ElementTree
URL = "http://genome.cse.ucsc.edu/cgi-bin/das/dsn"
@@ -38,7 +33,7 @@ def getbuilds(url):
for dsn in tree:
build = dsn.find("SOURCE").attrib['id']
description = dsn.find("DESCRIPTION").text.replace(" - Genome at UCSC","").replace(" Genome at UCSC","")
fields = description.split(" ")
temp = fields[0]
for i in range(len(fields)-1):
+2 -7
View File
@@ -5,12 +5,7 @@ Connects to sites and determines which builds are available at each.
import sys
import urllib
if sys.version_info[:2] >= ( 2, 5 ):
import xml.etree.ElementTree as ElementTree
else:
from galaxy import eggs
import pkg_resources; pkg_resources.require( "elementtree" )
from elementtree import ElementTree
import xml.etree.ElementTree as ElementTree
sites = ['http://genome.ucsc.edu/cgi-bin/',
'http://archaea.ucsc.edu/cgi-bin/',
@@ -38,7 +33,7 @@ def main():
print "#Invalid xml passed back from " + site
continue
print "#Harvested from",site
for dsn in tree:
build = dsn.find("SOURCE").attrib['id']
builds.append(build)
+6 -8
View File
@@ -15,7 +15,7 @@ no_auto = pbs_python
bx_python = 0.7.2
Cheetah = 2.2.2
MarkupSafe = 0.12
mercurial = 2.2.3
mercurial = 3.2.4
MySQL_python = 1.2.3c1
PyRods = 3.2.4
numpy = 1.6.0
@@ -26,7 +26,6 @@ pysam = 0.4.2
pysqlite = 2.5.6
python_lzo = 1.08_2.03_static
PyYAML = 3.10
guppy = 0.1.10
SQLAlchemy = 0.7.9
; msgpack_python = 0.2.4
@@ -34,14 +33,12 @@ SQLAlchemy = 0.7.9
amqp = 1.4.5
anyjson = 0.3.3
Beaker = 1.4
bioblend = 0.4.2
bioblend = 0.5.2
boto = 2.27.0
decorator = 3.1.2
docutils = 0.7
drmaa = 0.7.6
elementtree = 1.2.6_20050316
Fabric = 1.7.0
GeneTrack = 2.0.0_beta_1
importlib = 1.0.3
kombu = 3.0.13
lrucache = 0.2
@@ -60,9 +57,11 @@ poster = 0.8.1
python_openid = 2.2.5
python_daemon = 1.5.5
pytz = 2013.9
repoze.lru = 0.6
requests = 2.2.1
Routes = 1.12.3
Routes = 2.1
simplejson = 2.1.1
six = 1.9.0
sqlalchemy_migrate = 0.7.2
ssh = 1.7.14
SVGFig = 1.1.6
@@ -74,7 +73,7 @@ WebOb = 0.8.5
wsgiref = 0.1.2
Babel = 1.3
wchartype = 0.1
Whoosh = 0.3.18
Whoosh = 2.4.1
; fluent_logger = 0.3.3
raven = 3.1.8
@@ -83,7 +82,6 @@ raven = 3.1.8
psycopg2 = _9.2.4_static
pysqlite = _3.6.17_static
MySQL_python = _5.1.41_static
GeneTrack = _dev_48da9e998f0caf01c5be731e926f4b0481f658f0
pysam = _kanwei_b10f6e722e9a
; dependency source urls, necessary for scrambling. for an explanation, see
+3 -10
View File
@@ -7,7 +7,7 @@ import galaxy.model
import galaxy.security
from galaxy.managers.collections import DatasetCollectionManager
import galaxy.quota
from galaxy.tags.tag_handler import GalaxyTagHandler
from galaxy.managers.tags import GalaxyTagManager
from galaxy.visualization.genomes import Genomes
from galaxy.visualization.data_providers.registry import DataProviderRegistry
from galaxy.visualization.registry import VisualizationsRegistry
@@ -58,7 +58,7 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
# Security helper
self._configure_security()
# Tag handler
self.tag_handler = GalaxyTagHandler()
self.tag_handler = GalaxyTagManager( self )
# Dataset Collection Plugins
self.dataset_collections_service = DatasetCollectionManager(self)
@@ -107,10 +107,8 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
self.quota_agent = galaxy.quota.QuotaAgent( self.model )
else:
self.quota_agent = galaxy.quota.NoQuotaAgent( self.model )
# Heartbeat and memdump for thread / heap profiling
# Heartbeat for thread profiling
self.heartbeat = None
self.memdump = None
self.memory_usage = None
# Container for OpenID authentication routines
if self.config.enable_openid:
from galaxy.web.framework import openid_manager
@@ -125,11 +123,6 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
self.heartbeat = heartbeat.Heartbeat( fname=self.config.heartbeat_log )
self.heartbeat.daemon = True
self.heartbeat.start()
# Enable the memdump signal catcher if configured and available
if self.config.use_memdump:
from galaxy.util import memdump
if memdump.Memdump:
self.memdump = memdump.Memdump()
# Transfer manager client
if self.config.get_bool( 'enable_beta_job_managers', False ):
from galaxy.jobs import transfer_manager
+16 -8
View File
@@ -83,6 +83,7 @@ class Configuration( object ):
# 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.
self.integrated_tool_panel_config = resolve_path( kwargs.get( 'integrated_tool_panel_config', 'integrated_tool_panel.xml' ), self.root )
self.toolbox_filter_base_modules = listify( kwargs.get( "toolbox_filter_base_modules", "galaxy.tools.filters,galaxy.tools.toolbox.filters" ) )
self.tool_filters = listify( kwargs.get( "tool_filters", [] ), do_strip=True )
self.tool_label_filters = listify( kwargs.get( "tool_label_filters", [] ), do_strip=True )
self.tool_section_filters = listify( kwargs.get( "tool_section_filters", [] ), do_strip=True )
@@ -135,6 +136,7 @@ class Configuration( object ):
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.remote_user_secret = kwargs.get( "remote_user_secret", 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" ) )
self.allow_user_deletion = string_as_bool( kwargs.get( "allow_user_deletion", "False" ) )
@@ -144,7 +146,6 @@ class Configuration( object ):
self.collect_outputs_from = [ x.strip() for x in kwargs.get( 'collect_outputs_from', 'new_file_path,job_working_directory' ).lower().split(',') ]
self.template_path = resolve_path( kwargs.get( "template_path", "templates" ), self.root )
self.template_cache = resolve_path( kwargs.get( "template_cache_path", "database/compiled_templates" ), self.root )
self.workflow_schedulers_config_file = resolve_path( kwargs.get( 'workflow_schedulers_config_file', 'config/workflow_schedulers_conf.xml' ), self.root )
self.local_job_queue_workers = int( kwargs.get( "local_job_queue_workers", "5" ) )
self.cluster_job_queue_workers = int( kwargs.get( "cluster_job_queue_workers", "3" ) )
self.job_queue_cleanup_interval = int( kwargs.get("job_queue_cleanup_interval", "5") )
@@ -162,7 +163,6 @@ class Configuration( object ):
self.job_walltime_delta = timedelta( 0, s, 0, 0, m, h )
self.admin_users = kwargs.get( "admin_users", "" )
self.admin_users_list = [u.strip() for u in self.admin_users.split(',') if u]
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 )
@@ -173,6 +173,7 @@ class Configuration( object ):
self.instance_resource_url = kwargs.get( 'instance_resource_url', None )
self.registration_warning_message = kwargs.get( 'registration_warning_message', None )
self.ga_code = kwargs.get( 'ga_code', None )
self.session_duration = int(kwargs.get( 'session_duration', 0 ))
# Get the disposable email domains blacklist file and its contents
self.blacklist_location = kwargs.get( 'blacklist_file', None )
self.blacklist_content = None
@@ -195,6 +196,7 @@ class Configuration( object ):
# Tasked job runner.
self.use_tasked_jobs = string_as_bool( kwargs.get( 'use_tasked_jobs', False ) )
self.local_task_queue_workers = int(kwargs.get("local_task_queue_workers", 2))
self.commands_in_new_shell = string_as_bool( kwargs.get( 'enable_beta_tool_command_isolation', "False" ) )
# The transfer manager and deferred job queue
self.enable_beta_job_managers = string_as_bool( kwargs.get( 'enable_beta_job_managers', 'False' ) )
# These workflow modules should not be considered part of Galaxy's
@@ -229,7 +231,6 @@ class Configuration( object ):
self.external_chown_script = kwargs.get('external_chown_script', None)
self.environment_setup_file = kwargs.get( 'environment_setup_file', None )
self.use_heartbeat = string_as_bool( kwargs.get( 'use_heartbeat', 'False' ) )
self.use_memdump = string_as_bool( kwargs.get( 'use_memdump', 'False' ) )
self.log_actions = string_as_bool( kwargs.get( 'log_actions', 'False' ) )
self.log_events = string_as_bool( kwargs.get( 'log_events', 'False' ) )
self.sanitize_all_html = string_as_bool( kwargs.get( 'sanitize_all_html', True ) )
@@ -321,6 +322,9 @@ class Configuration( object ):
# Crummy, but PasteScript does not give you a way to determine this
if arg.lower().startswith('--server-name='):
self.server_name = arg.split('=', 1)[-1]
# Allow explicit override of server name in confg params
if "server_name" in kwargs:
self.server_name = kwargs.get("server_name")
# Store all configured server names
self.server_names = []
for section in global_conf_parser.sections():
@@ -457,6 +461,7 @@ class Configuration( object ):
shed_data_manager_config_file=[ 'shed_data_manager_conf.xml', 'config/shed_data_manager_conf.xml' ],
shed_tool_data_table_config=[ 'shed_tool_data_table_conf.xml', 'config/shed_tool_data_table_conf.xml' ],
tool_sheds_config_file=[ 'config/tool_sheds_conf.xml', 'tool_sheds_conf.xml', 'config/tool_sheds_conf.xml.sample' ],
workflow_schedulers_config_file=['config/workflow_schedulers_conf.xml', 'config/workflow_schedulers_conf.xml.sample'],
)
listify_defaults = dict(
@@ -709,10 +714,7 @@ class ConfiguresGalaxyMixin:
from galaxy import tools
self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self )
# Search support for tools
import galaxy.tools.search
index_help = getattr( self.config, "index_tool_help", True )
self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox, index_help )
self.reindex_tool_search()
from galaxy.tools.deps import containers
galaxy_root_dir = os.path.abspath(self.config.root)
@@ -723,7 +725,13 @@ class ConfiguresGalaxyMixin:
outputs_to_working_directory=self.config.outputs_to_working_directory,
container_image_cache_path=self.config.container_image_cache_path,
)
self.container_finder = galaxy.tools.deps.containers.ContainerFinder(app_info)
self.container_finder = containers.ContainerFinder(app_info)
def reindex_tool_search( self ):
# Call this when tools are added or removed.
import galaxy.tools.search
index_help = getattr( self.config, "index_tool_help", True )
self.toolbox_search = galaxy.tools.search.ToolBoxSearch( self.toolbox, index_help )
def _configure_tool_data_tables( self, from_shed_config ):
from galaxy.tools.data import ToolDataTableManager
+11 -1
View File
@@ -1,6 +1,6 @@
from .types import list
from .types import paired
from galaxy import model
PLUGIN_CLASSES = [list.ListDatasetCollectionType, paired.PairedDatasetCollectionType]
@@ -12,3 +12,13 @@ class DatasetCollectionTypesRegistry(object):
def get( self, plugin_type ):
return self.__plugins[ plugin_type ]
def prototype( self, plugin_type ):
plugin_type_object = self.get( plugin_type )
if not hasattr( plugin_type_object, 'prototype_elements' ):
raise Exception( "Cannot pre-determine structure for collection of type %s" % plugin_type )
dataset_collection = model.DatasetCollection()
elements = [ e for e in plugin_type_object.prototype_elements() ]
dataset_collection.elements = elements
return dataset_collection
+13 -1
View File
@@ -1,6 +1,6 @@
from ..types import BaseDatasetCollectionType
from galaxy.model import DatasetCollectionElement
from galaxy.model import DatasetCollectionElement, HistoryDatasetAssociation
FORWARD_IDENTIFIER = "forward"
REVERSE_IDENTIFIER = "reverse"
@@ -32,3 +32,15 @@ class PairedDatasetCollectionType( BaseDatasetCollectionType ):
)
yield left_association
yield right_association
def prototype_elements( self ):
left_association = DatasetCollectionElement(
element=HistoryDatasetAssociation(),
element_identifier=FORWARD_IDENTIFIER,
)
right_association = DatasetCollectionElement(
element=HistoryDatasetAssociation(),
element_identifier=REVERSE_IDENTIFIER,
)
yield left_association
yield right_association
+98 -3
View File
@@ -20,7 +20,7 @@ eggs.require( "bx-python" )
from bx.seq.twobit import TWOBIT_MAGIC_NUMBER, TWOBIT_MAGIC_NUMBER_SWAP, TWOBIT_MAGIC_SIZE
from galaxy.util import sqlite
from galaxy.datatypes.metadata import MetadataElement,ListParameter,DictParameter
from galaxy.datatypes.metadata import MetadataElement, MetadataParameter, ListParameter, DictParameter
from galaxy.datatypes import metadata
import dataproviders
@@ -106,6 +106,30 @@ class Ab1( Binary ):
Binary.register_unsniffable_binary_ext("ab1")
class CompressedArchive( Binary ):
"""
Class describing an compressed binary file
This class can be sublass'ed to implement archive filetypes that will not be unpacked by upload.py.
"""
file_ext = "compressed_archive"
compressed = True
def set_peek( self, dataset, is_multi_byte=False ):
if not dataset.dataset.purged:
dataset.peek = "Compressed binary file"
dataset.blurb = data.nice_size( dataset.get_size() )
else:
dataset.peek = 'file does not exist'
dataset.blurb = 'file purged from disk'
def display_peek( self, dataset ):
try:
return dataset.peek
except:
return "Compressed binary file (%s)" % ( data.nice_size( dataset.get_size() ) )
Binary.register_unsniffable_binary_ext("compressed_archive")
class GenericAsn1Binary( Binary ):
"""Class for generic ASN.1 binary format"""
@@ -240,10 +264,23 @@ class Bam( Binary ):
##$ samtools index
##Usage: samtools index <in.bam> [<out.index>]
stderr_name = tempfile.NamedTemporaryFile( prefix = "bam_index_stderr" ).name
command = 'samtools index %s %s' % ( dataset.file_name, index_file.file_name )
proc = subprocess.Popen( args=command, shell=True, stderr=open( stderr_name, 'wb' ) )
command = [ 'samtools', 'index', dataset.file_name, index_file.file_name ]
proc = subprocess.Popen( args=command, stderr=open( stderr_name, 'wb' ) )
exit_code = proc.wait()
#Did index succeed?
if exit_code == -6:
# SIGABRT, most likely samtools 1.0+ which does not accept the index name parameter.
dataset_symlink = os.path.join( os.path.dirname( index_file.file_name ),
'__dataset_%d_%s' % ( dataset.id, os.path.basename( index_file.file_name ) ) )
os.symlink( dataset.file_name, dataset_symlink )
try:
command = [ 'samtools', 'index', dataset_symlink ]
exit_code = subprocess.call( args=command, stderr=open( stderr_name, 'wb' ) )
shutil.move( dataset_symlink + '.bai', index_file.file_name )
except Exception, e:
open( stderr_name, 'ab+' ).write( 'Galaxy attempted to build the BAM index with samtools 1.0+ but failed: %s\n' % e)
finally:
os.unlink( dataset_symlink )
stderr = open( stderr_name ).read().strip()
if stderr:
if exit_code != 0:
@@ -640,8 +677,66 @@ class SQlite ( Binary ):
return dataproviders.dataset.SQliteDataDictProvider( dataset_source, **settings )
#Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
class GeminiSQLite( SQlite ):
"""Class describing a Gemini Sqlite database """
MetadataElement( name="gemini_version", default='0.10.0' , param=MetadataParameter, desc="Gemini Version",
readonly=True, visible=True, no_value='0.10.0' )
file_ext = "gemini.sqlite"
def set_meta( self, dataset, overwrite = True, **kwd ):
super( GeminiSQLite, self ).set_meta( dataset, overwrite = overwrite, **kwd )
try:
conn = sqlite.connect( dataset.file_name )
c = conn.cursor()
tables_query = "SELECT version FROM version"
result = c.execute( tables_query ).fetchall()
for version, in result:
dataset.metadata.gemini_version = version
# TODO: Can/should we detect even more attributes, such as use of PED file, what was input annotation type, etc.
except Exception, e:
log.warn( '%s, set_meta Exception: %s', self, e )
def sniff( self, filename ):
if super( GeminiSQLite, self ).sniff( filename ):
gemini_table_names = [ "gene_detailed", "gene_summary", "resources", "sample_genotype_counts", "sample_genotypes", "samples",
"variant_impacts", "variants", "version" ]
try:
conn = sqlite.connect( filename )
c = conn.cursor()
tables_query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
result = c.execute( tables_query ).fetchall()
result = map( lambda x: x[0], result )
for table_name in gemini_table_names:
if table_name not in result:
return False
return True
except Exception, e:
log.warn( '%s, sniff Exception: %s', self, e )
return False
def set_peek( self, dataset, is_multi_byte=False ):
if not dataset.dataset.purged:
dataset.peek = "Gemini SQLite Database, version %s" % ( dataset.metadata.gemini_version or 'unknown' )
dataset.blurb = data.nice_size( dataset.get_size() )
else:
dataset.peek = 'file does not exist'
dataset.blurb = 'file purged from disk'
def display_peek( self, dataset ):
try:
return dataset.peek
except:
return "Gemini SQLite Database, version %s" % ( dataset.metadata.gemini_version or 'unknown' )
Binary.register_sniffable_binary_format( "gemini.sqlite", "gemini.sqlite", GeminiSQLite )
# FIXME: We need to register gemini.sqlite before sqlite, since register_sniffable_binary_format and is_sniffable_binary called in upload.py
# ignores sniff order declared in datatypes_conf.xml
Binary.register_sniffable_binary_format("sqlite", "sqlite", SQlite)
class Xlsx(Binary):
"""Class for Excel 2007 (xlsx) files"""
file_ext="xlsx"
@@ -0,0 +1,12 @@
<tool id="tabular_to_dbnsfp" name="Convert tabular to dbnsfp" version="1.0.0">
<description></description>
<command interpreter="python">tabular_to_dbnsfp.py $input $dbnsfp.extra_files_path/dbNSFP.gz</command>
<inputs>
<param format="tabular" name="input" type="data" label="Choose a dbnsfp tabular file"/>
</inputs>
<outputs>
<data format="snpsiftdbnsfp" name="dbnsfp"/>
</outputs>
<help>
</help>
</tool>
@@ -3,7 +3,7 @@ Dataproviders that iterate over lines from their sources.
"""
import line
import xml.etree.ElementTree as elementtree
from xml.etree.ElementTree import Element, iterparse
_TODO = """
"""
@@ -30,7 +30,7 @@ class XMLDataProvider( HierarchalDataProvider ):
"""
Data provider that converts selected XML elements to dictionaries.
"""
# using elementtree's iterparse method to keep mem down
# using xml.etree's iterparse method to keep mem down
#TODO: this, however (AFAIK), prevents the use of xpath
settings = {
'selector' : 'str', #urlencoded
@@ -54,7 +54,7 @@ class XMLDataProvider( HierarchalDataProvider ):
"""
Returns true if the ``element`` matches the ``selector``.
:param element: an XML ``ElementTree.Element``
:param element: an XML ``Element``
:param selector: some partial string in the desired tags to return
Change point for more sophisticated selectors.
@@ -64,13 +64,13 @@ class XMLDataProvider( HierarchalDataProvider ):
#TODO: fails with '#' - browser thinks it's an anchor - use urlencode
#TODO: need removal/replacement of etree namespacing here - then move to string match
return bool( ( selector == None )
or ( isinstance( element, elementtree.Element ) and selector in element.tag ) )
or ( isinstance( element, Element ) and selector in element.tag ) )
def element_as_dict( self, element ):
"""
Converts an XML element (its text, tag, and attributes) to dictionary form.
:param element: an XML ``ElementTree.Element``
:param element: an XML ``Element``
"""
#TODO: Key collision is unlikely here, but still should be better handled
return {
@@ -84,7 +84,7 @@ class XMLDataProvider( HierarchalDataProvider ):
"""
Yield all children of element (and their children - recursively)
in dictionary form.
:param element: an XML ``ElementTree.Element``
:param element: an XML ``Element``
:param max_depth: the number of generations of descendents to return
"""
if not isinstance( max_depth, int ) or max_depth >= 1:
@@ -99,7 +99,7 @@ class XMLDataProvider( HierarchalDataProvider ):
yield child_data
def __iter__( self ):
context = elementtree.iterparse( self.source, events=self.ITERPARSE_ALL_EVENTS )
context = iterparse( self.source, events=self.ITERPARSE_ALL_EVENTS )
context = iter( context )
selected_element = None
@@ -167,23 +167,21 @@ class PopulatedDisplayApplicationLink( object ):
class DisplayApplication( object ):
@classmethod
def from_file( cls, filename, datatypes_registry ):
return cls.from_elem( parse_xml( filename ).getroot(), datatypes_registry )
return cls.from_elem( parse_xml( filename ).getroot(), datatypes_registry, filename=filename )
@classmethod
def from_elem( cls, elem, datatypes_registry ):
def from_elem( cls, elem, datatypes_registry, filename=None ):
att_dict = cls._get_attributes_from_elem( elem )
rval = DisplayApplication( att_dict['id'], att_dict['name'], datatypes_registry, att_dict['version'], filename=filename, elem=elem )
rval._load_links_from_elem( elem )
return rval
@classmethod
def _get_attributes_from_elem( cls, elem ):
display_id = elem.get( 'id', None )
assert display_id, "ID tag is required for a Display Application"
name = elem.get( 'name', display_id )
version = elem.get( 'version', None )
rval = DisplayApplication( display_id, name, datatypes_registry, version )
for link_elem in elem.findall( 'link' ):
link = DisplayApplicationLink.from_elem( link_elem, rval )
if link:
rval.links[ link.id ] = link
for dynamic_links in elem.findall( 'dynamic_links' ):
for link in DynamicDisplayApplicationBuilder( dynamic_links, rval, datatypes_registry.build_sites ):
rval.links[ link.id ] = link
return rval
def __init__( self, display_id, name, datatypes_registry, version = None ):
return dict( id=display_id, name=name, version=version )
def __init__( self, display_id, name, datatypes_registry, version = None, filename=None, elem=None ):
self.id = display_id
self.name = name
self.datatypes_registry = datatypes_registry
@@ -191,6 +189,16 @@ class DisplayApplication( object ):
version = "1.0.0"
self.version = version
self.links = odict()
self._filename = filename
self._elem = elem
def _load_links_from_elem( self, elem ):
for link_elem in elem.findall( 'link' ):
link = DisplayApplicationLink.from_elem( link_elem, self )
if link:
self.links[ link.id ] = link
for dynamic_links in elem.findall( 'dynamic_links' ):
for link in DynamicDisplayApplicationBuilder( dynamic_links, self, self.datatypes_registry.build_sites ):
self.links[ link.id ] = link
def get_link( self, link_name, data, dataset_hash, user_hash, trans, app_kwds ):
#returns a link object with data knowledge to generate links
return PopulatedDisplayApplicationLink( self.links[ link_name ], data, dataset_hash, user_hash, trans, app_kwds )
@@ -200,3 +208,23 @@ class DisplayApplication( object ):
if link_value.filter_by_dataset( data, trans ):
filtered.links[link_name] = link_value
return filtered
def reload( self ):
if self._filename:
elem = parse_xml( self._filename ).getroot()
elif self._elem:
elem = self._elem
else:
raise Exception( "Unable to reload DisplayApplication %s." % ( self.name ) )
# All toolshed-specific attributes added by e.g the registry will remain
attr_dict = self._get_attributes_from_elem( elem )
# We will not allow changing the id at this time (we'll need to fix several mappings upstream to handle this case)
assert attr_dict.get( 'id' ) == self.id, ValueError( "You cannot reload a Display application where the ID has changed. You will need to restart the server instead." )
# clear old links
for key in self.links.keys():
del self.links[key]
# Set new attributes
for key, value in attr_dict.iteritems():
setattr( self, key, value )
# Load new links
self._load_links_from_elem( elem )
return self
+12
View File
@@ -20,6 +20,7 @@ from sqlalchemy.orm import object_session
import galaxy.model
from galaxy.util import listify
from galaxy.util.object_wrapper import sanitize_lists_to_string
from galaxy.util import stringify_dictionary_keys
from galaxy.util import string_as_bool
from galaxy.util import in_directory
@@ -232,6 +233,9 @@ class MetadataParameter( object ):
def to_string( self, value ):
return str( value )
def to_safe_string( self, value ):
return sanitize_lists_to_string( self.to_string( value ) )
def make_copy( self, value, target_context = None, source_context = None ):
return copy.deepcopy( value )
@@ -480,6 +484,10 @@ class DictParameter( MetadataParameter ):
def to_string( self, value ):
return json.dumps( value )
def to_safe_string( self, value ):
# We do not sanitize json dicts
return json.safe_dumps( value )
class PythonObjectParameter( MetadataParameter ):
@@ -510,6 +518,10 @@ class FileParameter( MetadataParameter ):
return str( self.spec.no_value )
return value.file_name
def to_safe_string( self, value ):
# We do not sanitize file names
return self.to_string( value )
def get_html_field( self, value=None, context=None, other_values=None, **kwd ):
context = context or {}
other_values = other_values or {}
+24 -6
View File
@@ -346,7 +346,7 @@ class Registry( object ):
"""
sniffer_elem_classes = [ e.attrib[ 'type' ] for e in self.sniffer_elems ]
sniffers = root.find( 'sniffers' )
if sniffers:
if sniffers is not None:
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
@@ -529,14 +529,13 @@ class Registry( object ):
converter.id = tool_dict[ 'guid' ]
break
if deactivate:
if converter.id in toolbox.tools_by_id:
del toolbox.tools_by_id[ converter.id ]
toolbox.remove_tool_by_id( converter.id, remove_from_panel=False )
if source_datatype in self.datatype_converters:
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
toolbox.register_tool( converter )
if source_datatype not in self.datatype_converters:
self.datatype_converters[ source_datatype ] = odict()
self.datatype_converters[ source_datatype ][ target_datatype ] = converter
@@ -622,6 +621,26 @@ class Registry( object ):
self.log.debug( "Adding inherited display application '%s' to datatype '%s'" % ( display_app.id, extension ) )
d_type1.add_display_application( display_app )
def reload_display_applications( self, display_application_ids=None ):
"""
Reloads display applications: by id, or all if no ids provided
Returns tuple( [reloaded_ids], [failed_ids] )
"""
if not display_application_ids:
display_application_ids = self.display_applications.keys()
elif not isinstance( display_application_ids, list ):
display_application_ids = [ display_application_ids ]
reloaded = []
failed = []
for display_application_id in display_application_ids:
try:
self.display_applications[ display_application_id ].reload()
reloaded.append( display_application_id )
except Exception, e:
self.log.debug( 'Requested to reload display application "%s", but failed: %s.', display_application_id, e )
failed.append( display_application_id )
return ( reloaded, failed )
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
@@ -645,8 +664,7 @@ class Registry( object ):
tmp_name = tempfile.NamedTemporaryFile()
tmp_name.write( tool_xml_text )
tmp_name.flush()
set_meta_tool = toolbox.load_tool( tmp_name.name )
toolbox.tools_by_id[ set_meta_tool.id ] = set_meta_tool
set_meta_tool = toolbox.load_hidden_tool( tmp_name.name )
self.set_external_metadata_tool = set_meta_tool
self.log.debug( "Loaded external metadata tool: %s", self.set_external_metadata_tool.id )
+32 -1
View File
@@ -17,7 +17,7 @@ from galaxy.datatypes import metadata
from galaxy.datatypes.checkers import is_gzip
from galaxy.datatypes.sniff import get_test_fname, get_headers
from galaxy.datatypes.metadata import MetadataElement
from galaxy.datatypes.util.image_util import check_image_type
try:
eggs.require( "bx-python" )
@@ -814,3 +814,34 @@ class Lav( data.Text ):
except:
return False
class RNADotPlotMatrix( data.Data ):
file_ext = "rna_eps"
def set_peek( self, dataset, is_multi_byte=False ):
if not dataset.dataset.purged:
dataset.peek = 'RNA Dot Plot format (Postscript derivative)'
dataset.blurb = data.nice_size( dataset.get_size() )
else:
dataset.peek = 'file does not exist'
dataset.blurb = 'file purged from disk'
def sniff(self, filename):
"""Determine if the file is in RNA dot plot format."""
if check_image_type( filename, ['EPS'] ):
seq = False
coor = False
pairs = False
with open( filename ) as handle:
for line in handle:
line = line.strip()
if line:
if line.startswith('/sequence'):
seq = True
elif line.startswith('/coor'):
coor = True
elif line.startswith('/pairs'):
pairs = True
if seq and coor and pairs:
return True
return False
+223 -2
View File
@@ -2,22 +2,22 @@
""" Clearing house for generic text datatypes that are not XML or tabular.
"""
from galaxy.datatypes.data import Text
from galaxy.datatypes.data import get_file_peek
from galaxy.datatypes.data import nice_size
from galaxy.datatypes.metadata import MetadataElement
from galaxy import util
import tempfile
import subprocess
import json
import gzip
import os
import re
import logging
log = logging.getLogger(__name__)
class Json( Text ):
file_ext = "json"
@@ -155,3 +155,224 @@ class Obo( Text ):
return True
return False
class Arff( Text ):
"""
An ARFF (Attribute-Relation File Format) file is an ASCII text file that describes a list of instances sharing a set of attributes.
http://weka.wikispaces.com/ARFF
"""
file_ext = "arff"
"""Add metadata elements"""
MetadataElement( name="comment_lines", default=0, desc="Number of comment lines", readonly=True, optional=True, no_value=0 )
MetadataElement( name="columns", default=0, desc="Number of columns", readonly=True, visible=True, no_value=0 )
def set_peek( self, dataset, is_multi_byte=False ):
if not dataset.dataset.purged:
dataset.peek = get_file_peek( dataset.file_name, is_multi_byte=is_multi_byte )
dataset.blurb = "Attribute-Relation File Format (ARFF)"
dataset.blurb += ", %s comments, %s attributes" % ( dataset.metadata.comment_lines, dataset.metadata.columns )
else:
dataset.peek = 'file does not exist'
dataset.blurb = 'file purged from disc'
def sniff( self, filename ):
"""
Try to guess the Arff filetype.
It usually starts with a "format-version:" string and has several stanzas which starts with "id:".
"""
with open( filename ) as handle:
relation_found = False
attribute_found = False
prefix = ""
for line_count, line in enumerate( handle ):
if line_count > 1000:
# only investigate the first 1000 lines
return False
line = line.strip()
if not line:
continue
start_string = line[:20].upper()
if start_string.startswith("@RELATION"):
relation_found = True
elif start_string.startswith("@ATTRIBUTE"):
attribute_found = True
elif start_string.startswith("@DATA"):
# @DATA should be the last data block
if relation_found and attribute_found:
return True
return False
def set_meta( self, dataset, **kwd ):
"""
Trying to count the comment lines and the number of columns included.
A typical ARFF data block looks like this:
@DATA
5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
"""
if dataset.has_data():
comment_lines = 0
first_real_line = False
data_block = False
with open( dataset.file_name ) as handle:
for line in handle:
line = line.strip()
if not line:
continue
if line.startswith('%') and not first_real_line:
comment_lines += 1
else:
first_real_line = True
if data_block:
if line.startswith('{'):
# Sparse representation
"""
@data
0, X, 0, Y, "class A", {5}
or
@data
{1 X, 3 Y, 4 "class A"}, {5}
"""
token = line.split('}',1)
first_part = token[0]
last_column = first_part.split(',')[-1].strip()
numeric_value = last_column.split()[0]
column_count = int(numeric_value)
if len(token) > 1:
# we have an additional weight
column_count -= 1
else:
columns = line.strip().split(',')
column_count = len(columns)
if columns[-1].strip().startswith('{'):
# we have an additional weight at the end
column_count -= 1
# We have now the column_count and we know the initial comment lines. So we can terminate here.
break
if line[:5].upper() == "@DATA":
data_block = True
dataset.metadata.comment_lines = comment_lines
dataset.metadata.columns = column_count
class SnpEffDb( Text ):
"""Class describing a SnpEff genome build"""
file_ext = "snpeffdb"
MetadataElement( name="genome_version", default=None, desc="Genome Version", readonly=True, visible=True, no_value=None )
MetadataElement( name="regulation", default=[], desc="Regulation Names", readonly=True, visible=True, no_value=[], optional=True)
MetadataElement( name="annotation", default=[], desc="Annotation Names", readonly=True, visible=True, no_value=[], optional=True)
def __init__( self, **kwd ):
Text.__init__( self, **kwd )
def set_meta( self, dataset, **kwd ):
Text.set_meta(self, dataset, **kwd )
data_dir = dataset.extra_files_path
## search data_dir/genome_version for files
regulation_pattern = 'regulation_(.+).bin'
# annotation files that are included in snpEff by a flag
annotations_dict = {'nextProt.bin' : '-nextprot','motif.bin': '-motif'}
regulations = []
annotations = []
if data_dir and os.path.isdir(data_dir):
for root, dirs, files in os.walk(data_dir):
for fname in files:
if fname.startswith('snpEffectPredictor'):
# if snpEffectPredictor.bin download succeeded
genome_version = os.path.basename(root)
dataset.metadata.genome_version = genome_version
else:
m = re.match(regulation_pattern,fname)
if m:
name = m.groups()[0]
regulations.append(name)
elif fname in annotations_dict:
value = annotations_dict[fname]
name = value.lstrip('-')
annotations.append(name)
dataset.metadata.regulation = regulations
dataset.metadata.annotation = annotations
try:
fh = file(dataset.file_name,'w')
fh.write("%s\n" % genome_version)
if annotations:
fh.write("annotations: %s\n" % ','.join(annotations))
if regulations:
fh.write("regulations: %s\n" % ','.join(regulations))
fh.close()
except:
pass
class SnpSiftDbNSFP( Text ):
"""Class describing a dbNSFP database prepared fpr use by SnpSift dbnsfp """
MetadataElement( name='reference_name', default='dbSNFP' , desc='Reference Name', readonly=True, visible=True, set_in_upload=True, no_value='dbSNFP' )
MetadataElement( name="bgzip", default=None, desc="dbNSFP bgzip", readonly=True, visible=True, no_value=None )
MetadataElement( name="index", default=None, desc="Tabix Index File", readonly=True, visible=True, no_value=None)
MetadataElement( name="annotation", default=[], desc="Annotation Names", readonly=True, visible=True, no_value=[] )
file_ext = "snpsiftdbnsfp"
composite_type = 'auto_primary_file'
allow_datatype_change = False
"""
## The dbNSFP file is a tabular file with 1 header line
## The first 4 columns are required to be: chrom pos ref alt
## These match columns 1,2,4,5 of the VCF file
## SnpSift requires the file to be block-gzipped and the indexed with samtools tabix
## Example:
## Compress using block-gzip algorithm
bgzip dbNSFP2.3.txt
## Create tabix index
tabix -s 1 -b 2 -e 2 dbNSFP2.3.txt.gz
"""
def __init__( self, **kwd ):
Text.__init__( self, **kwd )
self.add_composite_file( '%s.grp', description = 'Group File', substitute_name_with_metadata = 'reference_name', is_binary = False )
self.add_composite_file( '%s.ti', description = '', substitute_name_with_metadata = 'reference_name', is_binary = False )
def init_meta( self, dataset, copy_from=None ):
Text.init_meta( self, dataset, copy_from=copy_from )
def generate_primary_file( self, dataset = None ):
"""
This is called only at upload to write the html file
cannot rename the datasets here - they come with the default unfortunately
"""
regenerate_primary_file( self, dataset)
def regenerate_primary_file(self,dataset):
"""
cannot do this until we are setting metadata
"""
annotations = "dbNSFP Annotations: %s\n" % ','.join(dataset.metadata.annotation)
f = open(dataset.file_name,'a')
if dataset.metadata.bgzip:
bn = dataset.metadata.bgzip
f.write(bn)
f.write('\n')
f.write(annotations)
f.close()
def set_meta( self, dataset, overwrite=True, **kwd ):
try:
efp = dataset.extra_files_path
if os.path.exists(efp):
flist = os.listdir(efp)
for i,fname in enumerate(flist):
if fname.endswith('.gz'):
dataset.metadata.bgzip = fname
try:
fh = gzip.open(os.path.join(efp,fname),'r')
buf = fh.read(5000)
lines = buf.splitlines()
headers = lines[0].split('\t')
dataset.metadata.annotation = headers[4:]
except Exception,e:
log.warn("set_meta fname: %s %s" % (fname,str(e)))
finally:
fh.close()
if fname.endswith('.tbi'):
dataset.metadata.index = fname
self.regenerate_primary_file(dataset)
except Exception,e:
log.warn("set_meta fname: %s %s" % (dataset.file_name if dataset and dataset.file_name else 'Unkwown',str(e)))
-2
View File
@@ -71,6 +71,4 @@ def get_image_ext ( file_path, image ):
return 'pbm'
if format == 'PGM':
return 'pgm'
if format == 'EPS':
return 'eps'
return None
-2
View File
@@ -385,8 +385,6 @@ class GalaxyConfig( object ):
"DRMAA_python": lambda: "sge" in self.config.get( "app:main", "start_job_runners" ).split(","),
"drmaa": lambda: "drmaa" in self.config.get( "app:main", "start_job_runners" ).split(","),
"pbs_python": lambda: "pbs" in self.config.get( "app:main", "start_job_runners" ).split(","),
"threadframe": lambda: self.config.get( "app:main", "use_heartbeat" ),
"guppy": lambda: self.config.get( "app:main", "use_memdump" ),
"python_openid": lambda: self.config.get( "app:main", "enable_openid" ),
"python_daemon": lambda: sys.version_info[:2] >= ( 2, 5 ),
"pysam": lambda: check_pysam(),
+24 -2
View File
@@ -711,7 +711,7 @@ class JobWrapper( object ):
self.job_id = job.id
self.session_id = job.session_id
self.user_id = job.user_id
self.tool = queue.app.toolbox.tools_by_id.get( job.tool_id, None )
self.tool = queue.app.toolbox.get_tool( job.tool_id, job.tool_version, exact=True )
self.queue = queue
self.app = queue.app
self.sa_session = self.app.model.context
@@ -765,6 +765,10 @@ class JobWrapper( object ):
def get_parallelism(self):
return self.tool.parallelism
@property
def commands_in_new_shell(self):
return self.app.config.commands_in_new_shell
# legacy naming
get_job_runner = get_job_runner_url
@@ -1202,6 +1206,11 @@ 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 ] )
# TODO: eliminate overlap with tools/evaluation.py
out_collections = dict( [ ( obj.name, obj.dataset_collection_instance ) for obj in job.output_dataset_collection_instances ] )
out_collections.update( [ ( obj.name, obj.dataset_collection ) for obj in job.output_dataset_collections ] )
input_ext = 'data'
for _, data in inp_data.items():
# For loop odd, but sort simulating behavior in galaxy.tools.actions
@@ -1218,6 +1227,12 @@ class JobWrapper( object ):
'children': self.tool.collect_child_datasets(out_data, self.working_directory),
'primary': self.tool.collect_primary_datasets(out_data, self.working_directory, input_ext)
}
self.tool.collect_dynamic_collections(
out_collections,
job_working_directory=self.working_directory,
inp_data=inp_data,
job=job,
)
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 ).
@@ -1768,7 +1783,11 @@ class ComputeEnvironment( object ):
@abstractmethod
def new_file_path( self ):
""" Location to dump new files for this job on remote server. """
""" Absolute path to dump new files for this job on compute server. """
@abstractmethod
def tool_directory( self ):
""" Absolute path to tool files for this job on compute server. """
@abstractmethod
def version_path( self ):
@@ -1821,6 +1840,9 @@ class SharedComputeEnvironment( SimpleComputeEnvironment ):
def version_path( self ):
return self.job_wrapper.get_version_string_path()
def tool_directory( self ):
return os.path.abspath(self.job_wrapper.tool.tool_dir)
class NoopQueue( object ):
"""
+42 -3
View File
@@ -1,5 +1,6 @@
"""
Actions to be run at job completion. Currently only used in workflows.
Actions to be run at job completion (or output hda creation, as in the case of
immediate_actions listed below. Currently only used in workflows.
"""
import datetime
@@ -420,6 +421,42 @@ class DeleteIntermediatesAction(DefaultJobAction):
return "Delete parent datasets of this step created in this workflow that aren't flagged as outputs."
class TagDatasetAction(DefaultJobAction):
name = "TagDatasetAction"
verbose_name = "Add tag to dataset"
@classmethod
def execute(cls, app, sa_session, action, job, replacement_dict):
if action.action_arguments:
tags = [t.strip() for t in action.action_arguments.get('tags', '').split(',')]
if tags:
for dataset_assoc in job.output_datasets:
if action.output_name == '' or dataset_assoc.name == action.output_name:
app.tag_handler.set_tags_from_list( job.user, dataset_assoc.dataset, tags)
sa_session.flush()
@classmethod
def get_config_form(cls, trans):
form = """
if (pja.action_arguments && pja.action_arguments.tags){
p_str += "<label for='pja__"+pja.output_name+"__TagDatasetAction__tags'>Tags:</label>\
<input type='text' name='pja__"+pja.output_name+"__TagDatasetAction__tags' value=\\"" + pja.action_arguments.tags.replace(/"/g, "&quot;") + "\\"/>";
}
else{
p_str += "<label for='pja__"+pja.output_name+"__TagDatasetAction__tags'>Tags:</label>\
<input type='text' name='pja__"+pja.output_name+"__TagDatasetAction__tags' value=''/>";
}
"""
return get_form_template(cls.name, cls.verbose_name, form, "This action will set tags for the dataset.")
@classmethod
def get_short_str(cls, pja):
if pja.action_arguments and pja.action_arguments.get('tags', ''):
return "Add tag(s) '%s' to '%s'." % (pja.action_arguments['tags'], pja.output_name)
else:
return "Tag addition action used without a tag specified. No tag will be added."
class ActionBox(object):
actions = { "RenameDatasetAction": RenameDatasetAction,
@@ -428,11 +465,13 @@ class ActionBox(object):
"ColumnSetAction": ColumnSetAction,
"EmailAction": EmailAction,
"DeleteIntermediatesAction": DeleteIntermediatesAction,
"TagDatasetAction": TagDatasetAction,
}
public_actions = ['RenameDatasetAction', 'ChangeDatatypeAction',
'ColumnSetAction', 'EmailAction',
'DeleteIntermediatesAction']
immediate_actions = ['ChangeDatatypeAction', 'RenameDatasetAction']
'DeleteIntermediatesAction', 'TagDatasetAction']
immediate_actions = ['ChangeDatatypeAction', 'RenameDatasetAction',
'TagDatasetAction']
@classmethod
def get_short_str(cls, action):
+30 -22
View File
@@ -5,6 +5,7 @@ from os.path import abspath
CAPTURE_RETURN_CODE = "return_code=$?"
YIELD_CAPTURED_CODE = 'sh -c "exit $return_code"'
DEFAULT_SHELL = "/bin/sh"
from logging import getLogger
log = getLogger( __name__ )
@@ -45,28 +46,21 @@ def build_command(
if not container:
__handle_dependency_resolution(commands_builder, job_wrapper, remote_command_params)
if container:
# Stop now and build command before handling metadata and copying
# working directory files back. These should always happen outside
# of docker container - no security implications when generating
# metadata and means no need for Galaxy to be available to container
# and not copying workdir outputs back means on can be more restrictive
# of where container can write to in some circumstances.
local_container_script = join( job_wrapper.working_directory, "container.sh" )
fh = file( local_container_script, "w" )
fh.write( "#!/bin/sh\n%s" % commands_builder.build() )
fh.close()
chmod( local_container_script, 0755 )
compute_container_script = local_container_script
if 'working_directory' in remote_command_params:
compute_container_script = "/bin/sh %s" % join(remote_command_params['working_directory'], "container.sh")
run_in_container_command = container.containerize_command(
compute_container_script
)
commands_builder = CommandsBuilder( run_in_container_command )
if container or job_wrapper.commands_in_new_shell:
externalized_commands = __externalize_commands(job_wrapper, commands_builder, remote_command_params)
if container:
# Stop now and build command before handling metadata and copying
# working directory files back. These should always happen outside
# of docker container - no security implications when generating
# metadata and means no need for Galaxy to be available to container
# and not copying workdir outputs back means on can be more restrictive
# of where container can write to in some circumstances.
run_in_container_command = container.containerize_command(
externalized_commands
)
commands_builder = CommandsBuilder( run_in_container_command )
else:
commands_builder = CommandsBuilder( externalized_commands )
if include_work_dir_outputs:
__handle_work_dir_outputs(commands_builder, job_wrapper, runner, remote_command_params)
@@ -77,6 +71,20 @@ def build_command(
return commands_builder.build()
def __externalize_commands(job_wrapper, commands_builder, remote_command_params, script_name="tool_script.sh"):
local_container_script = join( job_wrapper.working_directory, script_name )
fh = file( local_container_script, "w" )
fh.write( "#!%s\n%s" % (DEFAULT_SHELL, commands_builder.build()))
fh.close()
chmod( local_container_script, 0755 )
commands = local_container_script
if 'working_directory' in remote_command_params:
commands = "%s %s" % (DEFAULT_SHELL, join(remote_command_params['working_directory'], script_name))
log.info("Built script [%s] for tool command[%s]" % (local_container_script, commands))
return commands
def __handle_version_command(commands_builder, job_wrapper):
# Prepend version string
write_version_cmd = job_wrapper.write_version_cmd
+1 -1
View File
@@ -356,7 +356,7 @@ class DataTransfer( object ):
for i, step in enumerate( workflow.steps ):
job = None
if step.type == 'tool' or step.type is None:
tool = self.app.toolbox.tools_by_id[ step.tool_id ]
tool = self.app.toolbox.get_tool( step.tool_id )
def callback( input, value, prefixed_name, prefixed_label ):
if isinstance( input, DataToolParameter ):
if prefixed_name in step.input_connections_by_name:
+1 -1
View File
@@ -119,7 +119,7 @@ class JobHandlerQueue( object ):
& ( model.Job.handler == self.app.config.server_name ) ).all()
for job in jobs_at_startup:
if job.tool_id not in self.app.toolbox.tools_by_id:
if not self.app.toolbox.has_tool( job.tool_id, job.tool_version, exact=True ):
log.warning( "(%s) Tool '%s' removed from tool config, unable to recover job" % ( job.id, job.tool_id ) )
self.job_wrapper( job ).fail( 'This tool was disabled before the job completed. Please contact your Galaxy administrator.' )
elif job.job_runner_name is not None and job.job_runner_external_id is None:
+1 -1
View File
@@ -216,7 +216,7 @@ class BaseJobRunner( object ):
job_tool = job_wrapper.tool
for (joda, dataset) in self._walk_dataset_outputs( job ):
if joda and job_tool:
hda_tool_output = job_tool.outputs.get( joda.name, None )
hda_tool_output = job_tool.find_output_def( joda.name )
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?
+3
View File
@@ -708,3 +708,6 @@ class PulsarComputeEnvironment( ComputeEnvironment ):
def unstructured_path_rewriter( self ):
return self.rewriter
def tool_directory( self ):
return self._tool_dir
+245
View File
@@ -0,0 +1,245 @@
""" Entry point for starting Galaxy without starting as part of a web server.
Example Usage: Start a job/workflow handler without a web server and with
a given name using.
python lib/galaxy/main.py --server-name handler0
Start as a daemon with (requires daemonized - install with 'pip install daemonize'):
python lib/galaxy/main.py -d --daemon-log-file=handler0-daemon.log --pid-file handler0.pid --server-name handler0
In daemon mode logging of Galaxy (as opposed to this script) is configured via
a loggers section in Galaxy's ini file - this can be overridden with sensible
defaults logging to a single file with the following:
python lib/galaxy/main.py -d --server-name handler0 --daemon-log-file=handler0-daemon.log --pid-file handler0.pid --log-file handler0.log
"""
import logging
from logging.config import fileConfig
import functools
import os
import time
import sys
try:
import ConfigParser as configparser
except ImportError:
import configparser
try:
from daemonize import Daemonize
except ImportError:
Daemonize = None
# Vaguely Python 2.6 compatibile ArgumentParser import
try:
from argparse import ArgumentParser
except ImportError:
from optparse import OptionParser
class ArgumentParser(OptionParser):
def __init__(self, **kwargs):
self.delegate = OptionParser(**kwargs)
def add_argument(self, *args, **kwargs):
if "required" in kwargs:
del kwargs["required"]
return self.delegate.add_option(*args, **kwargs)
def parse_args(self, args=None):
(options, args) = self.delegate.parse_args(args)
return options
REQUIRES_DAEMONIZE_MESSAGE = "Attempted to use Galaxy in daemon mode, but daemonize is unavailable."
log = logging.getLogger(__name__)
GALAXY_ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
GALAXY_LIB_DIR = os.path.join(GALAXY_ROOT_DIR, "lib")
DEFAULT_INI_APP = "main"
DEFAULT_INIS = ["config/galaxy.ini", "universe_wsgi.ini", "config/galaxy.ini.sample"]
DEFAULT_PID = "galaxy.pid"
DEFAULT_VERBOSE = True
DESCRIPTION = "Daemonized entry point for Galaxy."
def load_galaxy_app(
config_builder,
config_env=False,
log=None,
**kwds
):
# Allow specification of log so daemon can reuse properly configured one.
if log is None:
log = logging.getLogger(__name__)
# If called in daemon mode, set the ROOT directory and ensure Galaxy is on
# sys.path.
if config_env:
try:
os.chdir(GALAXY_ROOT_DIR)
except Exception:
log.exception("Failed to chdir")
raise
try:
sys.path.append(GALAXY_LIB_DIR)
except Exception:
log.exception("Failed to add Galaxy to sys.path")
raise
config_builder.setup_logging()
from galaxy.util.properties import load_app_properties
kwds = config_builder.app_kwds()
kwds = load_app_properties(**kwds)
from galaxy.app import UniverseApplication
app = UniverseApplication(
global_conf={"__file__": config_builder.ini_path},
**kwds
)
return app
def app_loop(args, log):
try:
config_builder = GalaxyConfigBuilder(args)
galaxy_app = load_galaxy_app(
config_builder,
config_env=True,
log=log,
)
except BaseException:
log.exception("Failed to initialize Galaxy application")
raise
sleep = True
while sleep:
try:
time.sleep(5)
except KeyboardInterrupt:
sleep = False
except SystemExit:
sleep = False
except Exception:
pass
try:
galaxy_app.shutdown()
except Exception:
log.exception("Failed to shutdown Galaxy application")
raise
def absolute_config_path(path, galaxy_root):
if path and not os.path.isabs(path):
path = os.path.join(galaxy_root, path)
return path
def find_ini(supplied_ini, galaxy_root):
if supplied_ini:
return supplied_ini
# If not explicitly supplied an ini, check server.ini and then
# just restort to sample if that has not been configured.
for guess in DEFAULT_INIS:
ini_path = os.path.join(galaxy_root, guess)
if os.path.exists(ini_path):
return ini_path
return guess
class GalaxyConfigBuilder(object):
""" Generate paste-like configuration from supplied command-line arguments.
"""
def __init__(self, args=None, **kwds):
ini_path = kwds.get("ini_path", None) or (args and args.ini_path)
# If given app_conf_path - use that - else we need to ensure we have an
# ini path.
if not ini_path:
galaxy_root = kwds.get("galaxy_root", GALAXY_ROOT_DIR)
ini_path = find_ini(ini_path, galaxy_root)
ini_path = absolute_config_path(ini_path, galaxy_root=galaxy_root)
self.ini_path = ini_path
self.app_name = kwds.get("app") or (args and args.app) or DEFAULT_INI_APP
self.log_file = (args and args.log_file)
@classmethod
def populate_options(cls, arg_parser):
arg_parser.add_argument("-c", "--ini-path", default=None, help="Galaxy ini config file (defaults to config/galaxy.ini)")
arg_parser.add_argument("--app", default=DEFAULT_INI_APP, help="app section in ini file (defaults to main)")
arg_parser.add_argument("-d", "--daemonize", default=False, help="Daemonzie process", action="store_true")
arg_parser.add_argument("--daemon-log-file", default=None, help="log file for daemon script ")
arg_parser.add_argument("--log-file", default=None, help="Galaxy log file (overrides log configuration in ini_path if set)")
arg_parser.add_argument("--pid-file", default=DEFAULT_PID, help="pid file (default is %s)" % DEFAULT_PID)
arg_parser.add_argument("--server-name", default=None, help="set a galaxy server name")
def app_kwds(self):
config = dict(
ini_file=self.ini_path,
ini_section="app:%s" % self.app_name,
)
return config
def setup_logging(self):
# Galaxy will attempt to setup logging if loggers is not present in
# ini config file - this handles that loggers block however if present
# (the way paste normally would)
if not self.ini_path:
return
raw_config = configparser.ConfigParser()
raw_config.read([self.ini_path])
if raw_config.has_section('loggers'):
config_file = os.path.abspath(self.ini_path)
fileConfig(
config_file,
dict(__file__=config_file, here=os.path.dirname(config_file))
)
def main():
arg_parser = ArgumentParser(description=DESCRIPTION)
GalaxyConfigBuilder.populate_options(arg_parser)
args = arg_parser.parse_args()
if args.log_file:
os.environ["GALAXY_CONFIG_LOG_DESTINATION"] = os.path.abspath(args.log_file)
if args.server_name:
os.environ["GALAXY_CONFIG_SERVER_NAME"] = args.server_name
pid_file = args.pid_file
log.setLevel(logging.DEBUG)
log.propagate = False
if args.daemonize:
if Daemonize is None:
raise ImportError(REQUIRES_DAEMONIZE_MESSAGE)
keep_fds = []
if args.daemon_log_file:
fh = logging.FileHandler(args.daemon_log_file, "w")
fh.setLevel(logging.DEBUG)
log.addHandler(fh)
keep_fds.append(fh.stream.fileno())
else:
fh = logging.StreamHandler(sys.stderr)
fh.setLevel(logging.DEBUG)
log.addHandler(fh)
daemon = Daemonize(
app="galaxy",
pid=pid_file,
action=functools.partial(app_loop, args, log),
verbose=DEFAULT_VERBOSE,
logger=log,
keep_fds=keep_fds,
)
daemon.start()
else:
app_loop(args, log)
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
"""
Mixins for Annotatable model managers and serializers.
"""
import logging
log = logging.getLogger( __name__ )
class AnnotatableManagerMixin( object ):
#: class of AnnotationAssociation (e.g. HistoryAnnotationAssociation)
annotation_assoc = None
# TODO: most of this seems to be covered by item_attrs.UsesAnnotations
# TODO: use these below (serializer/deserializer)
def user_annotation( self, trans, item, user ):
return item.get_item_annotation_str( self.app.model.context, user, item )
def owner_annotation( self, trans, item ):
return self.user_annotation( trans, item, item.user )
def delete_annotation( self, trans, item, user ):
return item.delete_item_annotation( self.app.model.context, user, item )
def annotate( self, trans, item, user, annotation ):
if annotation is None:
self.delete_annotation( self, trans, item, user )
return None
annotation_obj = item.add_item_annotation( self.app.model.context, user, item, annotation )
return annotation_obj.annotation
#def by_user( self, trans, user, **kwargs ):
# pass
class AnnotatableSerializerMixin( object ):
def add_serializers( self ):
self.serializers[ 'annotation' ] = self.serialize_annotation
def serialize_annotation( self, trans, item, key ):
"""
Get and serialize an `item`'s annotation.
"""
# user = item.user
#TODO: trans
user = trans.user
sa_session = self.app.model.context
returned = item.get_item_annotation_str( sa_session, user, item )
return returned
class AnnotatableDeserializerMixin( object ):
def add_deserializers( self ):
self.deserializers[ 'annotation' ] = self.deserialize_annotation
def deserialize_annotation( self, trans, item, key, val ):
"""
Make sure `val` is a valid annotation and assign it, deleting any existing
if `val` is None.
"""
val = self.validate.nullable_basestring( key, val )
sa_session = self.app.model.context
#TODO: trans
user = trans.user
if val is None:
item.delete_item_annotation( sa_session, user, item )
return None
annotated_item = item.add_item_annotation( sa_session, user, item, val )
return annotated_item.annotation
+927 -12
View File
@@ -1,23 +1,51 @@
"""
Keeps the older BaseController security and fetching methods and also
defines a base ModelManager, ModelSerializer, and ModelDeserializer.
ModelManagers are used for operations on models that occur outside the scope of
a single model object, such as:
- object creation
- object lookup
- interactions between 2+ objects of different model classes
(Since these were to replace model Mixins from
web/framework/base/controller.py the rule of thumb used there also generally
has been applied here: if it uses the trans or sa_session, put it in a manager
and not the model.)
ModelSerializers allow flexible conversion of model objects to dictionaries.
They control what keys are sent, how values are simplified, can remap keys,
and allow both predefined and user controlled key sets.
ModelDeserializers control how a model validates and process an incoming
attribute change to a model object.
"""
# TODO: it may be there's a better way to combine the above three classes
# such as: a single flat class, serializers being singletons in the manager, etc.
# instead of the three separate classes. With no 'apparent' perfect scheme
# I'm opting to just keep them separate.
import pkg_resources
pkg_resources.require( "SQLAlchemy >= 0.4" )
import sqlalchemy
pkg_resources.require("repoze.lru") # used by Routes
pkg_resources.require("Routes")
import routes
from galaxy import exceptions
from galaxy import model
from galaxy import web
from galaxy.model import tool_shed_install
import logging
log = logging.getLogger( __name__ )
class ModelManager( object ):
pass
class ModelSerializer( object ):
pass
# ==== accessors from base/controller.py
def security_check( trans, item, check_ownership=False, check_accessible=False ):
""" Security checks for an item: checks if (a) user owns item or (b) item
"""
Security checks for an item: checks if (a) user owns item or (b) item
is accessible to user. This is a generic method for dealing with objects
uniformly from the older controller mixin code - however whenever possible
the managers for a particular model should be used to perform security
@@ -49,7 +77,10 @@ def security_check( trans, item, check_ownership=False, check_accessible=False )
def get_class( class_name ):
""" Returns the class object that a string denotes. Without this method, we'd have to do eval(<class_name>). """
"""
Returns the class object that a string denotes. Without this method, we'd have
to do eval(<class_name>).
"""
if class_name == 'History':
item_class = model.History
elif class_name == 'HistoryDatasetAssociation':
@@ -118,3 +149,887 @@ def get_object( trans, id, class_name, check_ownership=False, check_accessible=F
% ( class_name, getattr( item, 'name', id ) ), type="warning" )
return item
# =============================================================================
class ModelManager( object ):
"""
Base class for all model/resource managers.
Provides common queries and CRUD operations as a (hopefully) light layer
over the ORM.
"""
model_class = object
foreign_key_name = None
def __init__( self, app ):
self.app = app
def session( self ):
return self.app.model.context
def _session_setattr( self, item, attr, val, fn=None, flush=True ):
if fn:
fn( item, attr, val )
else:
setattr( item, attr, val )
self.session().add( item )
if flush:
self.session().flush()
return item
# .... query foundation wrapper
def query( self, trans, eagerloads=True, filters=None, order_by=None, limit=None, offset=None, **kwargs ):
"""
Return a basic query from model_class, filters, order_by, and limit and offset.
Set eagerloads to False to disable them for this query.
"""
query = self.session().query( self.model_class )
# joined table loading
if eagerloads is False:
query = query.enable_eagerloads( False )
query = self._apply_orm_filters( query, filters )
query = self._apply_order_by( query, order_by )
query = self._apply_orm_limit_offset( query, limit, offset )
return query
# .... filters
def _apply_orm_filters( self, query, filters ):
"""
Add any filters to the given query.
"""
if filters is None:
return query
if not isinstance( filters, list ):
filters = [ filters ]
# note: implicit AND
for filter in filters:
query = query.filter( filter )
return query
def _munge_filters( self, filtersA, filtersB ):
"""
Combine two lists into a single list.
(While allowing them to be None, non-lists, or lists.)
"""
# TODO: there's nothing specifically filter or model-related here - move to util
if filtersA is None:
return filtersB
if filtersB is None:
return filtersA
if not isinstance( filtersA, list ):
filtersA = [ filtersA ]
if not isinstance( filtersB, list ):
filtersB = [ filtersB ]
return filtersA + filtersB
# .... order, limit, and offset
def _apply_order_by( self, query, order_by ):
"""
Return the query after adding the order_by clauses.
Use the manager's default_order_by if order_by is None.
"""
if order_by is None:
return query.order_by( *self._default_order_by() )
if isinstance( order_by, ( list, tuple ) ):
return query.order_by( *order_by )
return query.order_by( order_by )
def _default_order_by( self ):
"""
Returns a tuple of columns for the default order when getting multiple models.
"""
return ( self.model_class.create_time, )
def _apply_orm_limit_offset( self, query, limit, offset ):
"""
Return the query after applying the given limit and offset (if not None).
"""
if limit is not None:
query = query.limit( limit )
if offset is not None:
query = query.offset( offset )
return query
# .... query resolution
def one( self, trans, **kwargs ):
"""
Sends kwargs to build the query and returns one and only one model.
"""
query = self.query( trans, **kwargs )
return self._one_with_recast_errors( query )
def _one_with_recast_errors( self, query ):
"""
Call sqlalchemy's one and recast errors to serializable errors if any.
:raises exceptions.ObjectNotFound: if no model is found
:raises exceptions.InconsistentDatabase: if more than one model is found
"""
# overridden to raise serializable errors
try:
return query.one()
except sqlalchemy.orm.exc.NoResultFound:
raise exceptions.ObjectNotFound( self.model_class.__name__ + ' not found' )
except sqlalchemy.orm.exc.MultipleResultsFound:
raise exceptions.InconsistentDatabase( 'found more than one ' + self.model_class.__name__ )
def _one_or_none( self, query ):
"""
Return the object if found, None if it's not.
:raises exceptions.InconsistentDatabase: if more than one model is found
"""
try:
return self._one_with_recast_errors( query )
except exceptions.ObjectNotFound:
return None
#NOTE: at this layer, all ids are expected to be decoded and in int form
def by_id( self, trans, id, **kwargs ):
"""
Gets a model by primary id.
"""
id_filter = self.model_class.id == id
return self.one( trans, filters=id_filter, **kwargs )
# .... multirow queries
def _orm_list( self, trans, query=None, **kwargs ):
"""
Sends kwargs to build the query return all models found.
"""
query = query or self.query( trans, **kwargs )
return query.all()
#def list( self, trans, query=None, filters=None, order_by=None, limit=None, offset=None, **kwargs ):
def list( self, trans, filters=None, order_by=None, limit=None, offset=None, **kwargs ):
"""
Returns all objects matching the given filters
"""
orm_filters, fn_filters = self._split_filters( filters )
if not fn_filters:
# if no fn_filtering required, we can use the 'all orm' version with limit offset
return self._orm_list( trans, filters=orm_filters, order_by=order_by, limit=limit, offset=offset, **kwargs )
# fn filters will change the number of items returnable by limit/offset - remove them here from the orm query
query = self.query( trans, filters=orm_filters, order_by=order_by, limit=None, offset=None, **kwargs )
items = query.all()
# apply limit, offset after SQL filtering
items = self._apply_fn_filters_gen( items, fn_filters )
return list( self._apply_fn_limit_offset_gen( items, limit, offset ) )
def _split_filters( self, filters ):
"""
Splits `filters` into a tuple of two lists:
a list of filters to be added to the SQL query
and a list of functional filters to be applied after the SQL query.
"""
orm_filters, fn_filters = ( [], [] )
if filters is None:
return ( orm_filters, fn_filters )
if not isinstance( filters, list ):
filters = [ filters ]
for filter_ in filters:
if self._is_fn_filter( filter_ ):
fn_filters.append( filter_ )
else:
orm_filters.append( filter_ )
return ( orm_filters, fn_filters )
def _is_fn_filter( self, filter_ ):
"""
Returns True if `filter_` is a functional filter to be applied after the SQL query.
"""
return callable( filter_ )
def _apply_fn_filters_gen( self, items, filters ):
"""
If all the filter functions in `filters` return True for an item in `items`,
yield that item.
"""
#cpu-expensive
for item in items:
filter_results = map( lambda f: f( item ), filters )
if all( filter_results ):
yield item
def _apply_fn_limit_offset_gen( self, items, limit, offset ):
"""
Iterate over `items` and begin yielding items after
`offset` number of items and stop when we've yielded
`limit` number of items.
"""
# change negative limit, offset to None
if limit is not None and limit < 0:
limit = None
if offset is not None and offset < 0:
offset = None
yielded = 0
for i, item in enumerate( items ):
if offset is not None and i < offset:
continue
if limit is not None and yielded >= limit:
break
yield item
yielded += 1
def by_ids( self, trans, ids, filters=None, **kwargs ):
"""
Returns an in-order list of models with the matching ids in `ids`.
"""
ids_filter = self.model_class.id.in_( ids )
found = self.list( trans, filters=self._munge_filters( ids_filter, filters ), **kwargs )
# TODO: this does not order by the original 'ids' array
# ...could use get (supposedly since found are in the session, the db won't be hit twice)
# return map( self.session().query( self.model_class ).get, ids )
# ...could implement own version here - slow?
return self._order_items_by_id( ids, found )
def _order_items_by_id( self, ids, items ):
"""
Given a list of (unique) ids and a list of items having an 'id' attribute,
return items that have the given ids in that order.
If an id in ids is not found or if an item in items doesn't have a given
id, they will not be in the returned list.
"""
ID_ATTR_NAME = 'id'
# TODO:?? aside from sqlalx.get mentioned above, I haven't seen an in-SQL way
# to make this happen. This may not be the most efficient way either.
# NOTE: that this isn't sorting by id - this is matching the order in items to the order in ids
# move items list into dict by id
item_dict = {}
for item in items:
item_id = getattr( item, ID_ATTR_NAME, None )
if item_id:
item_dict[ item_id ] = item
# pull from map in order of ids
in_order = []
for id in ids:
if id in item_dict:
in_order.append( item_dict[ id ] )
return in_order
def create( self, trans, flush=True, *args, **kwargs ):
"""
Generically create a new model.
"""
# override in subclasses
item = self.model_class( *args, **kwargs )
self.session().add( item )
if flush:
self.session().flush()
return item
def copy( self, trans, item, **kwargs ):
"""
Clone or copy an item.
"""
raise exceptions.NotImplemented( 'Abstract method' )
def update( self, trans, item, new_values, flush=True, **kwargs ):
"""
Given a dictionary of new values, update `item` and return it.
..note: NO validation or deserialization occurs here.
"""
self.session().add( item )
for key, value in new_values.items():
if hasattr( item, key ):
setattr( item, key, value )
if flush:
self.session().flush()
return item
# TODO: yagni?
def associate( self, trans, associate_with, item, foreign_key_name=None ):
"""
Generically associate `item` with `associate_with` based on `foreign_key_name`.
"""
foreign_key_name = foreign_key_name or self.foreign_key_name
setattr( associate_with, foreign_key_name, item )
return item
def query_associated( self, trans, associated_model_class, item, foreign_key_name=None ):
"""
Generically query other items that have been associated with this `item`.
"""
foreign_key_name = foreign_key_name or self.foreign_key_name
foreign_key = getattr( associated_model_class, foreign_key_name )
return self.session().query( associated_model_class ).filter( foreign_key == item )
# a rename of sql DELETE to differentiate from the Galaxy notion of mark_as_deleted
# def destroy( self, trans, item, **kwargs ):
# return item
# ==== SERIALIZERS/to_dict,from_dict
class ModelSerializingError( exceptions.InternalServerError ):
"""Thrown when request model values can't be serialized"""
pass
class ModelDeserializingError( exceptions.ObjectAttributeInvalidException ):
"""Thrown when an incoming value isn't usable by the model
(bad type, out of range, etc.)
"""
pass
class ModelSerializer( object ):
"""
Turns models into JSONable dicts.
Maintains a map of requestable keys and the Callable() serializer functions
that should be called for those keys.
E.g. { 'x' : lambda trans, item, key: item.x, ... }
Note: if a key to serialize is not listed in the Serializer.serializable_keyset
or serializers, it will not be returned.
To serialize call:
my_serializer = MySerializer( app )
...
keys_to_serialize = [ 'id', 'name', 'attr1', 'attr2', ... ]
item_dict = MySerializer.serialize( trans, my_item, keys_to_serialize )
"""
#: 'service' to use for getting urls - use class var to allow overriding when testing
url_for = staticmethod( routes.url_for )
def __init__( self, app ):
"""
Set up serializer map, any additional serializable keys, and views here.
"""
self.app = app
# a list of valid serializable keys that can use the default (string) serializer
# this allows us to: 'mention' the key without adding the default serializer
# TODO: we may want to eventually error if a key is requested
# that is in neither serializable_keyset or serializers
self.serializable_keyset = set([])
# a map of dictionary keys to the functions (often lambdas) that create the values for those keys
self.serializers = {}
# add subclass serializers defined there
self.add_serializers()
# update the keyset by the serializers (removing the responsibility from subclasses)
self.serializable_keyset.update( self.serializers.keys() )
# views are collections of serializable attributes (a named array of keys)
# inspired by model.dict_{view}_visible_keys
self.views = {}
self.default_view = None
def add_serializers( self ):
"""
Register a map of attribute keys -> serializing functions that will serialize
the attribute.
"""
# to be overridden in subclasses
pass
def add_view( self, view_name, key_list, include_keys_from=None ):
"""
Add the list of serializable attributes `key_list` to the serializer's
view dictionary under the key `view_name`.
If `include_keys_from` is a proper view name, extend `key_list` by
the list in that view.
"""
key_list = list( set( key_list + self.views.get( include_keys_from, [] ) ) )
self.views[ view_name ] = key_list
self.serializable_keyset.update( key_list )
return key_list
def serialize( self, trans, item, keys ):
"""
Serialize the model `item` to a dictionary.
Given model `item` and the list `keys`, create and return a dictionary
built from each key in `keys` that also exists in `serializers` and
values of calling the keyed/named serializers on item.
"""
# main interface fn for converting a model to a dict
returned = {}
for key in keys:
# check both serializers and serializable keys
if key in self.serializers:
returned[ key ] = self.serializers[ key ]( trans, item, key )
elif key in self.serializable_keyset:
returned[ key ] = self.default_serializer( trans, item, key )
# ignore bad/unreg keys
return returned
def default_serializer( self, trans, item, key ):
"""
Serialize the `item`'s attribute named `key`.
"""
# TODO:?? point of change but not really necessary?
return getattr( item, key )
# serializers for common galaxy objects
def serialize_date( self, trans, item, key ):
"""
Serialize a date attribute of `item`.
"""
date = getattr( item, key )
return date.isoformat() if date is not None else None
def serialize_id( self, trans, item, key ):
"""
Serialize an id attribute of `item`.
"""
id = getattr( item, key )
return self.app.security.encode_id( id ) if id is not None else None
# serializing to a view where a view is a predefied list of keys to serialize
def serialize_to_view( self, trans, item, view=None, keys=None, default_view=None ):
"""
Use a predefined list of keys (the string `view`) and any additional keys
listed in `keys`.
The combinations can be:
`view` only: return those keys listed in the named view
`keys` only: return those keys listed
no `view` or `keys`: use the `default_view` if any
`view` and `keys`: combine both into one list of keys
"""
#TODO: default view + view makes no sense outside the API.index context - move default view there
all_keys = []
keys = keys or []
# chose explicit over concise here
if view:
if keys:
all_keys = self._view_to_keys( view ) + keys
else:
all_keys = self._view_to_keys( view )
else:
if keys:
all_keys = keys
elif default_view:
all_keys = self._view_to_keys( default_view )
return self.serialize( trans, item, all_keys )
def _view_to_keys( self, view=None ):
"""
Converts a known view into a list of keys.
:raises ModelSerializingError: if the view is not listed in `self.views`.
"""
if view is None:
view = self.default_view
if view not in self.views:
raise ModelSerializingError( 'unknown view', view=view, available_views=self.views )
return self.views[ view ][:]
class ModelDeserializer( object ):
"""
An object that converts an incoming serialized dict into values that can be
directly assigned to an item's attributes and assigns them.
"""
#: the class used to create this deserializer's generically accessible model_manager
model_manager_class = None
#TODO:?? a larger question is: which should be first? Deserialize then validate - or - validate then deserialize?
def __init__( self, app ):
"""
Set up deserializers and validator.
"""
self.app = app
self.deserializers = {}
self.deserializable_keyset = set([])
self.add_deserializers()
# a sub object that can validate incoming values
self.validate = ModelValidator( self.app )
# create a generically accessible manager for the model this deserializer works with/for
self.manager = None
if self.model_manager_class:
self.manager = self.model_manager_class( self.app )
def add_deserializers( self ):
"""
Register a map of attribute keys -> functions that will deserialize data
into attributes to be assigned to the item.
"""
# to be overridden in subclasses
pass
def deserialize( self, trans, item, data, flush=True ):
"""
Convert an incoming serialized dict into values that can be
directly assigned to an item's attributes and assign them
"""
sa_session = self.app.model.context
new_dict = {}
for key, val in data.items():
if key in self.deserializers:
new_dict[ key ] = self.deserializers[ key ]( trans, item, key, val )
# !important: don't error on unreg. keys -- many clients will add weird ass keys onto the model
# TODO:?? add and flush here or in manager?
if flush and len( new_dict ):
sa_session.add( item )
sa_session.flush()
return new_dict
# ... common deserializers for primitives
def default_deserializer( self, trans, item, key, val ):
"""
If the incoming `val` is different than the `item` value change it
and, in either case, return the value.
"""
# TODO: sets the item attribute to value (this may not work in all instances)
# only do the following if val == getattr( item, key )
if hasattr( item, key ) and getattr( item, key ) != val:
setattr( item, key, val )
return val
def deserialize_basestring( self, trans, item, key, val ):
val = self.validate.basestring( key, val )
return self.default_deserializer( trans, item, key, val )
def deserialize_bool( self, trans, item, key, val ):
val = self.validate.bool( key, val )
return self.default_deserializer( trans, item, key, val )
def deserialize_int( self, trans, item, key, val, min=None, max=None ):
val = self.validate.int_range( key, val, min, max )
return self.default_deserializer( trans, item, key, val )
#def deserialize_date( self, trans, item, key, val ):
# #TODO: parse isoformat date into date object
# ... common deserializers for Galaxy
def deserialize_genome_build( self, trans, item, key, val ):
"""
Make sure `val` is a valid dbkey and assign it.
"""
val = self.validate.genome_build( trans, key, val )
return self.default_deserializer( trans, item, key, val )
class ModelValidator( object ):
"""
An object that inspects a dictionary (generally meant to be a set of
new/updated values for the model) and raises an error if a value is
not acceptable.
"""
def __init__( self, app, *args, **kwargs ):
self.app = app
def type( self, key, val, types ):
"""
Check `val` against the type (or tuple of types) in `types`.
:raises exceptions.RequestParameterInvalidException: if not an instance.
"""
if not isinstance( val, types ):
msg = 'must be a type: %s' % ( str( types ) )
raise exceptions.RequestParameterInvalidException( msg, key=key, val=val )
return val
# validators for primitives and compounds of primitives
def basestring( self, key, val ):
return self.type( key, val, basestring )
def bool( self, key, val ):
return self.type( key, val, bool )
def int( self, key, val ):
return self.type( key, val, int )
def nullable_basestring( self, key, val ):
"""
Must be a basestring or None.
"""
return self.type( key, val, ( basestring, type( None ) ) )
def int_range( self, key, val, min=None, max=None ):
"""
Must be a int between min and max.
"""
val = self.type( key, val, int )
if min is not None and val < min:
raise exceptions.RequestParameterInvalidException( "less than minimum", key=key, val=val, min=min )
if max is not None and val > max:
raise exceptions.RequestParameterInvalidException( "greater than maximum", key=key, val=val, max=max )
return val
def basestring_list( self, key, val ):
"""
Must be a list of basestrings.
"""
# TODO: Here's where compound types start becoming a nightmare. Any more or more complex
# and should find a different way.
val = self.type( key, val, list )
return [ self.basestring( key, elem ) for elem in val ]
# validators for Galaxy
def genome_build( self, trans, key, val ):
"""
Must be a valid genome_build/dbkey/reference/whatever-the-hell-were-calling-them-now.
"""
# TODO: is this correct?
if val is None:
return '?'
for genome_build_shortname, longname in self.app.genome_builds.get_genome_build_names( trans=trans ):
if val == genome_build_shortname:
return val
raise exceptions.RequestParameterInvalidException( "invalid reference", key=key, val=val )
# def slug( self, trans, item, key, val ):
# """validate slug"""
# pass
# ==== Building query filters based on model data
class ModelFilterParser( object ):
"""
Converts string tuples (partially converted query string params) of
attr, op, val into either:
- ORM based filters (filters that can be applied by the ORM at the SQL
level) or
- functional filters (filters that use derived values or values not
within the SQL tables)
These filters can then be applied to queries.
This abstraction allows 'smarter' application of limit and offset at either the
SQL level or the generator/list level based on the presence of functional
filters. In other words, if no functional filters are present, limit and offset
may be applied at the SQL level. If functional filters are present, limit and
offset need to applied at the list level.
These might be safely be replaced in the future by creating SQLAlchemy
hybrid properties or more thoroughly mapping derived values.
"""
#??: this class kindof 'lives' in both the world of the controllers/param-parsing and to models/orm
# (as the model informs how the filter params are parsed)
# I have no great idea where this 'belongs', so it's here for now
#: model class
model_class = None
def __init__( self, app ):
"""
Set up serializer map, any additional serializable keys, and views here.
"""
self.app = app
#: dictionary containing parsing data for ORM/SQLAlchemy-based filters
#..note: although kind of a pain in the ass and verbose, opt-in/whitelisting allows more control
#: over potentially expensive queries
self.orm_filter_parsers = {}
#: dictionary containing parsing data for functional filters - applied after a query is made
self.fn_filter_parsers = {}
# set up both of the above
self._add_parsers()
def _add_parsers( self ):
"""
Set up, extend, or alter `orm_filter_parsers` and `fn_filter_parsers`.
"""
self.orm_filter_parsers.update({
# (prob.) applicable to all models
'id' : { 'op': ( 'in' ), 'val': self.parse_id_list },
# dates can be directly passed through the orm into a filter (no need to parse into datetime object)
'create_time' : { 'op': ( 'le', 'ge' ) },
'update_time' : { 'op': ( 'le', 'ge' ) },
})
def parse_filters( self, filter_tuple_list ):
"""
Parse string 3-tuples (attr, op, val) into orm or functional filters.
"""
parsed = []
for ( attr, op, val ) in filter_tuple_list:
filter_ = self.parse_filter( attr, op, val )
parsed.append( filter_ )
return parsed
def parse_filter( self, attr, op, val ):
"""
Attempt to parse filter as a custom/fn filter, then an orm filter, and
if neither work - raise an error.
:raises exceptions.RequestParameterInvalidException: if no functional or orm
filter can be parsed.
"""
try:
# check for a custom filter
fn_filter = self._parse_fn_filter( attr, op, val )
if fn_filter is not None:
return fn_filter
# if no custom filter found, try to make an ORM filter
#note: have to use explicit is None here, bool( sqlalx.filter ) == False
orm_filter = self._parse_orm_filter( attr, op, val )
if orm_filter is not None:
return orm_filter
# by convention, assume most val parsers raise ValueError
except ValueError, val_err:
raise exceptions.RequestParameterInvalidException( 'unparsable value for filter',
column=attr, operation=op, value=val )
# if neither of the above work, raise an error with how-to info
#TODO: send back all valid filter keys in exception for added user help
raise exceptions.RequestParameterInvalidException( 'bad filter', column=attr, operation=op )
# ---- fn filters
def _parse_fn_filter( self, attr, op, val ):
"""
"""
# fn_filter_list is a dict: fn_filter_list[ attr ] = { 'opname1' : opfn1, 'opname2' : opfn2, etc. }
# attr, op is a nested dictionary pointing to the filter fn
attr_map = self.fn_filter_parsers.get( attr, None )
if not attr_map:
return None
allowed_ops = attr_map.get( 'op' )
# allowed ops is a map here, op => fn
filter_fn = allowed_ops.get( op, None )
if not filter_fn:
return None
# parse the val from string using the 'val' parser if present (otherwise, leave as string)
val_parser = attr_map.get( 'val', None )
if val_parser:
val = val_parser( val )
# curry/partial and fold the val in there now
return lambda i: filter_fn( i, val )
# ---- ORM filters
def _parse_orm_filter( self, attr, op, val ):
"""
"""
# orm_filter_list is a dict: orm_filter_list[ attr ] = <list of allowed ops>
# attr must be a whitelisted column
column = self.model_class.table.columns.get( attr )
column_map = self.orm_filter_parsers.get( attr, None )
if column is None or not column_map:
return None
# op must be whitelisted: contained in the list orm_filter_list[ attr ][ 'op' ]
allowed_ops = column_map.get( 'op' )
if op not in allowed_ops:
return None
op = self._convert_op_string_to_fn( column, op )
# parse the val from string using the 'val' parser if present (otherwise, leave as string)
val_parser = column_map.get( 'val', None )
if val_parser:
val = val_parser( val )
orm_filter = op( val )
return orm_filter
#: these are the easier/shorter string equivalents to the python operator fn names that need '__' around them
UNDERSCORED_OPS = ( 'lt', 'le', 'eq', 'ne', 'ge', 'gt' )
#UNCHANGED_OPS = ( 'like' )
def _convert_op_string_to_fn( self, column, op_string ):
"""
Convert the query string filter op shorthand into actual ORM usable
function names, then return the ORM function.
"""
# correct op_string to usable function key
fn_name = op_string
if op_string in self.UNDERSCORED_OPS:
fn_name = '__' + op_string + '__'
elif op_string == 'in':
fn_name = 'in_'
# get the column fn using the op_string and error if not a callable attr
#TODO: special case 'not in' - or disallow?
op_fn = getattr( column, fn_name, None )
if not op_fn or not callable( op_fn ):
return None
return op_fn
# --- more parsers! yay!
#TODO: These should go somewhere central - we've got ~6 parser modules/sections now
#TODO: to annotatable
def _owner_annotation( self, item ):
"""
Get the annotation by the item's owner.
"""
if not item.user:
return None
for annotation in item.annotations:
if annotation.user == item.user:
return annotation.annotation
return None
def filter_annotation_contains( self, item, val ):
"""
Test whether `val` is in the owner's annotation.
"""
owner_annotation = self._owner_annotation( item )
if owner_annotation is None:
return False
return val in owner_annotation
#TODO: to taggable
def _tag_str_gen( self, item ):
"""
Return a list of strings built from the item's tags.
"""
#TODO: which user is this? all?
for tag in item.tags:
tag_str = tag.user_tname
if tag.value is not None:
tag_str += ":" + tag.user_value
yield tag_str
def filter_has_partial_tag( self, item, val ):
"""
Return True if any tag partially contains `val`.
"""
for tag_str in self._tag_str_gen( item ):
if val in tag_str:
return True
return False
def filter_has_tag( self, item, val ):
"""
Return True if any tag exactly equals `val`.
"""
for tag_str in self._tag_str_gen( item ):
if val == tag_str:
return True
return False
def parse_bool( self, bool_string ):
"""
Parse a boolean from a string.
"""
# Be strict here to remove complexity of options (but allow already parsed).
if bool_string in ( 'True', True ):
return True
if bool_string in ( 'False', False ):
return False
raise ValueError( 'invalid boolean: ' + str( bool_string ) )
def parse_id_list( self, id_list_string, sep=',' ):
"""
Split `id_list_string` at `sep`.
"""
#TODO: move id decoding out
id_list = [ self.app.security.decode_id( id_ ) for id_ in id_list_string.split( sep ) ]
return id_list
+57 -21
View File
@@ -1,20 +1,18 @@
from galaxy.dataset_collections.registry import DatasetCollectionTypesRegistry
from galaxy.dataset_collections.matching import MatchingCollections
from galaxy.dataset_collections.type_description import CollectionTypeDescriptionFactory
from galaxy.dataset_collections import builder
from galaxy import model
from galaxy.exceptions import MessageException
from galaxy.dataset_collections import builder
from galaxy.dataset_collections.matching import MatchingCollections
from galaxy.dataset_collections.registry import DatasetCollectionTypesRegistry
from galaxy.dataset_collections.type_description import CollectionTypeDescriptionFactory
from galaxy.exceptions import ItemAccessibilityException
from galaxy.exceptions import MessageException
from galaxy.exceptions import RequestParameterInvalidException
from galaxy.managers import hdas # TODO: Refactor all mixin use into managers.
from galaxy.managers import histories
from galaxy.managers import lddas
from galaxy.managers import tags
from galaxy.managers.collections_util import validate_input_element_identifiers
from galaxy.util import validation
from galaxy.util import odict
from galaxy.util import validation
import logging
log = logging.getLogger( __name__ )
@@ -28,16 +26,18 @@ class DatasetCollectionManager( object ):
Abstraction for interfacing with dataset collections instance - ideally abstarcts
out model and plugin details.
"""
ELEMENTS_UNINITIALIZED = object()
def __init__( self, app ):
self.type_registry = DatasetCollectionTypesRegistry( app )
self.collection_type_descriptions = CollectionTypeDescriptionFactory( self.type_registry )
self.model = app.model
self.security = app.security
self.hda_manager = hdas.HDAManager()
self.history_manager = histories.HistoryManager()
self.tag_manager = tags.TagsManager( app )
self.ldda_manager = lddas.LDDAManager( )
self.hda_manager = hdas.HDAManager( app )
self.history_manager = histories.HistoryManager( app )
self.tag_manager = tags.TagManager( app )
self.ldda_manager = lddas.LDDAManager( app )
def create(
self,
@@ -63,6 +63,7 @@ class DatasetCollectionManager( object ):
element_identifiers=element_identifiers,
elements=elements,
)
if isinstance( parent, model.History ):
dataset_collection_instance = self.model.HistoryDatasetCollectionAssociation(
collection=dataset_collection,
@@ -72,19 +73,26 @@ class DatasetCollectionManager( object ):
for input_name, input_collection in implicit_collection_info[ "implicit_inputs" ]:
dataset_collection_instance.add_implicit_input_collection( input_name, input_collection )
for output_dataset in implicit_collection_info.get( "outputs" ):
output_dataset.hidden_beneath_collection_instance = dataset_collection_instance
if isinstance( output_dataset, model.HistoryDatasetCollectionAssociation ):
dataset_collection_instance.add_implicit_input_collection( input_name, input_collection )
else:
# dataset collection, don't need to do anything...
pass
trans.sa_session.add( output_dataset )
dataset_collection_instance.implicit_output_name = implicit_collection_info[ "implicit_output_name" ]
log.debug("Created collection with %d elements" % ( len( dataset_collection_instance.collection.elements ) ) )
# Handle setting hid
parent.add_dataset_collection( dataset_collection_instance )
elif isinstance( parent, model.LibraryFolder ):
dataset_collection_instance = self.model.LibraryDatasetCollectionAssociation(
collection=dataset_collection,
folder=parent,
name=name,
)
else:
message = "Internal logic error - create called with unknown parent type %s" % type( parent )
log.exception( message )
@@ -92,6 +100,18 @@ class DatasetCollectionManager( object ):
return self.__persist( dataset_collection_instance )
def create_dataset_collection(
self,
trans,
collection_type,
elements=None,
):
return self.__create_dataset_collection(
trans=trans,
collection_type=collection_type,
elements=elements,
)
def __create_dataset_collection(
self,
trans,
@@ -113,11 +133,26 @@ class DatasetCollectionManager( object ):
elements = self.__load_elements( trans, element_identifiers )
# else if elements is set, it better be an ordered dict!
type_plugin = collection_type_description.rank_type_plugin()
dataset_collection = builder.build_collection( type_plugin, elements )
if elements is not self.ELEMENTS_UNINITIALIZED:
type_plugin = collection_type_description.rank_type_plugin()
dataset_collection = builder.build_collection( type_plugin, elements )
else:
dataset_collection = model.DatasetCollection( populated=False )
dataset_collection.collection_type = collection_type
return dataset_collection
def set_collection_elements( self, dataset_collection, dataset_instances ):
if dataset_collection.populated:
raise Exception("Cannot reset elements of an already populated dataset collection.")
collection_type = dataset_collection.collection_type
collection_type_description = self.collection_type_descriptions.for_collection_type( collection_type )
type_plugin = collection_type_description.rank_type_plugin()
builder.set_collection_elements( dataset_collection, type_plugin, dataset_instances )
dataset_collection.mark_as_populated()
return dataset_collection
def delete( self, trans, instance_type, id ):
dataset_collection_instance = self.get_dataset_collection_instance( trans, instance_type, id, check_ownership=True )
dataset_collection_instance.deleted = True
@@ -154,8 +189,6 @@ class DatasetCollectionManager( object ):
return source_hdca
def _set_from_dict( self, trans, dataset_collection_instance, new_data ):
# Blatantly stolen from UsesHistoryDatasetAssociationMixin.set_hda_from_dict.
# send what we can down into the model
changed = dataset_collection_instance.set_from_dict( new_data )
# the rest (often involving the trans) - do here
@@ -163,7 +196,7 @@ class DatasetCollectionManager( object ):
dataset_collection_instance.add_item_annotation( trans.sa_session, trans.get_user(), dataset_collection_instance, new_data[ 'annotation' ] )
changed[ 'annotation' ] = new_data[ 'annotation' ]
if 'tags' in new_data.keys() and trans.get_user():
self.tag_manager.set_tags_from_list( trans, dataset_collection_instance, new_data[ 'tags' ], user=trans.user )
self.tag_manager.set_tags_from_list( trans.get_user(), dataset_collection_instance, new_data[ 'tags' ] )
if changed.keys():
trans.sa_session.flush()
@@ -223,7 +256,7 @@ class DatasetCollectionManager( object ):
return elements
def __load_element( self, trans, element_identifier ):
#if not isinstance( element_identifier, dict ):
# if not isinstance( element_identifier, dict ):
# # Is allowing this to just be the id of an hda too clever? Somewhat
# # consistent with other API methods though.
# element_identifier = dict( src='hda', id=str( element_identifier ) )
@@ -244,7 +277,7 @@ class DatasetCollectionManager( object ):
if src_type == 'hda':
decoded_id = int( trans.app.security.decode_id( encoded_id ) )
element = self.hda_manager.get( trans, decoded_id, check_ownership=False )
element = self.hda_manager.get_accessible( trans, decoded_id, trans.user )
elif src_type == 'ldda':
element = self.ldda_manager.get( trans, encoded_id )
elif src_type == 'hdca':
@@ -278,7 +311,10 @@ class DatasetCollectionManager( object ):
def __get_history_collection_instance( self, trans, id, check_ownership=False, check_accessible=True ):
instance_id = int( trans.app.security.decode_id( id ) )
collection_instance = trans.sa_session.query( trans.app.model.HistoryDatasetCollectionAssociation ).get( instance_id )
self.history_manager.secure( trans, collection_instance.history, check_ownership=check_ownership, check_accessible=check_accessible )
if check_ownership:
self.history_manager.error_unless_owner( trans, collection_instance.history, trans.user )
if check_accessible:
self.history_manager.error_unless_accessible( trans, collection_instance.history, trans.user )
return collection_instance
def __get_library_collection_instance( self, trans, id, check_ownership=False, check_accessible=True ):
+9 -6
View File
@@ -28,7 +28,6 @@ def api_payload_to_create_params( payload ):
element_identifiers=payload.get( "element_identifiers" ),
name=payload.get( "name", None ),
)
return params
@@ -65,9 +64,9 @@ def validate_input_element_identifiers( element_identifiers ):
validate_input_element_identifiers( element_identifier[ "element_identifiers" ] )
def dictify_dataset_collection_instance( dataset_colleciton_instance, parent, security, view="element" ):
dict_value = dataset_colleciton_instance.to_dict( view=view )
encoded_id = security.encode_id( dataset_colleciton_instance.id )
def dictify_dataset_collection_instance( dataset_collection_instance, parent, security, view="element" ):
dict_value = dataset_collection_instance.to_dict( view=view )
encoded_id = security.encode_id( dataset_collection_instance.id )
if isinstance( parent, model.History ):
encoded_history_id = security.encode_id( parent.id )
dict_value[ 'url' ] = web.url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset_collection" )
@@ -77,7 +76,9 @@ def dictify_dataset_collection_instance( dataset_colleciton_instance, parent, se
# TODO: Work in progress - this end-point is not right yet...
dict_value[ 'url' ] = web.url_for( 'library_content', library_id=encoded_library_id, id=encoded_id, folder_id=encoded_folder_id )
if view == "element":
dict_value[ 'elements' ] = map( dictify_element, dataset_colleciton_instance.collection.elements )
collection = dataset_collection_instance.collection
dict_value[ 'elements' ] = map( dictify_element, collection.elements )
dict_value[ 'populated' ] = collection.populated
security.encode_all_ids( dict_value, recursive=True ) # TODO: Use Kyle's recusrive formulation of this.
return dict_value
@@ -87,7 +88,9 @@ def dictify_element( element ):
object_detials = element.element_object.to_dict()
if element.child_collection:
# Recursively yield elements for each nested collection...
object_detials[ "elements" ] = map( dictify_element, element.child_collection.elements )
child_collection = element.child_collection
object_detials[ "elements" ] = map( dictify_element, child_collection.elements )
object_detials[ "populated" ] = child_collection.populated
dictified[ "object" ] = object_detials
return dictified
+276
View File
@@ -0,0 +1,276 @@
"""
Manager and Serializer for Datasets.
"""
from galaxy import model
from galaxy import exceptions
from galaxy.managers import base
from galaxy.managers import secured
from galaxy.managers import deletable
from galaxy.managers import users
import logging
log = logging.getLogger( __name__ )
class DatasetManager( base.ModelManager, secured.AccessibleManagerMixin, deletable.PurgableManagerMixin ):
"""
Manipulate datasets: the components contained in DatasetAssociations/DatasetInstances/HDAs/LDDAs
"""
model_class = model.Dataset
foreign_key_name = 'dataset'
#TODO:?? get + error_if_uploading is common pattern, should upload check be worked into access/owed?
def __init__( self, app ):
super( DatasetManager, self ).__init__( app )
# need for admin test
self.user_manager = users.UserManager( app )
def create( self, trans, flush=True, **kwargs ):
"""
Create and return a new Dataset object.
"""
# default to NEW state on new datasets
kwargs.update( dict( state=( kwargs.get( 'state', model.Dataset.states.NEW ) ) ) )
dataset = model.Dataset( **kwargs )
self.app.model.context.add( dataset )
if flush:
self.app.model.context.flush()
return dataset
#def copy( self, dataset, **kwargs ):
# """
# Clone, update, and return the given dataset.
# """
# pass
#def to_hda( self, trans, dataset, history, **kwargs ):
# """
# Create an hda from this dataset.
# """
# pass
#def to_ldda( self, trans, dataset, library_folder, **kwargs ):
# """
# Create an ldda from this dataset.
# """
# pass
#TODO: this may be more conv. somewhere else
#TODO: how to allow admin bypass?
def error_unless_dataset_purge_allowed( self, trans, item, msg=None ):
if not self.app.config.allow_user_dataset_purge:
msg = msg or 'This instance does not allow user dataset purging'
raise exceptions.ConfigDoesNotAllowException( msg )
return item
def purge( self, trans, dataset, flush=True ):
"""
Remove the object_store/file for this dataset from storage and mark
as purged.
:raises exceptions.ConfigDoesNotAllowException: if the instance doesn't allow
"""
self.error_unless_dataset_purge_allowed( trans, dataset )
# the following also marks dataset as purged and deleted
dataset.full_delete()
self.app.model.context.add( dataset )
if flush:
self.app.model.context.flush()
return dataset
# .... accessibility
# datasets can implement the accessible interface, but accessibility is checked in an entirely different way
# than those resources that have a user attribute (histories, pages, etc.)
def is_accessible( self, trans, dataset, user ):
"""
Is this dataset readable/viewable to user?
"""
if self.user_manager.is_admin( trans, user ):
return True
if self.has_access_permission( trans, dataset, user ):
return True
return False
def has_access_permission( self, trans, dataset, user ):
"""
Return T/F if the user has role-based access to the dataset.
"""
roles = user.all_roles() if user else []
return self.app.security_agent.can_access_dataset( roles, dataset )
#TODO: these need work
def _access_permission( self, trans, dataset, user=None, role=None ):
"""
Return most recent DatasetPermissions for the dataset and user.
"""
security_agent = self.app.security_agent
access_action = security_agent.permitted_actions.DATASET_ACCESS.action
# get a list of role ids to check access for (defaulting to all_roles)
user_roles = [ role ] if role else user.all_roles()
user_role_ids = [ r.id for r in user_roles ]
query = ( self.app.model.context.query( model.DatasetPermissions )
.filter( model.DatasetPermissions.action == access_action )
.filter( model.DatasetPermissions.dataset == dataset )
.filter( model.DatasetPermissions.role_id.in_( user_role_ids ) ) )
#TODO:?? most recent?
return query.first()
def _create_access_permission( self, trans, dataset, role, flush=True ):
"""
Create a new DatasetPermissions for the given dataset and role.
"""
access_action = self.app.security_agent.permitted_actions.DATASET_ACCESS.action
permission = model.DatasetPermissions( access_action, dataset, role )
self.app.model.context.add( permission )
if flush:
self.app.model.context.flush()
return permission
#def give_access_permission( self, trans, dataset, user, flush=True ):
# """
# """
# # for now, use the user's private role
# security_agent = self.app.security_agent
# user_role = security_agent.get_private_user_role( user )
#
# existing_permission = self._access_permission( trans, dataset, role=user_role )
# print 'existing access_perm:', existing_permission
# if existing_permission:
# return dataset
#
# permission = self._create_access_permission( trans, dataset, user_role )
# print 'access_roles:', [ role.name for role in dataset.get_access_roles( trans ) ]
#
# #access_action = security_agent.permitted_actions.DATASET_ACCESS.action
# #access_action = security_agent.get_action( access_action )
# #permissions = { access_action : [ user_role ] }
# #security_agent.set_dataset_permission( dataset, permissions )
#
# #self.app.model.context.add( dataset )
# #if flush:
# # self.app.model.context.flush()
#
# dbl_chk = self._access_permission( trans, dataset, role=user_role )
# print 'access_perm:', dbl_chk
#
# return dataset
#def remove_access_permission( self, trans, dataset, user ):
# """
# """
# pass
# .... manage/modify
#def has_manage_permission( self, trans, dataset, user ):
# """
# """
# pass
#
#def give_manage_permission( self, trans, dataset, user ):
# """
# """
# pass
#
#def remove_manage_permission( self, trans, dataset, user ):
# """
# """
# pass
#TODO: implement above for groups
#TODO: datatypes?
# .... data, object_store
class DatasetSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ):
def __init__( self, app ):
super( DatasetSerializer, self ).__init__( app )
self.default_view = 'summary'
self.add_view( 'summary', [
'id',
'create_time', 'update_time',
'state',
'deleted', 'purged', 'purgable',
#'object_store_id',
#'external_filename',
#'extra_files_path',
'file_size', 'total_size',
'uuid',
])
# could do visualizations and/or display_apps
def add_serializers( self ):
super( DatasetSerializer, self ).add_serializers()
deletable.PurgableSerializerMixin.add_serializers( self )
self.serializers.update({
'id' : self.serialize_id,
'create_time' : self.serialize_date,
'update_time' : self.serialize_date,
'uuid' : lambda t, i, k: str( i.uuid ) if i.uuid else None,
})
class DatasetDeserializer( base.ModelDeserializer, deletable.PurgableDeserializerMixin ):
model_manager_class = DatasetManager
def add_deserializers( self ):
super( DatasetDeserializer, self ).add_deserializers()
deletable.PurgableDeserializerMixin.add_deserializers( self )
# ============================================================================= AKA DatasetInstanceManager
class DatasetAssociationManager( base.ModelManager, secured.AccessibleManagerMixin, deletable.PurgableManagerMixin ):
"""
DatasetAssociation/DatasetInstances are intended to be working
proxies to a Dataset, associated with either a library or a
user/history (HistoryDatasetAssociation).
"""
# DA's were meant to be proxies - but were never fully implemented as them
# Instead, a dataset association HAS a dataset but contains metadata specific to a library (lda) or user (hda)
model_class = model.DatasetInstance
def __init__( self, app ):
super( DatasetAssociationManager, self ).__init__( app )
self.dataset_manager = DatasetManager( app )
def is_accessible( self, trans, dataset_assoc, user ):
"""
Is this DA accessible to `user`?
"""
# defer to the dataset
return self.dataset_manager.is_accessible( trans, dataset_assoc.dataset, user )
#def metadata( self, trans, dataset_assoc ):
# """
# Return the metadata collection.
# """
# # get metadata
# pass
#def is_being_used( self, trans, dataset_assoc ):
# """
# """
# #TODO: check history_associations, library_associations
# pass
class DatasetAssociationSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ):
def add_serializers( self ):
super( DatasetAssociationSerializer, self ).add_serializers()
deletable.PurgableSerializerMixin.add_serializers( self )
class DatasetAssociationDeserializer( base.ModelDeserializer, deletable.PurgableDeserializerMixin ):
def add_deserializers( self ):
super( DatasetAssociationDeserializer, self ).add_deserializers()
deletable.PurgableDeserializerMixin.add_deserializers( self )
+117
View File
@@ -0,0 +1,117 @@
"""
Many models in Galaxy are not meant to be removed from the database but only
marked as deleted. These models have the boolean attribute 'deleted'.
Other models are deletable and also may be purged. Most often these are
models have some backing/supporting resources that can be removed as well
(e.g. Datasets have data files on a drive). Purging these models removes
the supporting resources as well. These models also have the boolean
attribute 'purged'.
"""
# ---- Deletable and Purgable models
class DeletableManagerMixin( object ):
"""
A mixin/interface for a model that is deletable (i.e. has a 'deleted' attr).
Many resources in Galaxy can be marked as deleted - meaning (in most cases)
that they are no longer needed, should not be displayed, or may be actually
removed by an admin/script.
"""
def delete( self, trans, item, flush=True, **kwargs ):
"""
Mark as deleted and return.
"""
return self._session_setattr( item, 'deleted', True, flush=flush )
def undelete( self, trans, item, flush=True, **kwargs ):
"""
Mark as not deleted and return.
"""
return self._session_setattr( item, 'deleted', False, flush=flush )
class DeletableSerializerMixin( object ):
def add_serializers( self ):
self.serializable_keyset.add( 'deleted' )
# TODO: these are of questionable value if we don't want to enable users to delete/purge via update
class DeletableDeserializerMixin( object ):
def add_deserializers( self ):
self.deserializers[ 'deleted' ] = self.deserialize_deleted
def deserialize_deleted( self, trans, item, key, val ):
"""
Delete or undelete `item` based on `val` then return `item.deleted`.
"""
new_deleted = self.validate.bool( key, val )
if new_deleted == item.deleted:
return item.deleted
# TODO:?? flush=False?
if new_deleted:
self.manager.delete( trans, item, flush=False )
else:
self.manager.undelete( trans, item, flush=False )
return item.deleted
class DeletableFiltersMixin( object ):
def _add_parsers( self ):
self.orm_filter_parsers.update({
'deleted' : { 'op': ( 'eq' ), 'val': self.parse_bool }
})
class PurgableManagerMixin( DeletableManagerMixin ):
"""
A manager interface/mixin for a resource that allows deleting and purging where
purging is often removal of some additional, non-db resource (e.g. a dataset's
file).
"""
def purge( self, trans, item, flush=True, **kwargs ):
"""
Mark as purged and return.
Override this in subclasses to do the additional resource removal.
"""
return self._session_setattr( item, 'purged', True, flush=flush )
class PurgableSerializerMixin( DeletableSerializerMixin ):
def add_serializers( self ):
DeletableSerializerMixin.add_serializers( self )
self.serializable_keyset.add( 'purged' )
class PurgableDeserializerMixin( DeletableDeserializerMixin ):
def add_deserializers( self ):
DeletableDeserializerMixin.add_deserializers( self )
self.deserializers[ 'purged' ] = self.deserialize_purged
def deserialize_purged( self, trans, item, key, val ):
"""
If `val` is True, purge `item` and return `item.purged`.
"""
new_purged = self.validate.bool( key, val )
if new_purged == item.purged:
return item.purged
if new_purged:
self.manager.purge( trans, item, flush=False )
return self.purged
class PurgableFiltersMixin( DeletableFiltersMixin ):
def _add_parsers( self ):
DeletableFiltersMixin._add_parsers( self )
self.orm_filter_parsers.update({
'purged' : { 'op': ( 'eq' ), 'val': self.parse_bool }
})
+512 -159
View File
@@ -5,187 +5,196 @@ HistoryDatasetAssociations (HDAs) are datasets contained or created in a
history.
"""
import os
import gettext
from galaxy import model
from galaxy import exceptions
from galaxy.managers import base as manager_base
from galaxy.managers import histories as history_manager
import galaxy.web
from galaxy import datatypes
import galaxy.datatypes.metadata
from galaxy import objectstore
class HDAManager( manager_base.ModelManager ):
from galaxy.managers import datasets
from galaxy.managers import secured
from galaxy.managers import taggable
from galaxy.managers import annotatable
from galaxy.managers import users
import logging
log = logging.getLogger( __name__ )
class HDAManager( datasets.DatasetAssociationManager, secured.OwnableManagerMixin,
taggable.TaggableManagerMixin, annotatable.AnnotatableManagerMixin ):
"""
Interface/service object for interacting with HDAs.
"""
model_class = model.HistoryDatasetAssociation
foreign_key_name = 'history_dataset_association'
def __init__( self ):
tag_assoc = model.HistoryDatasetAssociationTagAssociation
annotation_assoc = model.HistoryDatasetAssociationAnnotationAssociation
#TODO: move what makes sense into DatasetManager
#TODO: which of these are common with LDDAs and can be pushed down into DatasetAssociationManager?
def __init__( self, app ):
"""
Set up and initialize other managers needed by hdas.
"""
self.histories_mgr = history_manager.HistoryManager()
super( HDAManager, self ).__init__( app )
self.user_manager = users.UserManager( app )
def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True ):
# .... security and permissions
def is_accessible( self, trans, hda, user ):
"""
Get an HDA by its unencoded db id, checking ownership (via its history)
or accessibility (via dataset shares/permissions).
Override to allow owners (those that own the associated history).
"""
hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( unencoded_id )
if hda is None:
raise exceptions.ObjectNotFound()
hda = self.secure( trans, hda, check_ownership, check_accessible )
return hda
if self.is_owner( trans, hda, user ):
return True
return super( HDAManager, self ).is_accessible( trans, hda, user )
def secure( self, trans, hda, check_ownership=True, check_accessible=True ):
"""
check ownership (via its history) or accessibility (via dataset
shares/permissions).
"""
# all items are accessible to an admin
if trans.user and trans.user_is_admin():
return hda
if check_ownership:
hda = self.check_ownership( trans, hda )
if check_accessible:
hda = self.check_accessible( trans, hda )
return hda
def can_access_dataset( self, trans, hda ):
"""
Use security agent to see if current user has access to dataset.
"""
current_user_roles = trans.get_current_user_roles()
return trans.app.security_agent.can_access_dataset( current_user_roles, hda.dataset )
#TODO: is_owner, is_accessible
def check_ownership( self, trans, hda ):
def is_owner( self, trans, hda, user ):
"""
Use history to see if current user owns HDA.
"""
if not trans.user:
#if hda.history == trans.history:
# return hda
raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy datasets", type='error' )
if trans.user_is_admin():
return hda
# check for ownership of the containing history and accessibility of the underlying dataset
if( self.histories_mgr.is_owner( trans, hda.history )
and self.can_access_dataset( trans, hda ) ):
return hda
raise exceptions.ItemOwnershipException(
"HistoryDatasetAssociation is not owned by the current user", type='error' )
history = hda.history
#TODO: some dup here with historyManager.is_owner but prevents circ import
if self.user_manager.is_admin( trans, user ):
return True
if self.user_manager.is_anonymous( user ) and history == trans.get_history():
return True
return history.user == user
def check_accessible( self, trans, hda ):
# .... create and copy
def create( self, trans, history=None, dataset=None, flush=True, **kwargs ):
"""
Raise error if HDA is not accessible.
"""
if trans.user and trans.user_is_admin():
return hda
# check for access of the containing history...
self.histories_mgr.check_accessible( trans, hda.history )
# ...then the underlying dataset
if self.can_access_dataset( trans, hda ):
return hda
raise exceptions.ItemAccessibilityException(
"HistoryDatasetAssociation is not accessible to the current user", type='error' )
Create a new hda optionally passing in it's history and dataset.
def err_if_uploading( self, trans, hda ):
..note: to explicitly set hid to `None` you must pass in `hid=None`, otherwise
it will be automatically set.
"""
if not dataset:
kwargs[ 'create_dataset' ] = True
hda = super( HDAManager, self ).create( trans, flush=flush,
history=history, dataset=dataset, sa_session=self.app.model.context, **kwargs )
if history:
set_hid = not ( 'hid' in kwargs )
history.add_dataset( hda )
#TODO:?? some internal sanity check here (or maybe in add_dataset) to make sure hids are not duped?
self.app.model.context.add( hda )
if flush:
self.app.model.context.flush()
return hda
def copy( self, trans, hda, history=None, **kwargs ):
"""
Copy and return the given HDA.
"""
#TODO:?? not using the following as this fn does not set history and COPIES hid (this doesn't seem correct)
#return hda.copy()
copy = model.HistoryDatasetAssociation(
name = hda.name,
info = hda.info,
blurb = hda.blurb,
peek = hda.peek,
tool_version= hda.tool_version,
extension = hda.extension,
dbkey = hda.dbkey,
dataset = hda.dataset,
visible = hda.visible,
deleted = hda.deleted,
parent_id = kwargs.get( 'parent_id', None ),
)
# add_dataset will update the hid to the next avail. in history
if history:
history.add_dataset( copy )
copy.copied_from_history_dataset_association = hda
copy.set_size()
#TODO: update from kwargs?
# Need to set after flushed, as MetadataFiles require dataset.id
self.app.model.context.add( copy )
self.app.model.context.flush()
copy.metadata = hda.metadata
# In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs
if not hda.datatype.copy_safe_peek:
copy.set_peek()
self.app.model.context.flush()
return copy
def copy_ldda( self, trans, history, ldda, **kwargs ):
"""
Copy this HDA as a LDDA and return.
"""
return ldda.to_history_dataset_association( history, add_to_history=True )
#TODO: stub
#def is_a_copy( self, trans, hda ):
# pass
#TODO: stub
#def copied_from( self, trans, hda ):
# pass
#TODO: stub
#def by_user( self, trans, user ):
# pass
def purge( self, trans, hda, flush=True ):
"""
Purge this HDA and the dataset underlying it.
"""
# error here if disallowed - before jobs are stopped
#TODO: poss. move to DatasetAssociationManager
self.dataset_manager.error_unless_dataset_purge_allowed( trans, hda )
super( HDAManager, self ).purge( trans, hda, flush=flush )
# signal to stop the creating job?
if hda.creating_job_associations:
job = hda.creating_job_associations[0].job
job.mark_deleted( self.app.config.track_jobs_in_database )
self.app.job_manager.job_stop_queue.put( job.id )
# more importantly, purge dataset as well
if hda.dataset.user_can_purge:
self.dataset_manager.purge( trans, hda.dataset )
return hda
def error_if_uploading( self, trans, hda ):
"""
Raise error if HDA is still uploading.
"""
if hda.state == trans.model.Dataset.states.UPLOAD:
#TODO: may be better added to an overridden get_accessible
if hda.state == model.Dataset.states.UPLOAD:
raise exceptions.Conflict( "Please wait until this dataset finishes uploading" )
return hda
def get_hda_dict( self, trans, hda ):
"""
Return full details of this HDA in dictionary form.
"""
#precondition: the user's access to this hda has already been checked
#TODO:?? postcondition: all ids are encoded (is this really what we want at this level?)
expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path
hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path )
hda_dict[ 'api_type' ] = "file"
# .... via history
#def by_history_id( self, trans, history_id, filters=None, **kwargs ):
# history_id_filter = self.model_class.history_id == history_id
# filters = self._munge_filters( history_id_filter, filters )
# return self.list( trans, filters=filters, **kwargs )
# Add additional attributes that depend on trans must be added here rather than at the model level.
can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset )
can_access_hda = ( trans.user_is_admin() or can_access_hda )
if not can_access_hda:
return self.get_inaccessible_hda_dict( trans, hda )
hda_dict[ 'accessible' ] = True
#def by_history( self, trans, history, filters=None, **kwargs ):
# return history.datasets
#TODO: I'm unclear as to which access pattern is right
hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, hda.history.user, hda )
#annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) )
# ---- return here if deleted AND purged OR can't access
purged = ( hda.purged or hda.dataset.purged )
if ( hda.deleted and purged ):
#TODO: to_dict should really go AFTER this - only summary data
return trans.security.encode_dict_ids( hda_dict )
if expose_dataset_path:
try:
hda_dict[ 'file_name' ] = hda.file_name
except objectstore.ObjectNotFound:
log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id )
hda_dict[ 'download_url' ] = galaxy.web.url_for( 'history_contents_display',
history_id = trans.security.encode_id( hda.history.id ),
history_content_id = trans.security.encode_id( hda.id ) )
# indeces, assoc. metadata files, etc.
meta_files = []
for meta_type in hda.metadata.spec.keys():
if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ):
meta_files.append( dict( file_type=meta_type ) )
if meta_files:
hda_dict[ 'meta_files' ] = meta_files
# currently, the viz reg is optional - handle on/off
if trans.app.visualizations_registry:
hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda )
else:
hda_dict[ 'visualizations' ] = hda.get_visualizations()
#TODO: it may also be wiser to remove from here and add as API call that loads the visualizations
# when the visualizations button is clicked (instead of preloading/pre-checking)
# ---- return here if deleted
if hda.deleted and not purged:
return trans.security.encode_dict_ids( hda_dict )
return trans.security.encode_dict_ids( hda_dict )
def get_inaccessible_hda_dict( self, trans, hda ):
"""
Return truncated serialization of HDA when inaccessible to user.
"""
return trans.security.encode_dict_ids({
'id' : hda.id,
'history_id': hda.history.id,
'hid' : hda.hid,
'name' : hda.name,
'state' : hda.state,
'deleted' : hda.deleted,
'visible' : hda.visible,
'accessible': False
})
def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ):
"""
Return truncated serialization of HDA when error raised getting
details.
"""
return trans.security.encode_dict_ids({
'id' : hda.id if hda else id,
'history_id': hda.history.id if hda else history_id,
'hid' : hda.hid if hda else '(unknown)',
'name' : hda.name if hda else '(unknown)',
'error' : error_msg,
'state' : trans.model.Dataset.states.NEW
})
# .... associated
def creating_job( self, trans, hda ):
#TODO: is this needed? Can't you use the hda.creating_job attribute? When is this None?
job = None
for job_output_assoc in hda.creating_job_associations:
job = job_output_assoc.job
break
return job
# .... serialization
def get_display_apps( self, trans, hda ):
"""
Return dictionary containing new-style display app urls.
@@ -198,7 +207,7 @@ class HDAManager( manager_base.ModelManager ):
app_links.append({
'target': link_app.url.get( 'target_frame', '_blank' ),
'href' : link_app.get_display_url( hda, trans ),
'text' : gettext( link_app.name )
'text' : gettext.gettext( link_app.name )
})
if app_links:
display_apps.append( dict( label=display_app.name, links=app_links ) )
@@ -210,12 +219,12 @@ class HDAManager( manager_base.ModelManager ):
Return dictionary containing old-style display app urls.
"""
display_apps = []
if not trans.app.config.enable_old_display_applications:
if not self.app.config.enable_old_display_applications:
return display_apps
for display_app in hda.datatype.get_display_types():
target_frame, display_links = hda.datatype.get_display_links( hda,
display_app, trans.app, trans.request.base )
display_app, self.app, trans.request.base )
if len( display_links ) > 0:
display_label = hda.datatype.get_display_label( display_app )
@@ -225,17 +234,361 @@ class HDAManager( manager_base.ModelManager ):
app_links.append({
'target': target_frame,
'href' : display_link,
'text' : gettext( display_name )
'text' : gettext.gettext( display_name )
})
if app_links:
display_apps.append( dict( label=display_label, links=app_links ) )
return display_apps
def get_visualizations( self, trans, hda ):
"""
Return a list of dictionaries with links to visualization pages
for those visualizations that apply to this hda.
"""
# use older system if registry is off in the config
if not self.app.visualizations_registry:
return hda.get_visualizations()
return self.app.visualizations_registry.get_visualizations( trans, hda )
# =============================================================================
class HistorySerializer( manager_base.ModelSerializer ):
#TODO: to data provider or Text datatype directly
def text_data( self, dataset, preview=True ):
"""
Get data from text file, truncating if necessary.
"""
truncated = False
dataset_data = None
if os.path.exists( dataset.file_name ):
if isinstance( dataset.datatype, datatypes.data.Text ):
max_peek_size = 1000000 # 1 MB
if preview and os.stat( dataset.file_name ).st_size > max_peek_size:
dataset_data = open( dataset.file_name ).read( max_peek_size )
truncated = True
else:
dataset_data = open( dataset.file_name ).read( max_peek_size )
truncated = False
else:
# For now, cannot get data from non-text datasets.
dataset_data = None
return truncated, dataset_data
# this is a weird syntax and return val
def data_conversion_status( self, trans, hda ):
"""
Returns a message if dataset is not ready to be used in visualization.
"""
if not hda:
return hda.conversion_messages.NO_DATA
if hda.state == model.Job.states.ERROR:
return hda.conversion_messages.ERROR
if hda.state != model.Job.states.OK:
return hda.conversion_messages.PENDING
return None
def stop_creating_job( self, hda ):
"""
Stops an HDA's creating job if all the job's other outputs are deleted.
"""
RUNNING_STATES = (
self.app.model.Job.states.QUEUED,
self.app.model.Job.states.RUNNING,
self.app.model.Job.states.NEW
)
if hda.parent_id is None and len( hda.creating_job_associations ) > 0:
# Mark associated job for deletion
job = hda.creating_job_associations[0].job
if job.state in RUNNING_STATES:
# Are *all* of the job's other output datasets deleted?
if job.check_if_output_datasets_deleted():
job.mark_deleted( self.app.config.track_jobs_in_database )
self.app.job_manager.job_stop_queue.put( job.id )
return True
return False
class HDASerializer( datasets.DatasetAssociationSerializer,
taggable.TaggableSerializerMixin, annotatable.AnnotatableSerializerMixin ):
#TODO: inherit from datasets.DatasetAssociationSerializer
#TODO: move what makes sense into DatasetSerializer
def __init__( self, app ):
super( HDASerializer, self ).__init__( app )
self.hda_manager = HDAManager( app )
self.default_view = 'summary'
self.add_view( 'summary', [
'id', 'name',
'history_id', 'hid',
# why include if model_class is there?
'history_content_type',
'dataset_id',
'state', 'extension',
'deleted', 'purged', 'visible', 'resubmitted',
'type', 'url'
])
self.add_view( 'detailed', [
'model_class',
'history_id', 'hid',
# why include if model_class is there?
'hda_ldda',
#TODO: accessible needs to go away
'accessible',
# remapped
'genome_build', 'misc_info', 'misc_blurb',
'file_ext', 'file_size',
'create_time', 'update_time',
'metadata', 'meta_files', 'data_type',
'peek',
#TODO: why is this named uuid!? The hda doesn't have a uuid - it's the underlying dataset's uuid!
'uuid',
# should be:
#'dataset_uuid',
'file_path',
'display_apps',
'display_types',
'visualizations',
#'url',
'download_url',
'annotation', 'tags',
'api_type'
], include_keys_from='summary' )
self.add_view( 'extended', [
'tool_version', 'parent_id', 'designation',
], include_keys_from='detailed' )
# keyset returned to create show a dataset where the owner has no access
self.add_view( 'inaccessible', [
'id', 'name', 'history_id', 'hid', 'history_content_type',
'state', 'deleted', 'visible'
])
def add_serializers( self ):
super( HDASerializer, self ).add_serializers()
taggable.TaggableSerializerMixin.add_serializers( self )
annotatable.AnnotatableSerializerMixin.add_serializers( self )
self.serializers.update({
'model_class' : lambda *a: 'HistoryDatasetAssociation',
#TODO: accessible needs to go away
'accessible' : lambda *a: True,
'id' : self.serialize_id,
'history_id' : self.serialize_id,
'history_content_type': lambda *a: 'dataset',
'dataset_id' : self.serialize_id,
'hda_ldda' : lambda *a: 'hda',
# remapped
'info' : lambda t, i, k: i.info.strip() if isinstance( i.info, basestring ) else i.info,
'misc_info' : lambda t, i, k: self.serializers[ 'info' ]( t, i, k ),
'misc_blurb' : lambda t, i, k: i.blurb,
'genome_build' : lambda t, i, k: i.dbkey,
'file_ext' : lambda t, i, k: i.extension,
'file_size' : lambda t, i, k: self.serializers[ 'size' ]( t, i, k ),
'create_time' : self.serialize_date,
'update_time' : self.serialize_date,
'copied_from_history_dataset_association_id' : self.serialize_id,
'copied_from_library_dataset_dataset_association_id': self.serialize_id,
'resubmitted' : lambda t, i, k: i._state == t.app.model.Dataset.states.RESUBMITTED,
'meta_files' : self.serialize_meta_files,
'file_path' : self.serialize_file_path,
'size' : lambda t, i, k: int( i.get_size() ),
'nice_size' : lambda t, i, k: i.get_size( nice_size=True ),
'data_type' : lambda t, i, k: i.datatype.__class__.__module__ + '.' + i.datatype.__class__.__name__,
'peek' : lambda t, i, k: i.display_peek() if i.peek and i.peek != 'no peek' else None,
# currently we send this sub-obj flattened (see serialize and add_flattened_metadata below)
#'metadata' : self.serialize_metadata,
# make it available here under a different key
'metadata_dict' : self.serialize_metadata,
'parent_id' : self.serialize_id,
'annotation' : self.serialize_annotation,
'tags' : self.serialize_tags,
'display_apps' : lambda t, i, k: self.hda_manager.get_display_apps( t, i ),
'display_types' : lambda t, i, k: self.hda_manager.get_old_display_applications( t, i ),
'visualizations': lambda t, i, k: self.hda_manager.get_visualizations( t, i ),
'dataset_uuid' : lambda t, i, k: str( i.dataset.uuid ) if i.dataset.uuid else None,
'uuid' : lambda t, i, k: self.serializers[ 'dataset_uuid' ]( t, i, k ),
#'url' : url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset" ),
#TODO: this intermittently causes a routes.GenerationException - temp use the legacy route to prevent this
# see also: https://trello.com/c/5d6j4X5y
# see also: https://sentry.galaxyproject.org/galaxy/galaxy-main/group/20769/events/9352883/
'url' : lambda t, i, k: self.url_for( 'history_content',
history_id=t.security.encode_id( i.history_id ), id=t.security.encode_id( i.id ) ),
'urls' : self.serialize_urls,
'download_url' : lambda t, i, k: self.url_for( 'history_contents_display',
history_id=t.security.encode_id( i.history.id ),
history_content_id=t.security.encode_id( i.id ) ),
'api_type' : lambda *a: 'file',
'type' : lambda *a: 'file'
})
def serialize( self, trans, hda, keys ):
"""
Override to add metadata as flattened keys on the serialized HDA.
"""
# if 'metadata' isn't removed from keys here serialize will retrieve the un-serializable MetadataCollection
#TODO: remove these when metadata is sub-object
KEYS_HANDLED_SEPARATELY = ( 'metadata', )
left_to_handle = self.pluck_from_list( keys, KEYS_HANDLED_SEPARATELY )
serialized = super( HDASerializer, self ).serialize( trans, hda, keys )
if 'metadata' in left_to_handle:
# we currently add metadata directly to the dict instead of as a sub-object
metadata = self.prefixed_metadata( trans, hda )
serialized.update( metadata )
return serialized
#TODO: this is more util/gen. use
def pluck_from_list( self, l, elems ):
"""
Removes found elems from list l and returns list of found elems if found.
"""
found = []
for elem in elems:
try:
index = l.index( elem )
found.append( l.pop( index ) )
except ValueError, val_err:
pass
return found
def prefixed_metadata( self, trans, hda ):
"""
Adds (a prefixed version of) the hda metadata to the dict, prefixing each key
with 'metadata_'.
"""
metadata = self.serialize_metadata( trans, hda, 'metadata' )
#TODO: this is factored out for removal - metadata should be a sub-object instead:
# i.e. 'metadata' : self.serialize_metadata,
prefixed = {}
for key, val in metadata.items():
prefixed_key = 'metadata_' + key
prefixed[ prefixed_key ] = val
return prefixed
def serialize_metadata( self, trans, hda, key, excluded=None ):
"""
Cycle through metadata and return as dictionary.
"""
# dbkey is a repeat actually (metadata_dbkey == genome_build)
#excluded = [ 'dbkey' ] if excluded is None else excluded
excluded = [] if excluded is None else excluded
metadata = {}
for name, spec in hda.metadata.spec.items():
if name in excluded:
continue
val = hda.metadata.get( name )
#NOTE: no files
if isinstance( val, model.MetadataFile ):
# only when explicitly set: fetching filepaths can be expensive
if not self.app.config.expose_dataset_path:
continue
val = val.file_name
#TODO:? possibly split this off?
# If no value for metadata, look in datatype for metadata.
elif val is None and hasattr( hda.datatype, name ):
val = getattr( hda.datatype, name )
metadata[ name ] = val
return metadata
# add to serialize_metadata above
def serialize_meta_files( self, trans, hda, key ):
"""
Cycle through meta files and return them as a list of dictionaries.
"""
meta_files = []
for meta_type in hda.metadata.spec.keys():
if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ):
meta_files.append( dict( file_type=meta_type ) )
return meta_files
#def file_info #TODO
#TODO: and to dataset instead (passing through object store)
def serialize_file_path( self, trans, hda, key ):
"""
Return the `file_name` of the HDA if the config exposes it, None otherwise.
"""
#TODO: allow admin
if self.app.config.expose_dataset_path:
try:
return hda.file_name
except objectstore.ObjectNotFound:
log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id )
return None
def serialize_urls( self, trans, hda, key ):
"""
Return web controller urls useful for this HDA.
"""
url_for = self.url_for
encoded_id = self.security.encode_id( hda.id )
urls = {
'purge' : url_for( controller='dataset', action='purge_async', dataset_id=encoded_id ),
'display' : url_for( controller='dataset', action='display', dataset_id=encoded_id, preview=True ),
'edit' : url_for( controller='dataset', action='edit', dataset_id=encoded_id ),
'download' : url_for( controller='dataset', action='display',
dataset_id=encoded_id, to_ext=hda.extension ),
'report_error' : url_for( controller='dataset', action='errors', id=encoded_id ),
'rerun' : url_for( controller='tool_runner', action='rerun', id=encoded_id ),
'show_params' : url_for( controller='dataset', action='show_params', dataset_id=encoded_id ),
'visualization' : url_for( controller='visualization', action='index',
id=encoded_id, model='HistoryDatasetAssociation' ),
'meta_download' : url_for( controller='dataset', action='get_metadata_file',
hda_id=encoded_id, metadata_name='' ),
}
return urls
class HDADeserializer( datasets.DatasetAssociationDeserializer,
taggable.TaggableDeserializerMixin, annotatable.AnnotatableDeserializerMixin ):
"""
Interface/service object for serializing HDAs into dictionaries.
Interface/service object for validating and deserializing dictionaries into histories.
"""
pass
model_manager_class = HDAManager
def __init__( self, app ):
super( HDADeserializer, self ).__init__( app )
self.hda_manager = self.manager
def add_deserializers( self ):
super( HDADeserializer, self ).add_deserializers()
taggable.TaggableDeserializerMixin.add_deserializers( self )
annotatable.AnnotatableDeserializerMixin.add_deserializers( self )
self.deserializers.update({
'name' : self.deserialize_basestring,
'visible' : self.deserialize_bool,
# remapped
'genome_build' : lambda t, i, k, v: self.deserialize_genome_build( t, i, 'dbkey', v ),
'misc_info' : lambda t, i, k, v: self.deserialize_basestring( t, i, 'info', v ),
#TODO: mixin: deletable
'deleted' : self.deserialize_bool,
# sharable
'published' : self.deserialize_bool,
'importable' : self.deserialize_bool,
})
self.deserializable_keyset.update( self.deserializers.keys() )
+301 -165
View File
@@ -5,179 +5,155 @@ Histories are containers for datasets or dataset collections
created (or copied) by users over the course of an analysis.
"""
from galaxy import exceptions
from galaxy.model import orm
from galaxy.managers import base as manager_base
import galaxy.managers.collections_util
import galaxy.web
from galaxy import model
from galaxy.managers import base
from galaxy.managers import sharable
from galaxy.managers import deletable
from galaxy.managers import hdas
from galaxy.managers.collections_util import dictify_dataset_collection_instance
import logging
log = logging.getLogger( __name__ )
# =============================================================================
class HistoryManager( manager_base.ModelManager ):
"""
Interface/service object for interacting with HDAs.
"""
class HistoryManager( sharable.SharableModelManager, deletable.PurgableManagerMixin ):
#TODO: all the following would be more useful if passed the user instead of defaulting to trans.user
def __init__( self, *args, **kwargs ):
super( HistoryManager, self ).__init__( *args, **kwargs )
model_class = model.History
foreign_key_name = 'history'
user_share_model = model.HistoryUserShareAssociation
def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True, deleted=None ):
tag_assoc = model.HistoryTagAssociation
annotation_assoc = model.HistoryAnnotationAssociation
rating_assoc = model.HistoryRatingAssociation
#TODO: incorporate imp/exp (or alias to)
def __init__( self, app, *args, **kwargs ):
super( HistoryManager, self ).__init__( app, *args, **kwargs )
self.hda_manager = hdas.HDAManager( app )
def copy( self, trans, history, user, **kwargs ):
"""
Get a History from the database by id, verifying ownership.
Copy and return the given `history`.
"""
# this is a replacement for UsesHistoryMixin because mixins are a bad soln/structure
history = trans.sa_session.query( trans.app.model.History ).get( unencoded_id )
if history is None:
raise exceptions.ObjectNotFound()
if deleted is True and not history.deleted:
raise exceptions.ItemDeletionException( 'History "%s" is not deleted' % ( history.name ), type="error" )
elif deleted is False and history.deleted:
raise exceptions.ItemDeletionException( 'History "%s" is deleted' % ( history.name ), type="error" )
return history.copy( target_user=user, **kwargs )
history = self.secure( trans, history, check_ownership, check_accessible )
return history
def by_user( self, trans, user=None, include_deleted=False, only_deleted=False ):
# .... sharable
# overriding to handle anonymous users' current histories in both cases
def by_user( self, trans, user, **kwargs ):
"""
Get all the histories for a given user (defaulting to `trans.user`)
ordered by update time and filtered on whether they've been deleted.
Get all the histories for a given user (allowing anon users' theirs)
ordered by update time.
"""
# handle default and/or anonymous user (which still may not have a history yet)
user = user or trans.user
if not user:
current_history = trans.get_history()
if self.user_manager.is_anonymous( user ):
current_history = self.get_current( trans )
return [ current_history ] if current_history else []
history_model = trans.model.History
query = ( trans.sa_session.query( history_model )
.filter( history_model.user == user )
.order_by( orm.desc( history_model.table.c.update_time ) ) )
if only_deleted:
query = query.filter( history_model.deleted == True )
elif not include_deleted:
query = query.filter( history_model.deleted == False )
return query.all()
return super( HistoryManager, self ).by_user( trans, user, **kwargs )
def secure( self, trans, history, check_ownership=True, check_accessible=True ):
"""
Checks if (a) user owns item or (b) item is accessible to user.
"""
# all items are accessible to an admin
if trans.user and trans.user_is_admin():
return history
if check_ownership:
history = self.check_ownership( trans, history )
if check_accessible:
history = self.check_accessible( trans, history )
return history
def is_current( self, trans, history ):
"""
True if the given history is the user's current history.
Returns False if the session has no current history.
"""
if trans.history is None:
return False
return trans.history == history
def is_owner( self, trans, history ):
def is_owner( self, trans, history, user ):
"""
True if the current user is the owner of the given history.
"""
# anon users are only allowed to view their current history
if not trans.user:
return self.is_current( trans, history )
return trans.user == history.user
def check_ownership( self, trans, history ):
"""
Raises error if the current user is not the owner of the history.
"""
if trans.user and trans.user_is_admin():
return history
if not trans.user and not self.is_current( trans, history ):
raise exceptions.AuthenticationRequired( "Must be logged in to manage Galaxy histories", type='error' )
if self.is_owner( trans, history ):
return history
raise exceptions.ItemOwnershipException( "History is not owned by the current user", type='error' )
def is_accessible( self, trans, history ):
"""
True if the user can access (read) the current history.
"""
# admin always have access
if trans.user and trans.user_is_admin():
if self.user_manager.is_anonymous( user ) and history == self.get_current( trans ):
return True
# owner has implicit access
if self.is_owner( trans, history ):
return True
# importable and shared histories are always accessible
if history.importable:
return True
if trans.user in history.users_shared_with_dot_users:
return True
return False
return super( HistoryManager, self ).is_owner( trans, history, user )
def check_accessible( self, trans, history ):
#TODO: possibly to sharable
def most_recent( self, trans, user, filters=None, **kwargs ):
"""
Raises error if the current user can't access the history.
"""
if self.is_accessible( trans, history ):
return history
raise exceptions.ItemAccessibilityException( "History is not accessible to the current user", type='error' )
Return the most recently update history for the user.
#TODO: bleh...
If user is anonymous, return the current history. If the user is anonymous
and the current history is deleted, return None.
"""
#TODO: trans
if not user:
current_history = self.get_current( trans )
return None if ( not current_history or current_history.deleted ) else current_history
desc_update_time = self.model_class.table.c.update_time
filters = self._munge_filters( filters, self.model_class.user_id == user.id )
#TODO: normalize this return value
return self.query( trans, filters=filters, order_by=desc_update_time, limit=1, **kwargs ).first()
# .... purgable
def purge( self, trans, history, flush=True, **kwargs ):
"""
Purge this history and all HDAs, Collections, and Datasets inside this history.
"""
self.hda_manager.dataset_manager.error_unless_dataset_purge_allowed( trans, history )
# First purge all the datasets
for hda in history.datasets:
if not hda.purged:
self.hda_manager.purge( trans, hda, flush=True )
# Now mark the history as purged
super( HistoryManager, self ).purge( trans, history, flush=flush, **kwargs )
# .... current
def get_current( self, trans ):
"""
Return the current history.
"""
#TODO: trans
return trans.get_history()
def set_current( self, trans, history ):
"""
Set the current history.
"""
#TODO: trans
trans.set_history( history )
return history
def set_current_by_id( self, trans, history_id ):
"""
Set the current history by an id.
"""
return self.set_current( trans, self.by_id( trans, history_id ) )
# .... serialization
#TODO: move to serializer (i.e. history with contents attr)
def _get_history_data( self, trans, history ):
"""
Returns a dictionary containing ``history`` and ``contents``, serialized
history and an array of serialized history contents respectively.
"""
# import here prevents problems related to circular dependecy between histories and hdas managers.
import galaxy.managers.hdas
hda_mgr = galaxy.managers.hdas.HDAManager()
collection_dictifier = galaxy.managers.collections_util.dictify_dataset_collection_instance
#TODO: instantiate here? really?
history_serializer = HistorySerializer( self.app )
hda_serializer = hdas.HDASerializer( self.app )
history_dictionary = {}
contents_dictionaries = []
try:
history_dictionary = history_serializer.serialize_to_view( trans, history, view='detailed' )
#for content in history.contents_iter( **contents_kwds ):
for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ):
hda_dict = {}
contents_dict = {}
if isinstance( content, trans.app.model.HistoryDatasetAssociation ):
try:
hda_dict = hda_mgr.get_hda_dict( trans, content )
except Exception, exc:
# don't fail entire list if hda err's, record and move on
log.exception( 'Error bootstrapping hda: %s', exc )
hda_dict = hda_mgr.get_hda_dict_with_error( trans, content, str( exc ) )
if isinstance( content, model.HistoryDatasetAssociation ):
contents_dict = hda_serializer.serialize_to_view( trans, content, view='detailed' )
elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ):
elif isinstance( content, model.HistoryDatasetCollectionAssociation ):
try:
service = trans.app.dataset_collections_service
service = self.app.dataset_collections_service
dataset_collection_instance = service.get_dataset_collection_instance(
trans=trans,
instance_type='history',
id=trans.security.encode_id( content.id ),
id=self.app.security.encode_id( content.id ),
)
hda_dict = collection_dictifier( dataset_collection_instance,
security=trans.security, parent=dataset_collection_instance.history, view="element" )
contents_dict = dictify_dataset_collection_instance( dataset_collection_instance,
security=self.app.security, parent=dataset_collection_instance.history, view="element" )
except Exception, exc:
log.exception( "Error in history API at listing dataset collection: %s", exc )
#TODO: return some dict with the error
contents_dictionaries.append( hda_dict )
# re-use the hdas above to get the history data...
history_dictionary = self.get_history_dict( trans, history, contents_dictionaries=contents_dictionaries )
contents_dictionaries.append( contents_dict )
except Exception, exc:
user_id = str( trans.user.id ) if trans.user else '(anonymous)'
@@ -191,53 +167,213 @@ class HistoryManager( manager_base.ModelManager ):
'contents' : contents_dictionaries
}
def get_history_dict( self, trans, history, contents_dictionaries=None ):
# remove this
def get_state_counts( self, trans, history, exclude_deleted=True, exclude_hidden=False ):
"""
Returns history data in the form of a dictionary.
Return a dictionary keyed to possible dataset states and valued with the number
of datasets in this history that have those states.
"""
#TODO: to serializer
history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id })
history_dict[ 'user_id' ] = None
if history.user_id:
history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id )
#TODO: the default flags above may not make a lot of sense (T,T?)
state_counts = {}
for state in model.Dataset.states.values():
state_counts[ state ] = 0
history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True )
history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, history.user, history )
if not history_dict[ 'annotation' ]:
history_dict[ 'annotation' ] = ''
#TODO:?? collections and coll. states?
for hda in history.datasets:
if exclude_deleted and hda.deleted:
continue
if exclude_hidden and not hda.visible:
continue
state_counts[ hda.state ] = state_counts[ hda.state ] + 1
return state_counts
#TODO: item_slug url
if history_dict[ 'importable' ] and history_dict[ 'slug' ]:
username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] ))
history_dict[ 'username_and_slug' ] = username_and_slug
# remove this
def get_state_ids( self, trans, history ):
"""
Return a dictionary keyed to possible dataset states and valued with lists
containing the ids of each HDA in that state.
"""
state_ids = {}
for state in model.Dataset.states.values():
state_ids[ state ] = []
#TODO: re-add
#hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history )
##TODO remove the following in v2
#( state_counts, state_ids ) = self._get_hda_state_summaries( trans, hda_summaries )
#history_dict[ 'state_details' ] = state_counts
#history_dict[ 'state_ids' ] = state_ids
#history_dict[ 'state' ] = self._get_history_state_from_hdas( trans, history, state_counts )
#TODO:?? collections and coll. states?
for hda in history.datasets:
#TODO: do not encode ids at this layer
encoded_id = self.app.security.encode_id( hda.id )
state_ids[ hda.state ].append( encoded_id )
return state_ids
return history_dict
#TODO: remove this (is state used/useful?)
def get_history_state( self, trans, history ):
"""
Returns the history state based on the states of the HDAs it contains.
"""
states = model.Dataset.states
def most_recent( self, trans, user=None, deleted=False ):
user = user or trans.user
if not user:
return None if trans.history.deleted else trans.history
# (default to ERROR)
state = states.ERROR
#TODO: dup with by_user - should return query from there and call first and not all
history_model = trans.model.History
query = ( trans.sa_session.query( history_model )
.filter( history_model.user == user )
.order_by( orm.desc( history_model.table.c.update_time ) ) )
if not deleted:
query = query.filter( history_model.deleted == False )
return query.first()
#TODO: history_state and state_counts are classically calc'd at the same time
# so this is rel. ineff. - if we keep this...
hda_state_counts = self.get_state_counts( trans, history, exclude_deleted=False )
num_hdas = sum( hda_state_counts.values() )
if num_hdas == 0:
state = states.NEW
# =============================================================================
class HistorySerializer( manager_base.ModelSerializer ):
else:
if( ( hda_state_counts[ states.RUNNING ] > 0 )
or ( hda_state_counts[ states.SETTING_METADATA ] > 0 )
or ( hda_state_counts[ states.UPLOAD ] > 0 ) ):
state = states.RUNNING
#TODO: this method may be more useful if we *also* polled the histories jobs here too
elif hda_state_counts[ states.QUEUED ] > 0:
state = states.QUEUED
elif( ( hda_state_counts[ states.ERROR ] > 0 )
or ( hda_state_counts[ states.FAILED_METADATA ] > 0 ) ):
state = states.ERROR
elif hda_state_counts[ states.OK ] == num_hdas:
state = states.OK
return state
class HistorySerializer( sharable.SharableModelSerializer, deletable.PurgableSerializerMixin ):
"""
Interface/service object for serializing histories into dictionaries.
"""
pass
SINGLE_CHAR_ABBR = 'h'
def __init__( self, app ):
super( HistorySerializer, self ).__init__( app )
self.history_manager = HistoryManager( app )
self.hda_manager = hdas.HDAManager( app )
self.hda_serializer = hdas.HDASerializer( app )
self.default_view = 'summary'
self.add_view( 'summary', [
'id',
'model_class',
'name',
'deleted',
#'purged',
#'count'
'url',
#TODO: why these?
'published',
'annotation',
'tags',
])
self.add_view( 'detailed', [
'contents_url',
#'hdas',
'empty',
'size', 'nice_size',
'user_id',
'create_time', 'update_time',
'importable', 'slug', 'username_and_slug',
'genome_build',
#TODO: remove the next three - instead getting the same info from the 'hdas' list
'state',
'state_details',
'state_ids',
# in the Historys' case, each of these views includes the keys from the previous
], include_keys_from='summary' )
#assumes: outgoing to json.dumps and sanitized
def add_serializers( self ):
super( HistorySerializer, self ).add_serializers()
deletable.PurgableSerializerMixin.add_serializers( self )
self.serializers.update({
'model_class' : lambda *a: 'History',
'id' : self.serialize_id,
'create_time' : self.serialize_date,
'update_time' : self.serialize_date,
'size' : lambda t, i, k: int( i.get_disk_size() ),
'nice_size' : lambda t, i, k: i.get_disk_size( nice_size=True ),
'state' : lambda t, i, k: self.history_manager.get_history_state( t, i ),
'url' : lambda t, i, k: self.url_for( 'history', id=t.security.encode_id( i.id ) ),
'contents_url' : lambda t, i, k:
self.url_for( 'history_contents', history_id=t.security.encode_id( i.id ) ),
'empty' : lambda t, i, k: len( i.datasets ) <= 0,
'count' : lambda trans, item, key: len( item.datasets ),
'hdas' : lambda t, i, k: [ t.security.encode_id( hda.id ) for hda in i.datasets ],
'state_details' : lambda t, i, k: self.history_manager.get_state_counts( t, i ),
'state_ids' : lambda t, i, k: self.history_manager.get_state_ids( t, i ),
'contents' : self.serialize_contents
})
def serialize_contents( self, trans, history, key ):
contents_dictionaries = []
for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ):
contents_dict = {}
if isinstance( content, model.HistoryDatasetAssociation ):
contents_dict = self.hda_serializer.serialize_to_view( trans, content, view='detailed' )
elif isinstance( content, model.HistoryDatasetCollectionAssociation ):
contents_dict = self.serialize_collection( trans, content )
contents_dictionaries.append( contents_dict )
return contents_dictionaries
def serialize_collection( self, trans, collection ):
service = self.app.dataset_collections_service
dataset_collection_instance = service.get_dataset_collection_instance(
trans=trans,
instance_type='history',
id=self.security.encode_id( collection.id ),
)
return dictify_dataset_collection_instance( dataset_collection_instance,
security=self.app.security, parent=dataset_collection_instance.history, view="element" )
class HistoryDeserializer( sharable.SharableModelDeserializer, deletable.PurgableDeserializerMixin ):
"""
Interface/service object for validating and deserializing dictionaries into histories.
"""
model_manager_class = HistoryManager
def __init__( self, app ):
super( HistoryDeserializer, self ).__init__( app )
self.history_manager = self.manager
def add_deserializers( self ):
super( HistoryDeserializer, self ).add_deserializers()
deletable.PurgableDeserializerMixin.add_deserializers( self )
self.deserializers.update({
'name' : self.deserialize_basestring,
'genome_build' : self.deserialize_genome_build,
})
class HistoryFilters( sharable.SharableModelFilters, deletable.PurgableFiltersMixin ):
model_class = model.History
def _add_parsers( self ):
super( HistoryFilters, self )._add_parsers()
deletable.PurgableFiltersMixin._add_parsers( self )
self.orm_filter_parsers.update({
# history specific
'name' : { 'op': ( 'eq', 'contains', 'like' ) },
'genome_build' : { 'op': ( 'eq', 'contains', 'like' ) },
})
#TODO: I'm not entirely convinced this (or tags) are a good idea for filters since they involve a/the user
self.fn_filter_parsers.update({
#TODO: add this in annotatable mixin
'annotation' : { 'op': { 'has' : self.filter_annotation_contains, } },
#TODO: add this in taggable mixin
'tag' : {
'op': {
'eq' : self.filter_has_tag,
'has': self.filter_has_partial_tag,
}
}
})
+8 -5
View File
@@ -1,16 +1,19 @@
from galaxy.managers import base as manager_base
class LDDAManager( manager_base.ModelManager ):
""" A fairly sparse manager for LDDAs.
class LDDAManager( object ):
"""
A fairly sparse manager for LDDAs.
"""
def __init__( self ):
def __init__( self, app ):
"""
Set up and initialize other managers needed by lddas.
"""
pass
def get( self, trans, id, check_accessible=True ):
return manager_base.get_object( trans, id, 'LibraryDatasetDatasetAssociation',
check_ownership=False, check_accessible=check_accessible )
return manager_base.get_object( trans, id,
'LibraryDatasetDatasetAssociation',
check_ownership=False,
check_accessible=check_accessible )
+74
View File
@@ -0,0 +1,74 @@
"""
Manager and Serializers for Pages.
Pages are markup created and saved by users that can contain Galaxy objects
(such as datasets) and are often used to describe or present an analysis
from within Galaxy.
"""
from galaxy import model
from galaxy.managers import sharable
import logging
log = logging.getLogger( __name__ )
class PageManager( sharable.SharableModelManager ):
"""
"""
model_class = model.Page
foreign_key_name = 'page'
user_share_model = model.PageUserShareAssociation
tag_assoc = model.PageTagAssociation
annotation_assoc = model.PageAnnotationAssociation
rating_assoc = model.PageRatingAssociation
def __init__( self, app, *args, **kwargs ):
"""
"""
super( PageManager, self ).__init__( app, *args, **kwargs )
def copy( self, trans, page, user, **kwargs ):
"""
"""
pass
class PageSerializer( sharable.SharableModelSerializer ):
"""
Interface/service object for serializing pages into dictionaries.
"""
SINGLE_CHAR_ABBR = 'p'
def __init__( self, app ):
super( PageSerializer, self ).__init__( app )
self.page_manager = PageManager( app )
self.default_view = 'summary'
self.add_view( 'summary', [] )
self.add_view( 'detailed', [] )
def add_serializers( self ):
super( PageSerializer, self ).add_serializers()
self.serializers.update({
})
class PageDeserializer( sharable.SharableModelDeserializer ):
"""
Interface/service object for validating and deserializing dictionaries
into pages.
"""
model_manager_class = PageManager
def __init__( self, app ):
super( PageDeserializer, self ).__init__( app )
self.page_manager = self.manager
def add_deserializers( self ):
super( PageDeserializer, self ).add_deserializers()
self.deserializers.update({
})
self.deserializable_keyset.update( self.deserializers.keys() )
+48
View File
@@ -0,0 +1,48 @@
"""
Mixins for Ratable model managers and serializers.
"""
from galaxy import exceptions
from galaxy import model
import logging
log = logging.getLogger( __name__ )
#TODO: stub
class RatableManagerMixin( object ):
#: class of RatingAssociation (e.g. HistoryRatingAssociation)
rating_assoc = None
#TODO: most of this seems to be covered by item_attrs.UsesItemRatings
#def by_user( self, trans, user, **kwargs ):
# pass
class RatableSerializerMixin( object ):
def add_serializers( self ):
self.serializers[ 'user_rating' ] = self.serialize_user_rating
self.serializers[ 'community_rating' ] = self.serialize_community_rating
def serialize_user_rating( self, trans, item, key ):
"""
"""
pass
def serialize_community_rating( self, trans, item, key ):
"""
"""
pass
class RatableDeserializerMixin( object ):
def add_deserializers( self ):
pass
#self.deserializers[ 'user_rating' ] = self.deserialize_rating
#def deserialize_rating( self, trans, item, key, val ):
# val = self.validate.int_range( key, val, 0, 5 )
# return self.set_rating...( trans, item, val, user=trans.user )
+122
View File
@@ -0,0 +1,122 @@
"""
Accessible models can be read and copied but not modified or deleted.
Owned models can be modified and deleted.
"""
from galaxy import exceptions
from galaxy.managers import base
class AccessibleManagerMixin( object ):
"""
A security interface to check if a User can read/view an item's.
This can also be thought of as 'read but not modify' privileges.
"""
# don't want to override by_id since consumers will also want to fetch w/o any security checks
def is_accessible( self, trans, item, user ):
"""
Return True if the item accessible to user.
"""
# override in subclasses
raise exceptions.NotImplemented( "Abstract interface Method" )
def get_accessible( self, trans, id, user, **kwargs ):
"""
Return the item with the given id if it's accessible to user,
otherwise raise an error.
:raises exceptions.ItemAccessibilityException:
"""
item = self.by_id( trans, id )
return self.error_unless_accessible( trans, item, user )
def error_unless_accessible( self, trans, item, user ):
"""
Raise an error if the item is NOT accessible to user, otherwise return the item.
:raises exceptions.ItemAccessibilityException:
"""
if self.is_accessible( trans, item, user ):
return item
raise exceptions.ItemAccessibilityException( "%s is not accessible by user" % ( self.model_class.__name__ ) )
# TODO:?? are these even useful?
def list_accessible( self, trans, user, **kwargs ):
"""
Return a list of items accessible to the user, raising an error if ANY
are inaccessible.
:raises exceptions.ItemAccessibilityException:
"""
raise exceptions.NotImplemented( "Abstract interface Method" )
# NOTE: this will be a large, inefficient list if filters are not passed in kwargs
# items = ModelManager.list( self, trans, **kwargs )
# return [ self.error_unless_accessible( trans, item, user ) for item in items ]
def filter_accessible( self, trans, user, **kwargs ):
"""
Return a list of items accessible to the user.
"""
raise exceptions.NotImplemented( "Abstract interface Method" )
# NOTE: this will be a large, inefficient list if filters are not passed in kwargs
# items = ModelManager.list( self, trans, **kwargs )
# return filter( lambda item: self.is_accessible( trans, item, user ), items )
class OwnableManagerMixin( object ):
"""
A security interface to check if a User is an item's owner.
Some resources are associated with the User that created or imported them
and these Users can be considered the models' owner.
This can also be thought of as write/edit privileges.
"""
def is_owner( self, trans, item, user ):
"""
Return True if user owns the item.
"""
# override in subclasses
raise exceptions.NotImplemented( "Abstract interface Method" )
def get_owned( self, trans, id, user, **kwargs ):
"""
Return the item with the given id if owned by the user,
otherwise raise an error.
:raises exceptions.ItemOwnershipException:
"""
item = self.by_id( trans, id )
return self.error_unless_owner( trans, item, user )
def error_unless_owner( self, trans, item, user ):
"""
Raise an error if the item is NOT owned by user, otherwise return the item.
:raises exceptions.ItemAccessibilityException:
"""
if self.is_owner( trans, item, user ):
return item
raise exceptions.ItemOwnershipException( "%s is not owned by user" % ( self.model_class.__name__ ) )
def list_owned( self, trans, user, **kwargs ):
"""
Return a list of items owned by the user, raising an error if ANY
are not.
:raises exceptions.ItemAccessibilityException:
"""
raise exceptions.NotImplemented( "Abstract interface Method" )
# just alias to by_user (easier/same thing)
#return self.by_user( trans, user, **kwargs )
def filter_owned( self, trans, user, **kwargs ):
"""
Return a list of items owned by the user.
"""
# just alias to list_owned
return self.list_owned( trans, user, **kwargs )
+404
View File
@@ -0,0 +1,404 @@
"""
Superclass Manager and Serializers for Sharable objects.
A sharable Galaxy object:
has an owner/creator User
is sharable with other, specific Users
is importable (copyable) by users that have access
has a slug which can be used as a link to view the resource
can be published effectively making it available to all other Users
can be rated
"""
import re
from galaxy import exceptions
from galaxy.managers import base
from galaxy.managers import secured
from galaxy.managers import taggable
from galaxy.managers import annotatable
from galaxy.managers import ratable
from galaxy.managers import users
import logging
log = logging.getLogger( __name__ )
class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secured.AccessibleManagerMixin,
taggable.TaggableManagerMixin, annotatable.AnnotatableManagerMixin, ratable.RatableManagerMixin ):
# e.g. histories, pages, stored workflows, visualizations
# base.DeleteableModelMixin? (all four are deletable)
#: the model used for UserShareAssociations with this model
user_share_model = None
#: the single character abbreviation used in username_and_slug: e.g. 'h' for histories: u/user/h/slug
SINGLE_CHAR_ABBR = None
def __init__( self, app ):
super( SharableModelManager, self ).__init__( app )
# user manager is needed to check access/ownership/admin
self.user_manager = users.UserManager( app )
# .... has a user
def by_user( self, trans, user, filters=None, **kwargs ):
"""
Return list for all items (of model_class type) associated with the given
`user`.
"""
user_filter = self.model_class.user_id == user.id
filters=self._munge_filters( user_filter, filters )
return self.list( trans, filters=filters, **kwargs )
# .... owned model interface
def is_owner( self, trans, item, user ):
"""
Return true if this sharable belongs to `user` (or `user` is an admin).
"""
if self.user_manager.is_anonymous( user ):
# note: this needs to be overridden in the case of anon users and session.current_history
return False
# ... effectively a good fit to have this here, but not semantically
if self.user_manager.is_admin( trans, user ):
return True
return item.user == user
# .... accessible interface
def is_accessible( self, trans, item, user ):
"""
If the item is importable, is owned by `user`, or (the valid) `user`
is in 'users shared with' list for the item: return True.
"""
if item.importable:
return True
# note: owners always have access - checking for accessible implicitly checks for ownership
if self.is_owner( trans, item, user ):
return True
if self.user_manager.is_anonymous( user ):
return False
if user in item.users_shared_with_dot_users:
return True
return False
# .... importable
def make_importable( self, trans, item, flush=True ):
"""
Makes item accessible--viewable and importable--and sets item's slug.
Does not flush/commit changes, however. Item must have name, user,
importable, and slug attributes.
"""
self.create_unique_slug( trans, item, flush=False )
return self._session_setattr( item, 'importable', True, flush=flush )
def make_non_importable( self, trans, item, flush=True ):
"""
Makes item accessible--viewable and importable--and sets item's slug.
Does not flush/commit changes, however. Item must have name, user,
importable, and slug attributes.
"""
# item must be unpublished if non-importable
if item.published:
self.unpublish( trans, item, flush=False )
return self._session_setattr( item, 'importable', False, flush=flush )
# .... published
def publish( self, trans, item, flush=True ):
"""
Set both the importable and published flags on `item` to True.
"""
# item must be importable to be published
if not item.importable:
self.make_importable( trans, item, flush=False )
return self._session_setattr( item, 'published', True, flush=flush )
def unpublish( self, trans, item, flush=True ):
"""
Set the published flag on `item` to False.
"""
return self._session_setattr( item, 'published', False, flush=flush )
#def _query_published( self, trans, filters=None, **kwargs ):
# """
# """
# published_filter = self.model_class.published == True
# filters = self._munge_filters( published_filter, filters )
# return self.list( trans, filters=filters, **kwargs )
#
#def list_published( self, trans, **kwargs ):
# """
# """
# query = self._query_published( trans, user, **kwargs )
# return self.list( trans, query=query, **kwargs )
# .... user sharing
# sharing is often done via a 3rd table btwn a User and an item -> a <Item>UserShareAssociation
def get_share_assocs( self, trans, item, user=None ):
"""
Get the UserShareAssociations for the `item`.
Optionally send in `user` to test for a single match.
"""
query = self.query_associated( trans, self.user_share_model, item )
if user is not None:
query = query.filter_by( user=user )
return query.all()
def share_with( self, trans, item, user, flush=True ):
"""
Get or create a share for the given user (or users if `user` is a list).
"""
# allow user to be a list and call recursivly
if isinstance( user, list ):
return map( lambda user: self.share_with( trans, item, user, flush=False ), user )
# get or create
existing = self.get_share_assocs( trans, item, user=user )
if existing:
return existing.pop( 0 )
return self._create_user_share_assoc( trans, item, user, flush=flush )
def _create_user_share_assoc( self, trans, item, user, flush=True ):
"""
Create a share for the given user.
"""
user_share_assoc = self.user_share_model()
self.app.model.context.add( user_share_assoc )
self.associate( trans, user_share_assoc, item )
user_share_assoc.user = user
# ensure an item slug so shared users can access
if not item.slug:
self.create_unique_slug( trans, item )
if flush:
self.app.model.context.flush()
return user_share_assoc
def unshare_with( self, trans, item, user, flush=True ):
"""
Delete a user share (or list of shares) from the database.
"""
if isinstance( user, list ):
return map( lambda user: self.unshare_with( trans, item, user, flush=False ), user )
# Look for and delete sharing relation for user.
user_share_assoc = self.get_share_assocs( trans, item, user=user )[0]
self.app.model.context.delete( user_share_assoc )
if flush:
self.app.model.context.flush()
return user_share_assoc
#def _query_shared_with( self, trans, user, filters=None, **kwargs ):
##TODO:
# """
# """
# pass
#def list_shared_with( self, trans, user, **kwargs ):
# """
# """
# query = self._query_shared_with( trans, user, **kwargs )
# return self.list( trans, query=query, **kwargs )
# .... slugs
# slugs are human readable strings often used to link to sharable resources (replacing ids)
#TODO: as validator, deserializer, etc. (maybe another object entirely?)
def set_slug( self, trans, item, new_slug, flush=True ):
"""
Validate and set the new slug for `item`.
"""
# precondition: has been validated
if not self.is_valid_slug( new_slug ):
raise exceptions.RequestParameterInvalidException( "Invalid slug", slug=new_slug )
# error if slug is already in use
if self._slug_exists( trans, trans.user, new_slug ):
raise exceptions.Conflict( "Slug already exists", slug=new_slug )
item.slug = new_slug
if flush:
self.app.model.context.flush()
return item
def is_valid_slug( self, slug ):
"""
Returns true if `slug` is valid.
"""
VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" )
return VALID_SLUG_RE.match( slug )
def _existing_set_of_slugs( self, trans, user ):
query = ( self.app.model.context.query( self.model_class.slug )
.filter_by( user=user ) )
return set( query.all() )
def _slug_exists( self, trans, user, slug ):
query = ( self.app.model.context.query( self.model_class.slug )
.filter_by( user=user, slug=slug ) )
return query.count() != 0
def _slugify( self, start_with ):
# Replace whitespace with '-'
slug_base = re.sub( "\s+", "-", start_with )
# Remove all non-alphanumeric characters.
slug_base = re.sub( "[^a-zA-Z0-9\-]", "", slug_base )
# Remove trailing '-'.
if slug_base.endswith('-'):
slug_base = slug_base[:-1]
return slug_base
def _default_slug_base( self, trans, item ):
# override in subclasses
if hasattr( item, 'title' ):
return item.title.lower()
return item.name.lower()
def get_unique_slug( self, trans, item ):
"""
Returns a slug that is unique among user's importable items
for item's class.
"""
cur_slug = item.slug
# Setup slug base.
if cur_slug is None or cur_slug == "":
slug_base = self._slugify( self._default_slug_base( trans, item ) )
else:
slug_base = cur_slug
# Using slug base, find a slug that is not taken. If slug is taken,
# add integer to end.
new_slug = slug_base
count = 1
while ( self.app.model.context.query( item.__class__ )
.filter_by( user=item.user, slug=new_slug, importable=True )
.count() != 0 ):
# Slug taken; choose a new slug based on count. This approach can
# handle numerous items with the same name gracefully.
new_slug = '%s-%i' % ( slug_base, count )
count += 1
return new_slug
def create_unique_slug( self, trans, item, flush=True ):
"""
Set a new, unique slug on the item.
"""
item.slug = self.get_unique_slug( trans, item )
self.app.model.context.add( item )
if flush:
self.app.model.context.flush()
return item
#def by_slug( self, trans, user, **kwargs ):
# """
# """
# pass
# .... display
#def display_by_username_and_slug( self, trans, username, slug ):
# """ Display item by username and slug. """
#def set_public_username( self, trans, id, username, **kwargs ):
# """ Set user's public username and delegate to sharing() """
#def sharing( self, trans, id, **kwargs ):
# """ Handle item sharing. """
class SharableModelSerializer( base.ModelSerializer,
taggable.TaggableSerializerMixin, annotatable.AnnotatableSerializerMixin, ratable.RatableSerializerMixin ):
#TODO: stub
SINGLE_CHAR_ABBR = None
def add_serializers( self ):
super( SharableModelSerializer, self ).add_serializers()
taggable.TaggableSerializerMixin.add_serializers( self )
annotatable.AnnotatableSerializerMixin.add_serializers( self )
ratable.RatableSerializerMixin.add_serializers( self )
self.serializers.update({
'user_id' : self.serialize_id,
'username_and_slug' : self.serialize_username_and_slug
})
self.serializable_keyset.update([
'importable', 'published', 'slug'
])
def serialize_username_and_slug( self, trans, item, key ):
if not ( item.user and item.slug and self.SINGLE_CHAR_ABBR ):
return None
#TODO: self.url_for
return ( '/' ).join(( 'u', item.user.username, self.SINGLE_CHAR_ABBR, item.slug ) )
#def published_url( self, trans, item, key ):
# """
# """
# url = url_for(controller='history', action="display_by_username_and_slug",
# username=item.user.username, slug=item.slug )
# return url
# the only ones that needs any fns:
# user/user_id
# user_shares?
# username_and_slug?
class SharableModelDeserializer( base.ModelDeserializer,
taggable.TaggableDeserializerMixin, annotatable.AnnotatableDeserializerMixin, ratable.RatableDeserializerMixin ):
def add_deserializers( self ):
super( SharableModelDeserializer, self ).add_deserializers()
taggable.TaggableDeserializerMixin.add_deserializers( self )
annotatable.AnnotatableDeserializerMixin.add_deserializers( self )
ratable.RatableDeserializerMixin.add_deserializers( self )
self.deserializers.update({
'published' : self.deserialize_published,
'importable' : self.deserialize_importable,
})
def deserialize_published( self, trans, item, key, val ):
"""
"""
val = self.validate.bool( key, val )
if item.published == val:
return val
if val:
self.manager.publish( trans, item, flush=False )
else:
self.manager.unpublish( trans, item, flush=False )
return item.published
def deserialize_importable( self, trans, item, key, val ):
"""
"""
val = self.validate.bool( key, val )
if item.importable == val:
return val
if val:
self.manager.make_importable( trans, item, flush=False )
else:
self.manager.make_non_importable( trans, item, flush=False )
return item.published
#def deserialize_slug( self, trans, item, val ):
# """
# """
# #TODO: call manager.set_slug
# pass
#def deserialize_user_shares():
class SharableModelFilters( base.ModelFilterParser ):
def _add_parsers( self ):
super( SharableModelFilters, self )._add_parsers()
self.orm_filter_parsers.update({
'importable' : { 'op': ( 'eq' ), 'val': self.parse_bool },
'published' : { 'op': ( 'eq' ), 'val': self.parse_bool },
'slug' : { 'op': ( 'eq', 'contains', 'like' ) },
# chose by user should prob. only be available for admin? (most often we'll only need trans.user)
#'user' : { 'op': ( 'eq' ), 'val': self.parse_id_list },
})
+66
View File
@@ -0,0 +1,66 @@
"""
Mixins for Taggable model managers and serializers.
"""
from galaxy import exceptions
from galaxy import model
import logging
log = logging.getLogger( __name__ )
class TaggableManagerMixin( object ):
#: class of TagAssociation (e.g. HistoryTagAssociation)
tag_assoc = None
#TODO: most of this can be done by delegating to the TagManager?
#def by_user( self, trans, user, **kwargs ):
# pass
class TaggableSerializerMixin( object ):
def add_serializers( self ):
self.serializers[ 'tags' ] = self.serialize_tags
def serialize_tags( self, trans, item, key ):
"""
Return tags as a list of strings.
"""
if not hasattr( item, 'tags' ):
return None
tags_str_list = []
for tag in item.tags:
tag_str = tag.user_tname
if tag.value is not None:
tag_str += ":" + tag.user_value
tags_str_list.append( tag_str )
return tags_str_list
class TaggableDeserializerMixin( object ):
def add_deserializers( self ):
self.deserializers[ 'tags' ] = self.deserialize_tags
def deserialize_tags( self, trans, item, key, val ):
"""
Make sure `val` is a valid list of tag strings and assign them.
Note: this will erase any previous tags.
"""
new_tags_list = self.validate.basestring_list( key, val )
#TODO: have to assume trans.user here...
#TODO: trans
user = trans.user
#TODO: duped from tags manager - de-dupe when moved to taggable mixin
tag_handler = self.app.tag_handler
tag_handler.delete_item_tags( user, item )
new_tags_str = ','.join( new_tags_list )
tag_handler.apply_item_tags( user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) )
#TODO:!! does the creation of new_tags_list mean there are now more and more unused tag rows in the db?
return item.tags
+307 -10
View File
@@ -1,20 +1,317 @@
import logging
import re
from sqlalchemy.sql import select
from sqlalchemy.sql.expression import func
log = logging.getLogger( __name__ )
class TagsManager( object ):
""" Manages CRUD operations related to tagging objects.
# Item-specific information needed to perform tagging.
class ItemTagAssocInfo( object ):
def __init__( self, item_class, tag_assoc_class, item_id_col ):
self.item_class = item_class
self.tag_assoc_class = tag_assoc_class
self.item_id_col = item_id_col
class TagManager( object ):
"""
Manages CRUD operations related to tagging objects.
"""
def __init__( self, app ):
self.app = app
self.tag_handler = app.tag_handler
# Minimum tag length.
self.min_tag_len = 2
# Maximum tag length.
self.max_tag_len = 255
# Tag separator.
self.tag_separators = ',;'
# Hierarchy separator.
self.hierarchy_separator = '.'
# Key-value separator.
self.key_value_separators = "=:"
# Initialize with known classes - add to this in subclasses.
self.item_tag_assoc_info = {}
def set_tags_from_list( self, trans, item, new_tags_list, user=None ):
#precondition: item is already security checked against user
#precondition: incoming tags is a list of sanitized/formatted strings
user = user or trans.user
def set_tags_from_list( self, user, item, new_tags_list ):
# precondition: item is already security checked against user
# precondition: incoming tags is a list of sanitized/formatted strings
self.tag_handler.delete_item_tags( trans, user, item )
self.delete_item_tags( user, item )
new_tags_str = ','.join( new_tags_list )
self.tag_handler.apply_item_tags( trans, user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) )
trans.sa_session.flush()
self.apply_item_tags( user, item, unicode( new_tags_str.encode( 'utf-8' ), 'utf-8' ) )
self.app.model.context.flush()
return item.tags
def get_tag_assoc_class( self, item_class ):
"""Returns tag association class for item class."""
return self.item_tag_assoc_info[item_class.__name__].tag_assoc_class
def get_id_col_in_item_tag_assoc_table( self, item_class ):
"""Returns item id column in class' item-tag association table."""
return self.item_tag_assoc_info[item_class.__name__].item_id_col
def get_community_tags( self, item=None, limit=None ):
"""Returns community tags for an item."""
# Get item-tag association class.
item_class = item.__class__
item_tag_assoc_class = self.get_tag_assoc_class( item_class )
if not item_tag_assoc_class:
return []
# Build select statement.
cols_to_select = [ item_tag_assoc_class.table.c.tag_id, func.count( '*' ) ]
from_obj = item_tag_assoc_class.table.join( item_class.table ).join( self.app.model.Tag.table )
where_clause = ( self.get_id_col_in_item_tag_assoc_table( item_class ) == item.id )
order_by = [ func.count( "*" ).desc() ]
group_by = item_tag_assoc_class.table.c.tag_id
# Do query and get result set.
query = select( columns=cols_to_select,
from_obj=from_obj,
whereclause=where_clause,
group_by=group_by,
order_by=order_by,
limit=limit )
result_set = self.app.model.context.execute( query )
# Return community tags.
community_tags = []
for row in result_set:
tag_id = row[0]
community_tags.append( self.get_tag_by_id( tag_id ) )
return community_tags
def get_tool_tags( self ):
result_set = self.app.model.context.execute( select( columns=[ self.app.model.ToolTagAssociation.table.c.tag_id ],
from_obj=self.app.model.ToolTagAssociation.table ).distinct() )
tags = []
for row in result_set:
tag_id = row[0]
tags.append( self.get_tag_by_id( tag_id ) )
return tags
def remove_item_tag( self, user, item, tag_name ):
"""Remove a tag from an item."""
# Get item tag association.
item_tag_assoc = self._get_item_tag_assoc( user, item, tag_name )
# Remove association.
if item_tag_assoc:
# Delete association.
self.app.model.context.delete( item_tag_assoc )
item.tags.remove( item_tag_assoc )
return True
return False
def delete_item_tags( self, user, item ):
"""Delete tags from an item."""
# Delete item-tag associations.
for tag in item.tags:
self.app.model.context.delete( tag )
# Delete tags from item.
del item.tags[:]
def item_has_tag( self, user, item, tag ):
"""Returns true if item is has a given tag."""
# Get tag name.
if isinstance( tag, basestring ):
tag_name = tag
elif isinstance( tag, self.app.model.Tag ):
tag_name = tag.name
# Check for an item-tag association to see if item has a given tag.
item_tag_assoc = self._get_item_tag_assoc( user, item, tag_name )
if item_tag_assoc:
return True
return False
def apply_item_tag( self, user, item, name, value=None ):
# Use lowercase name for searching/creating tag.
lc_name = name.lower()
# Get or create item-tag association.
item_tag_assoc = self._get_item_tag_assoc( user, item, lc_name )
if not item_tag_assoc:
# Create item-tag association.
# Create tag; if None, skip the tag (and log error).
tag = self._get_or_create_tag( lc_name )
if not tag:
log.warn( "Failed to create tag with name %s" % lc_name )
return
# Create tag association based on item class.
item_tag_assoc_class = self.get_tag_assoc_class( item.__class__ )
item_tag_assoc = item_tag_assoc_class()
# Add tag to association.
item.tags.append( item_tag_assoc )
item_tag_assoc.tag = tag
item_tag_assoc.user = user
# Apply attributes to item-tag association. Strip whitespace from user name and tag.
lc_value = None
if value:
lc_value = value.lower()
item_tag_assoc.user_tname = name
item_tag_assoc.user_value = value
item_tag_assoc.value = lc_value
return item_tag_assoc
def apply_item_tags( self, user, item, tags_str ):
"""Apply tags to an item."""
# Parse tags.
parsed_tags = self.parse_tags( tags_str )
# Apply each tag.
for name, value in parsed_tags.items():
self.apply_item_tag( user, item, name, value )
def get_tags_str( self, tags ):
"""Build a string from an item's tags."""
# Return empty string if there are no tags.
if not tags:
return ""
# Create string of tags.
tags_str_list = list()
for tag in tags:
tag_str = tag.user_tname
if tag.value is not None:
tag_str += ":" + tag.user_value
tags_str_list.append( tag_str )
return ", ".join( tags_str_list )
def get_tag_by_id( self, tag_id ):
"""Get a Tag object from a tag id."""
return self.app.model.context.query( self.app.model.Tag ).filter_by( id=tag_id ).first()
def get_tag_by_name( self, tag_name ):
"""Get a Tag object from a tag name (string)."""
if tag_name:
return self.app.model.context.query( self.app.model.Tag ).filter_by( name=tag_name.lower() ).first()
return None
def _create_tag( self, tag_str ):
"""Create a Tag object from a tag string."""
tag_hierarchy = tag_str.split( self.hierarchy_separator )
tag_prefix = ""
parent_tag = None
for sub_tag in tag_hierarchy:
# Get or create subtag.
tag_name = tag_prefix + self._scrub_tag_name( sub_tag )
tag = self.app.model.context.query( self.app.model.Tag ).filter_by( name=tag_name).first()
if not tag:
tag = self.app.model.Tag( type=0, name=tag_name )
# Set tag parent.
tag.parent = parent_tag
# Update parent and tag prefix.
parent_tag = tag
tag_prefix = tag.name + self.hierarchy_separator
return tag
def _get_or_create_tag( self, tag_str ):
"""Get or create a Tag object from a tag string."""
# Scrub tag; if tag is None after being scrubbed, return None.
scrubbed_tag_str = self._scrub_tag_name( tag_str )
if not scrubbed_tag_str:
return None
# Get item tag.
tag = self.get_tag_by_name( scrubbed_tag_str )
# Create tag if necessary.
if tag is None:
tag = self._create_tag( scrubbed_tag_str )
return tag
def _get_item_tag_assoc( self, user, item, tag_name ):
"""
Return ItemTagAssociation object for a user, item, and tag string; returns None if there is
no such association.
"""
scrubbed_tag_name = self._scrub_tag_name( tag_name )
for item_tag_assoc in item.tags:
if ( item_tag_assoc.user == user ) and ( item_tag_assoc.user_tname == scrubbed_tag_name ):
return item_tag_assoc
return None
def parse_tags( self, tag_str ):
"""
Returns a list of raw (tag-name, value) pairs derived from a string; method scrubs tag names and values as well.
Return value is a dictionary where tag-names are keys.
"""
# Gracefully handle None.
if not tag_str:
return dict()
# Split tags based on separators.
reg_exp = re.compile( '[' + self.tag_separators + ']' )
raw_tags = reg_exp.split( tag_str )
# Extract name-value pairs.
name_value_pairs = dict()
for raw_tag in raw_tags:
nv_pair = self._get_name_value_pair( raw_tag )
scrubbed_name = self._scrub_tag_name( nv_pair[0] )
scrubbed_value = self._scrub_tag_value( nv_pair[1] )
name_value_pairs[scrubbed_name] = scrubbed_value
return name_value_pairs
def _scrub_tag_value( self, value ):
"""Scrub a tag value."""
# Gracefully handle None:
if not value:
return None
# Remove whitespace from value.
reg_exp = re.compile( '\s' )
scrubbed_value = re.sub( reg_exp, "", value )
return scrubbed_value
def _scrub_tag_name( self, name ):
"""Scrub a tag name."""
# Gracefully handle None:
if not name:
return None
# Remove whitespace from name.
reg_exp = re.compile( '\s' )
scrubbed_name = re.sub( reg_exp, "", name )
# Ignore starting ':' char.
if scrubbed_name.startswith( self.hierarchy_separator ):
scrubbed_name = scrubbed_name[1:]
# If name is too short or too long, return None.
if len( scrubbed_name ) < self.min_tag_len or len( scrubbed_name ) > self.max_tag_len:
return None
return scrubbed_name
def _scrub_tag_name_list( self, tag_name_list ):
"""Scrub a tag name list."""
scrubbed_tag_list = list()
for tag in tag_name_list:
scrubbed_tag_list.append( self._scrub_tag_name( tag ) )
return scrubbed_tag_list
def _get_name_value_pair( self, tag_str ):
"""Get name, value pair from a tag string."""
# Use regular expression to parse name, value.
reg_exp = re.compile( "[" + self.key_value_separators + "]" )
name_value_pair = reg_exp.split( tag_str )
# Add empty slot if tag does not have value.
if len( name_value_pair ) < 2:
name_value_pair.append( None )
return name_value_pair
class GalaxyTagManager( TagManager ):
def __init__( self, app ):
from galaxy import model
TagManager.__init__( self, app )
self.item_tag_assoc_info["History"] = ItemTagAssocInfo( model.History,
model.HistoryTagAssociation,
model.HistoryTagAssociation.table.c.history_id )
self.item_tag_assoc_info["HistoryDatasetAssociation"] = \
ItemTagAssocInfo( model.HistoryDatasetAssociation,
model.HistoryDatasetAssociationTagAssociation,
model.HistoryDatasetAssociationTagAssociation.table.c.history_dataset_association_id )
self.item_tag_assoc_info["Page"] = ItemTagAssocInfo( model.Page,
model.PageTagAssociation,
model.PageTagAssociation.table.c.page_id )
self.item_tag_assoc_info["StoredWorkflow"] = ItemTagAssocInfo( model.StoredWorkflow,
model.StoredWorkflowTagAssociation,
model.StoredWorkflowTagAssociation.table.c.stored_workflow_id )
self.item_tag_assoc_info["Visualization"] = ItemTagAssocInfo( model.Visualization,
model.VisualizationTagAssociation,
model.VisualizationTagAssociation.table.c.visualization_id )
class CommunityTagManager( TagManager):
def __init__( self, app ):
TagManager.__init__( self, app )
+231
View File
@@ -0,0 +1,231 @@
"""
Manager and Serializer for Users.
"""
import pkg_resources
pkg_resources.require( "SQLAlchemy >= 0.4" )
import sqlalchemy
from galaxy import model
from galaxy import exceptions
from galaxy.managers import base
from galaxy.managers import api_keys
import logging
log = logging.getLogger( __name__ )
class UserManager( base.ModelManager ):
model_class = model.User
foreign_key_name = 'user'
#TODO: there is quite a bit of functionality around the user (authentication, permissions, quotas, groups/roles)
# most of which it may be unneccessary to have here
#TODO: incorp BaseAPIController.validate_in_users_and_groups
#TODO: incorp CreatesUsersMixin
#TODO: incorp CreatesApiKeysMixin
#TODO: incorporate security/validate_user_input.py
#TODO: incorporate UsesFormDefinitionsMixin?
def create( self, trans, **kwargs ):
"""
Create a new user.
"""
#TODO: deserialize and validate here
email = kwargs[ 'email' ]
username = kwargs[ 'username' ]
password = kwargs[ 'password' ]
self._error_on_duplicate_email( trans, email )
user = model.User( email=email, password=password )
user.username = username
if self.app.config.user_activation_on:
user.active = False
else:
# Activation is off, every new user is active by default.
user.active = True
self.app.model.context.add( user )
try:
self.app.model.context.flush()
#TODO:?? flush needed for permissions below? If not, make optional
except sqlalchemy.exceptions.IntegrityError, db_err:
raise exceptions.Conflict( db_err.message )
# can throw an sqlalx.IntegrityError if username not unique
self.app.security_agent.create_private_user_role( user )
#TODO: trans
if trans.webapp.name == 'galaxy':
# We set default user permissions, before we log in and set the default history permissions
permissions = self.app.config.new_user_dataset_access_role_default_private
self.app.security_agent.user_set_default_permissions( user, default_access_private=permissions )
return user
def _error_on_duplicate_email( self, trans, email ):
"""
Check for a duplicate email and raise if found.
:raises exceptions.Conflict: if any are found
"""
#TODO: remove this check when unique=True is added to the email column
if self.by_email( trans, email ) is not None:
raise exceptions.Conflict( 'Email must be unique', email=email )
# ---- filters
def by_email( self, trans, email, filters=None, **kwargs ):
"""
Find a user by their email.
"""
filters = self._munge_filters( self.model_class.email == email, filters )
try:
#TODO: use one_or_none
return super( UserManager, self ).one( trans, filters=filters, **kwargs )
except exceptions.ObjectNotFound, not_found:
return None
def by_email_like( self, trans, email_with_wildcards, filters=None, order_by=None, **kwargs ):
"""
Find a user searching with SQL wildcards.
"""
filters = self._munge_filters( self.model_class.email.like( email_with_wildcards ), filters )
order_by = order_by or ( model.User.email, )
return super( UserManager, self ).list( trans, filters=filters, order_by=order_by, **kwargs )
# ---- admin
def is_admin( self, trans, user ):
"""
Return True if this user is an admin.
"""
admin_emails = self._admin_emails( trans )
return user and admin_emails and user.email in admin_emails
def _admin_emails( self, trans ):
"""
Return a list of admin email addresses from the config file.
"""
return [ x.strip() for x in self.app.config.get( "admin_users", "" ).split( "," ) ]
def admins( self, trans, filters=None, **kwargs ):
"""
Return a list of admin Users.
"""
filters = self._munge_filters( self.model_class.email.in_( self._admin_emails( trans ) ), filters )
return super( UserManager, self ).list( trans, filters=filters, **kwargs )
def error_unless_admin( self, trans, user, msg="Administrators only", **kwargs ):
"""
Raise an error if `user` is not an admin.
:raises exceptions.AdminRequiredException: if `user` is not an admin.
"""
# useful in admin only methods
if not self.is_admin( trans, user ):
raise exceptions.AdminRequiredException( msg, **kwargs )
return user
# ---- anonymous
def is_anonymous( self, user ):
"""
Return True if `user` is anonymous.
"""
# define here for single point of change and make more readable
return user is None
def error_if_anonymous( self, trans, user, msg="Log-in required", **kwargs ):
"""
Raise an error if `user` is anonymous.
"""
if user is None:
#TODO: code is correct (403), but should be named AuthenticationRequired (401 and 403 are flipped)
raise exceptions.AuthenticationFailed( msg, **kwargs )
return user
# ---- current
def current_user( self, trans ):
# define here for single point of change and make more readable
return trans.user
# ---- api keys
def create_api_key( self, trans, user ):
"""
Create and return an API key for `user`.
"""
#TODO: seems like this should return the model
return api_keys.ApiKeyManager( self.app ).create_api_key( user )
#TODO: possibly move to ApiKeyManager
def valid_api_key( self, trans, user ):
"""
Return this most recent APIKey for this user or None if none have been created.
"""
query = ( self.app.model.context.query( model.APIKeys )
.filter_by( user=user )
.order_by( sqlalchemy.desc( model.APIKeys.create_time ) ) )
all = query.all()
for a in all:
print a.user.username, a.key, a.create_time
print all
if len( all ):
return all[0]
return None
#return query.first()
#TODO: possibly move to ApiKeyManager
def get_or_create_valid_api_key( self, trans, user ):
"""
Return this most recent APIKey for this user or create one if none have been
created.
"""
existing = self.valid_api_key( trans, user )
if existing:
return existing
return self.create_api_key( self, trans, user )
# ---- roles
def private_role( self, trans, user ):
"""
Return the security agent's private role for `user`.
"""
#TODO: not sure we need to go through sec agent... it's just the first role of type private
return self.app.security_agent.get_private_user_role( user )
class UserSerializer( base.ModelSerializer ):
def __init__( self ):
"""
Convert a User and associated data to a dictionary representation.
"""
super( UserSerializer, self ).__init__()
self.default_view = 'summary'
self.add_view( 'summary', [
'id', 'email', 'username'
])
self.add_view( 'detailed', [
'update_time',
'create_time',
'total_disk_usage',
'nice_total_disk_usage',
'deleted',
'purged',
'active'
], include_keys_from='summary' )
#self.add_view( 'summary', [
# 'preferences',
# # all tags
# 'tags',
# # all annotations
# 'annotations'
#], include_keys_from='detailed' )
def add_serializers( self ):
self.serializers.update({
'id' : self.serialize_id,
'create_time' : self.serialize_date,
'update_time' : self.serialize_date,
})
+76
View File
@@ -0,0 +1,76 @@
"""
Manager and Serializers for Visualizations.
Visualizations are saved configurations/variables used to
reproduce a specific view in a Galaxy visualization.
"""
from galaxy import model
from galaxy.managers import sharable
import logging
log = logging.getLogger( __name__ )
class VisualizationManager( sharable.SharableModelManager ):
"""
Handle operations outside and between visualizations and other models.
"""
# TODO: revisions
model_class = model.Visualization
foreign_key_name = 'visualization'
user_share_model = model.VisualizationUserShareAssociation
tag_assoc = model.VisualizationTagAssociation
annotation_assoc = model.VisualizationAnnotationAssociation
rating_assoc = model.VisualizationRatingAssociation
def __init__( self, app, *args, **kwargs ):
"""
"""
super( VisualizationManager, self ).__init__( app, *args, **kwargs )
#def copy( self, trans, visualization, user, **kwargs ):
# """
# """
# pass
class VisualizationSerializer( sharable.SharableModelSerializer ):
"""
Interface/service object for serializing visualizations into dictionaries.
"""
SINGLE_CHAR_ABBR = 'v'
def __init__( self, app ):
super( VisualizationSerializer, self ).__init__( app )
self.visualizations_manager = VisualizationManager( app )
self.default_view = 'summary'
self.add_view( 'summary', [] )
self.add_view( 'detailed', [] )
def add_serializers( self ):
super( VisualizationSerializer, self ).add_serializers()
self.serializers.update({
})
class VisualizationDeserializer( sharable.SharableModelDeserializer ):
"""
Interface/service object for validating and deserializing
dictionaries into visualizations.
"""
model_manager_class = VisualizationManager
def __init__( self, app ):
super( VisualizationDeserializer, self ).__init__( app )
self.visualization_manager = self.manager
def add_deserializers( self ):
super( VisualizationDeserializer, self ).add_deserializers()
self.deserializers.update({
})
self.deserializable_keyset.update( self.deserializers.keys() )

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