From 95ca08bb990af916f0167dac10df3c5a8d283597 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Sun, 4 May 2008 20:10:57 +0000 Subject: [PATCH] Workflow: Repeat/Conditional now work in workflows, adding/removing/refreshing properly update the 'terminals' for the nodes in the editor. Many other fixes to saving and loading of workflows (error tracking should now be working properly). --- lib/galaxy/tools/__init__.py | 21 +++- lib/galaxy/tools/parameters/grouping.py | 15 ++- lib/galaxy/web/controllers/workflow.py | 60 +++++++--- .../scripts/galaxy.workflow_editor.canvas.js | 113 ++++++++++++------ templates/tool_form.tmpl | 5 +- templates/workflow/editor.mako | 28 +++-- templates/workflow/editor_tool_form.mako | 18 +-- templates/workflow/index.mako | 3 +- 8 files changed, 181 insertions(+), 82 deletions(-) diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 2ca423a30e5..3bd71f4d334 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -689,20 +689,28 @@ class Tool: group_errors = [] group_old_errors = old_errors.get( input.name, None ) any_group_errors = False - # Check any removals before updating state - for i in range( len( group_state ) ): - if key + "_" + str(i) + "_remove" in incoming: + # Check any removals before updating state -- only one + # removal can be performed, others will be ignored + for i, rep_state in enumerate( group_state ): + rep_index = rep_state['__index__'] + if key + "_" + str(rep_index) + "_remove" in incoming: del group_state[i] + if group_old_errors: + del group_old_errors[i] + break # Update state - for i in range( len( group_state ) ): - prefix = "%s_%d|" % ( key, i ) + max_index = -1 + for i, rep_state in enumerate( group_state ): + rep_index = rep_state['__index__'] + max_index = max( max_index, rep_index ) + prefix = "%s_%d|" % ( key, rep_index ) if group_old_errors: rep_old_errors = group_old_errors[i] else: rep_old_errors = {} rep_errors = self.update_state( trans, input.inputs, - group_state[i], + rep_state, incoming, prefix=prefix, context=context, @@ -717,6 +725,7 @@ class Tool: # Check for addition if key + "_add" in incoming: new_state = {} + new_state['__index__'] = max_index + 1 self.fill_in_new_state( trans, input.inputs, new_state, context ) group_state.append( new_state ) if any_group_errors: diff --git a/lib/galaxy/tools/parameters/grouping.py b/lib/galaxy/tools/parameters/grouping.py index fe9041d1ef5..7c0f302e4bd 100644 --- a/lib/galaxy/tools/parameters/grouping.py +++ b/lib/galaxy/tools/parameters/grouping.py @@ -14,7 +14,7 @@ class Group( object ): types (dict, list, tuple, str, unicode, int, long, float, bool, None) """ return value - def value_from_basic( self, value, app ): + def value_from_basic( self, value, app, ignore_errors=False ): """ Convert a basic representation as produced by `value_to_basic` back into the preferred value form. @@ -37,14 +37,21 @@ class Repeat( Group ): rval = [] for d in value: rval_dict = {} + # Propogate __index__ + if '__index__' in d: + rval_dict['__index__'] = d['__index__'] for input in self.inputs.itervalues(): rval_dict[ input.name ] = input.value_to_basic( d[input.name], app ) rval.append( rval_dict ) return rval def value_from_basic( self, value, app, ignore_errors=False ): rval = [] - for d in value: + for i, d in enumerate( value ): rval_dict = {} + # If the special __index__ key is not set, create it (for backward + # compatibility) + rval_dict['__index__'] = d.get( '__index__', i ) + # Restore child inputs for input in self.inputs.itervalues(): rval_dict[ input.name ] = input.value_from_basic( d[input.name], app, ignore_errors ) rval.append( rval_dict ) @@ -82,9 +89,9 @@ class Conditional( Group ): def value_from_basic( self, value, app, ignore_errors=False ): rval = dict() current_case = rval['__current_case__'] = value['__current_case__'] - rval[ self.test_param.name ] = self.test_param.value_from_basic( value[ self.test_param.name ], app ) + rval[ self.test_param.name ] = self.test_param.value_from_basic( value[ self.test_param.name ], app, ignore_errors ) for input in self.cases[current_case].inputs.itervalues(): - rval[ input.name ] = input.value_from_basic( value[ input.name ], app, ignore_errors=False ) + rval[ input.name ] = input.value_from_basic( value[ input.name ], app, ignore_errors ) return rval def visit_inputs( self, prefix, value, callback ): current_case = value['__current_case__'] diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 52a34ebe7d1..f12b9a63710 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -100,6 +100,8 @@ class WorkflowController( BaseController ): rval['tool_errors'] = errors else: rval['tool_errors'] = None + # Updated data_inputs + rval['data_inputs'] = get_data_inputs( tool.inputs, state.inputs ) rval['state'] = state.encode( tool, trans.app ) return rval @@ -116,11 +118,6 @@ class WorkflowController( BaseController ): rval = {} rval['name'] = tool.name rval['tool_id'] = tool.id - data_inputs = [] - for name, input in tool.inputs.iteritems(): - if isinstance( input, DataToolParameter ): - data_inputs.append( dict( name=input.name, label=input.label, extensions=input.extensions ) ) - rval['data_inputs'] = data_inputs data_outputs = [] for name, ( format, metadata_source, parent ) in tool.outputs.iteritems(): data_outputs.append( dict( name=name, extension=format ) ) @@ -129,6 +126,7 @@ class WorkflowController( BaseController ): rval['form_html'] = trans.fill_template( "workflow/editor_tool_form.mako", tool=tool, as_html=as_html, values=state.inputs, errors={} ) rval['tool_state'] = state.encode( tool, trans.app ) + rval['data_inputs'] = get_data_inputs( tool.inputs, state.inputs ) return rval @web.json @@ -158,8 +156,10 @@ class WorkflowController( BaseController ): tool = trans.app.toolbox.tools_by_id[tool_id] # Build a state from the tool_inputs dict state = DefaultToolState() - state.inputs = tool.params_from_strings( step.tool_inputs, trans.app ) + state.inputs = tool.params_from_strings( step.tool_inputs, trans.app, ignore_errors=True ) step_dict['tool_state'] = state.encode( tool, trans.app ) + # Error messages for the tool + step_dict['tool_errors'] = ( step.tool_errors or None ) # Connections input_conn_dict = {} for conn in step.input_connections: @@ -169,11 +169,7 @@ class WorkflowController( BaseController ): # Position step_dict['position'] = step.position # Input and output specs - data_inputs = [] - for name, input in tool.inputs.iteritems(): - if isinstance( input, DataToolParameter ): - data_inputs.append( dict( name=input.name, label=input.label, extensions=input.extensions ) ) - step_dict['data_inputs'] = data_inputs + step_dict['data_inputs'] = get_data_inputs( tool.inputs, state.inputs ) data_outputs = [] for name, ( format, metadata_source, parent ) in tool.outputs.iteritems(): data_outputs.append( dict( name=name, extension=format ) ) @@ -181,7 +177,7 @@ class WorkflowController( BaseController ): # Build the tool form html errors = step.tool_errors step_dict['form_html'] = trans.fill_template( "workflow/editor_tool_form.mako", - tool=tool, as_html=as_html, values=state.inputs, errors={} ) + tool=tool, as_html=as_html, values=state.inputs, errors=( step.tool_errors or {} ) ) step_dict['name'] = tool.name data['steps'][step.order_index] = step_dict return data @@ -329,6 +325,7 @@ class WorkflowController( BaseController ): for other_hid, input_name in associations: if other_hid in hid_to_output_pair: other_job_id, other_name = hid_to_output_pair[ other_hid ] + print "!!!", input_name, other_job_id, other_name, other_hid # Only create association if the associated output dataset # is being included in this workflow if other_job_id in job_id_to_step_index: @@ -550,8 +547,8 @@ def cleanup_param_values( inputs, values ): values[key] = None # HACK: Nested associations are not yet working, but we # still need to clean them up so we can serialize - if not( prefix ): - associations.append( ( tmp.hid, prefix + key ) ) + # if not( prefix ): + associations.append( ( tmp.hid, prefix + key ) ) # Cleanup the other deprecated crap associated with datasets # as well. Worse, for nested datasets all the metadata is # being pushed into the root. FIXME: MUST REMOVE SOON @@ -561,8 +558,9 @@ def cleanup_param_values( inputs, values ): del root_values[k] elif isinstance( input, Repeat ): group_values = values[key] - for i in range( len( group_values ) ): - prefix = "%s_%d|" % ( key, i ) + for i, rep_values in enumerate( group_values ): + rep_index = rep_values['__index__'] + prefix = "%s_%d|" % ( key, rep_index ) cleanup( prefix, input.inputs, group_values[i] ) elif isinstance( input, Conditional ): group_values = values[input.name] @@ -570,4 +568,32 @@ def cleanup_param_values( inputs, values ): prefix = "%s|" % ( key ) cleanup( prefix, input.cases[current_case].inputs, group_values ) cleanup( "", inputs, values ) - return associations \ No newline at end of file + return associations + +def get_data_inputs( inputs, input_values ): + """ + For a set of input definitions and a tool state, find all of the dataset + inputs + """ + data_inputs = [] + def visitor( inputs, input_values, name_prefix, label_prefix ): + for input in inputs.itervalues(): + if isinstance( input, Repeat ): + for i, d in enumerate( input_values[ input.name ] ): + index = d['__index__'] + new_name_prefix = name_prefix + "%s_%d|" % ( input.name, index ) + new_label_prefix = label_prefix + "%s %d > " % ( input.title, i + 1 ) + visitor( input.inputs, d, new_name_prefix, new_label_prefix ) + elif isinstance( input, Conditional ): + values = input_values[ input.name ] + current = values["__current_case__"] + label_prefix = label_prefix + name_prefix = name_prefix + "|" + input.name + visitor( input.cases[current].inputs, values, name_prefix, label_prefix ) + else: + if isinstance( input, DataToolParameter ): + data_inputs.append( dict( name=name_prefix+input.name, label=label_prefix+input.label, extensions=input.extensions ) ) + visitor( inputs, input_values, "", "" ) + return data_inputs + + diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index c1be6366bf5..b1d13ff76e7 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -1,14 +1,19 @@ -function OutputTerminal( element, datatype ) { +function Terminal( element ) { this.element = element; this.connectors = []; - this.datatype = datatype } -$.extend( OutputTerminal.prototype, { +Terminal.prototype = { connect: function ( connector ) { this.connectors.push( connector ); + if ( this.node ) { + this.node.changed(); + } }, disconnect: function ( connector ) { this.connectors.splice( $.inArray( connector, this.connectors ), 1 ); + if ( this.node ) { + this.node.changed(); + } }, redraw: function () { $.each( this.connectors, function( _, c ) { @@ -20,23 +25,24 @@ $.extend( OutputTerminal.prototype, { c.destroy(); }); } -} ) +} + +function OutputTerminal( element, datatype ) { + Terminal.call( this, element ); + this.datatype = datatype; +} + +OutputTerminal.prototype.__proto__ = Terminal.prototype; function InputTerminal( element, datatypes ) { - this.element = element; - this.connectors = []; - this.max_connections = 1; + Terminal.call( this, element ); this.datatypes = datatypes; } -$.extend( InputTerminal.prototype, { - connect: function ( connector ) { - this.connectors.push( connector ); - }, - disconnect: function ( connector ) { - this.connectors.splice( $.inArray( connector, this.connectors ), 1 ); - }, + +InputTerminal.prototype = { + __proto__: Terminal.prototype, can_accept: function ( other ) { - if ( this.connectors.length < this.max_connections ) { + if ( this.connectors.length < 1 ) { for ( t in this.datatypes ) { // FIXME: No idea what to do about this case if ( other.datatype == "input" ) { return true; } @@ -46,18 +52,8 @@ $.extend( InputTerminal.prototype, { } } return false; - }, - redraw: function () { - $.each( this.connectors, function( _, c ) { - c.redraw(); - }) - }, - destroy: function () { - $.each( this.connectors.slice(), function( _, c ) { - c.destroy(); - }); } -} ); +} function Connector( handle1, handle2 ) { this.canvas = null; @@ -147,7 +143,7 @@ $.extend( Node.prototype, { $(elements).each( function() { var terminal = this.terminal = new InputTerminal( this, types ); terminal.node = node; - terminal.name = name + terminal.name = name; $(this).droppable( { tolerance: 'intersect', accept: function( draggable ) { @@ -184,7 +180,9 @@ $.extend( Node.prototype, { $.each( terminal.connectors, function( _, x ) { x.destroy() } ); t.remove(); }))) - .bind( "mouseleave", function() { $(this).fadeOut( "fast", function() { $(this).remove() } ) } ); + .bind( "mouseleave", function() { + $(this).fadeOut( "fast", function() { $(this).remove() } ) + }); // Position it and show t.css( { top: $(this).offset().top - 2, @@ -235,6 +233,10 @@ $.extend( Node.prototype, { node.output_terminals[name] = terminal; }); }, + redraw : function () { + $.each( this.input_terminals, function( _, t ) { t.redraw() } ); + $.each( this.output_terminals, function( _, t ) { t.redraw() } ); + }, destroy : function () { $.each( this.input_terminals, function( k, t ) { t.destroy(); @@ -257,13 +259,20 @@ $.extend( Node.prototype, { var f = this.element; this.form_html = data.form_html; this.tool_state = data.tool_state; + this.tool_errors = data.tool_errors; + if ( this.tool_errors ) { + f.addClass( "tool-node-error" ); + } else { + f.removeClass( "tool-node-error" ); + } var node = this; - b = f.find( ".toolFormBody" ); + var b = f.find( ".toolFormBody" ); b.find( "div" ).remove(); + var ibox = $("
").appendTo( b ); $.each( data.data_inputs, function( i, input ) { t = $("
") node.enable_input_terminal( t, input.name, input.extensions ); - b.append( $("
" + input.name + "
" ).prepend( t ) ); + ibox.append( $("
" + input.label + "
" ).prepend( t ) ); }); if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { b.append( $( "
" ) ); @@ -276,18 +285,45 @@ $.extend( Node.prototype, { workflow.node_changed( this ); }, update_field_data : function( data ) { + var el = $(this.element), + node = this; this.tool_state = data.state; this.form_html = data.form_html; this.tool_errors = data.tool_errors; if ( this.tool_errors ) { - $(this.element).addClass( "tool-node-error" ); + el.addClass( "tool-node-error" ); } else { - $(this.element).removeClass( "tool-node-error" ); - } - if ( workflow.active_node == this ) { - // Reactive with new form_html - workflow.activate_node( this ); + el.removeClass( "tool-node-error" ); } + // Update input rows + var old_body = el.find( "div.inputs" ); + var new_body = $("
"); + old = old_body.find( "div.input-data-row") + $.each( data.data_inputs, function( i, input ) { + var t = $("
"); + node.enable_input_terminal( t, input.name, input.extensions ); + // If already connected save old connection + old_body.find( "div[name=" + input.name + "]" ).each( function() { + $(this).find( ".input-terminal" ).each( function() { + var c = this.terminal.connectors[0]; + if ( c ) { + t[0].terminal.connectors[0] = c; + c.handle2 = t[0].terminal; + } + }); + $(this).remove(); + }); + // Append to new body + new_body.append( $("
" + input.label + "
" ).prepend( t ) ); + }); + old_body.replaceWith( new_body ); + // Cleanup any leftover terminals + old_body.find( "div.input-data-row > .terminal" ).each( function() { + this.terminal.destroy(); + }) + // If active, reactivate with new form_html + this.changed(); + this.redraw(); }, error : function ( text ) { var b = $(this.element).find( ".toolFormBody" ); @@ -296,6 +332,9 @@ $.extend( Node.prototype, { this.form_html = tmp; b.html( tmp ); workflow.node_changed( this ); + }, + changed: function() { + workflow.node_changed( this ); } } ); @@ -311,6 +350,7 @@ $.extend( Workflow.prototype, { this.id_counter++; this.nodes[ node.id ] = node; this.has_changes = true; + node.workflow = this; }, remove_node : function( node ) { if ( this.active_node == node ) { @@ -393,6 +433,7 @@ $.extend( Workflow.prototype, { this.active_node = node; }, node_changed : function ( node ) { + this.has_changes = true; if ( this.active_node == node ) { // Reactive with new form_html this.activate_node( node ); diff --git a/templates/tool_form.tmpl b/templates/tool_form.tmpl index b56fad4ffa0..ace6bd3f1e5 100644 --- a/templates/tool_form.tmpl +++ b/templates/tool_form.tmpl @@ -45,9 +45,10 @@ jQuery( function() { #else #set rep_errors = dict() #end if + #set index = $repeat_state[$i]['__index__']
${input.title} ${i + 1}
- $do_inputs( $input.inputs, $repeat_state[$i], $rep_errors, $prefix + $input.name + "_" + str(i) + "|", $context ) -
+ $do_inputs( $input.inputs, $repeat_state[$i], $rep_errors, $prefix + $input.name + "_" + str($index) + "|", $context ) +
#end for
diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index 5f65f7deab0..579dfd78aaf 100644 --- a/templates/workflow/editor.mako +++ b/templates/workflow/editor.mako @@ -47,12 +47,9 @@ body = $( "" ); } $( ".dialog-box" ).find( ".body" ).html( body ); - // $( "#overlay").show(); if ( ! $(".dialog-box-container").is( ":visible" ) ) { $("#overlay").show(); $(".dialog-box-container").fadeIn() - } else { - // $(".dialog-box").center( "horizontal" ); } }; @@ -81,6 +78,8 @@ success: function( data ) { reset(); workflow.from_simple( data ); + workflow.has_changes = false; + scroll_to_nodes(); hide_modal(); }, beforeSubmit: function( data ) { @@ -218,9 +217,18 @@ workflow.remove_all(); } workflow = new Workflow(); - // Start at the middle of the canvas - // $(window).scrollTop( 2500 ); - // $(window).scrollLeft( 2500 ); + } + + function scroll_to_nodes() { + // Scroll to the top left node + if ( $("div.toolFormInCanvas").length > 0 ) { + var x = 5000, y = 5000; + $("div.toolFormInCanvas").each( function() { + x = Math.min( x, $(this).position().left ); + y = Math.min( x, $(this).position().left ); + }); + $("#canvas-container").css( { left: - x + 20, top: - y + 20 } ); + } } // Add a new step to the workflow by tool id @@ -255,12 +263,15 @@ // }); $("#right-content").html( text ); $("#right-content").find( "form" ).ajaxForm( { - method: 'POST', + type: 'POST', dataType: 'json', success: function( data ) { node.update_field_data( data ); }, - beforeSubmit: function( data ) { data.push( { name: 'tool_state', value: node.tool_state } ); } + beforeSubmit: function( data ) { + data.push( { name: 'tool_state', value: node.tool_state } ); + data.push( { name: '_', value: "true" } ); + } }).each( function() { form = this; $(this).find( "select[refresh_on_change='true']").change( function() { @@ -324,6 +335,7 @@ success: function( data ) { reset(); workflow.from_simple( data ); + workflow.has_changes = false; show_modal( "Workflow loaded", "Workflow loaded.", { "Ok" : function () { hide_modal(); } }); diff --git a/templates/workflow/editor_tool_form.mako b/templates/workflow/editor_tool_form.mako index ad6921f0671..da75062e777 100644 --- a/templates/workflow/editor_tool_form.mako +++ b/templates/workflow/editor_tool_form.mako @@ -5,15 +5,17 @@
${input.title_plural}
<% repeat_values = values[input.name] %> %for i in range( len( repeat_values ) ): - %if input.name in errors: - <% rep_errors = errors[input.name][i] %> - %else: - <% rep_errors = dict() %> - %endif + <% + if input.name in errors: + rep_errors = errors[input.name][i] + else: + rep_errors = dict() + index = repeat_values[i]['__index__'] + %>
${input.title} ${i + 1}
- ${do_inputs( input.inputs, repeat_values[ i ], rep_errors, prefix + input.name + "_" + str(i) + "|" )} -
+ ${do_inputs( input.inputs, repeat_values[ i ], rep_errors, prefix + input.name + "_" + str(index) + "|" )} +
%endfor
@@ -24,7 +26,7 @@ <% prefix = prefix + input.name + "|" %> <% group_errors = errors.get( input.name, {} ) %> ${row_for_param( input.test_param, group_values[ input.test_param.name ], group_errors, prefix )} - ${do_inputs( input.cases[ current_case ].inputs, group_values, group_errors, prefix + input.name + "|" )} + ${do_inputs( input.cases[ current_case ].inputs, group_values, group_errors, prefix )} %else: ${row_for_param( input, values[ input.name ], errors, prefix )} %endif diff --git a/templates/workflow/index.mako b/templates/workflow/index.mako index e2451cbaa61..71fe19bbe7f 100644 --- a/templates/workflow/index.mako +++ b/templates/workflow/index.mako @@ -51,10 +51,11 @@ %if workflows: - + %for workflow in workflows: +
NameLast UpdatedActions
Name# of StepsLast UpdatedActions
${workflow.name}${len(workflow.latest_workflow.steps)} ${str(workflow.update_time)[:19]} run