diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index c29008de9fb..00696f28f12 100644 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -142,7 +142,7 @@ class DefaultToolState( object ): """ # Convert parameters to a dictionary of strings, and save curent # page in that dict - value = tool.params_to_strings( self.inputs, app ) + value = params_to_strings( tool.inputs, self.inputs, app ) value["__page__"] = self.page value = simplejson.dumps( value ) # Make it secure @@ -161,7 +161,7 @@ class DefaultToolState( object ): # Restore from string values = json_fix( simplejson.loads( value ) ) self.page = values.pop( "__page__" ) - self.inputs = tool.params_from_strings( values, app, ignore_errors=True ) + self.inputs = params_from_strings( tool.inputs, values, app, ignore_errors=True ) class Tool: """ @@ -740,6 +740,7 @@ class Tool: prefix = "%s|" % ( key ) # Deal with the 'test' element and see if it's value changed test_param_key = prefix + input.test_param.name + test_param_error = None if test_param_key not in incoming and update_only: # Update only, keep previous value and state, but still # recurse in case there are nested changes @@ -751,7 +752,7 @@ class Tool: # Get value of test param and determine current case test_incoming = incoming.get( prefix + input.test_param.name, None ) value, test_param_error = \ - self.check_param( trans, input.test_param, test_incoming, context ) + check_param( trans, input.test_param, test_incoming, context ) current_case = input.get_current_case( value, trans ) if current_case != old_current_case: # Current case has changed, throw away old state @@ -786,74 +787,48 @@ class Tool: # preserve the old error message. if input.name in old_errors: errors[ input.name ] = old_errors[ input.name ] - # SelectToolParameters and DataToolParameters whose options are dynamically - # generated based on the current value of a dependency parameter require special - # handling. When the dependency parameter's value is changed, the form is - # submitted ( due to the refresh_on_change behavior ). When this occurs, the - # "dependent" parameter's value has not been reset ( dynamically generated based - # on the new value of its dependency ) prior to reaching this point, so we need - # to regenerate it before it is validated in check_param(). - incoming_value_generated = False - if not( 'runtool_btn' in incoming or 'URL' in incoming ): - # Form must have been refreshed, probably due to a refresh_on_change - try: - if input.is_dynamic: - dependencies = input.get_dependencies() - for dependency_name in dependencies: - dependency_value = changed_dependencies.get( dependency_name, None ) - if dependency_value: - # We need to dynamically generate the current input based on - # the changed dependency parameter - changed_params = {} - changed_params[dependency_name] = dependency_value - changed_params[input.name] = input - incoming_value = input.get_initial_value( trans, changed_params ) - incoming_value_generated = True - # Delete the dependency_param from chagned_dependencies since its - # dependent param has been generated based its new value. - del changed_dependencies[dependency_name] - break - except: - pass - if not incoming_value_generated: - incoming_value = incoming.get( key, None ) - value, error = self.check_param( trans, input, incoming_value, context ) - if input.dependent_params and state[ input.name ] != value: - # We need to keep track of changed dependency parametrs ( parameters - # that have dependent parameters whose options are dynamically generated ) - changed_dependencies[ input.name ] = value - if error: - errors[ input.name ] = error - state[ input.name ] = value + else: + # SelectToolParameters and DataToolParameters whose options are dynamically + # generated based on the current value of a dependency parameter require special + # handling. When the dependency parameter's value is changed, the form is + # submitted ( due to the refresh_on_change behavior ). When this occurs, the + # "dependent" parameter's value has not been reset ( dynamically generated based + # on the new value of its dependency ) prior to reaching this point, so we need + # to regenerate it before it is validated in check_param(). + incoming_value_generated = False + if not( 'runtool_btn' in incoming or 'URL' in incoming ): + # Form must have been refreshed, probably due to a refresh_on_change + try: + if input.is_dynamic: + dependencies = input.get_dependencies() + for dependency_name in dependencies: + dependency_value = changed_dependencies.get( dependency_name, None ) + if dependency_value: + # We need to dynamically generate the current input based on + # the changed dependency parameter + changed_params = {} + changed_params[dependency_name] = dependency_value + changed_params[input.name] = input + incoming_value = input.get_initial_value( trans, changed_params ) + incoming_value_generated = True + # Delete the dependency_param from chagned_dependencies since its + # dependent param has been generated based its new value. + del changed_dependencies[dependency_name] + break + except: + pass + if not incoming_value_generated: + incoming_value = incoming.get( key, None ) + value, error = check_param( trans, input, incoming_value, context ) + if input.dependent_params and state[ input.name ] != value: + # We need to keep track of changed dependency parametrs ( parameters + # that have dependent parameters whose options are dynamically generated ) + changed_dependencies[ input.name ] = value + if error: + errors[ input.name ] = error + state[ input.name ] = value return errors - def check_param( self, trans, param, incoming_value, param_values ): - """ - Check the value of a single parameter `param`. The value in - `incoming_value` is converted from its HTML encoding and validated. - The `param_values` argument contains the processed values of - previous parameters (this may actually be an ExpressionContext - when dealing with grouping scenarios). - """ - value = incoming_value - error = None - try: - if param.name == 'file_data': - pass - elif value is not None or isinstance(param, DataToolParameter): - # Convert value from HTML representation - value = param.from_html( value, trans, param_values ) - # Allow the value to be converted if neccesary - filtered_value = param.filter_value( value, trans, param_values ) - # Then do any further validation on the value - param.validate( filtered_value, trans.history ) - elif value is None and isinstance( param, SelectToolParameter ): - # An empty select list or column list - param.validate( value, trans.history ) - except ValueError, e: - error = str( e ) - return value, error - def get_static_param_values( self, trans ): """ Returns a map of parameter names and values if the tool does not @@ -880,34 +855,10 @@ class Tool: return self.tool_action.execute( self, trans, incoming ) def params_to_strings( self, params, app ): - """ - Convert a dictionary of parameter values to a dictionary of strings - suitable for persisting. The `value_to_basic` method of each parameter - is called to convert its value to basic types, the result of which - is then json encoded (this allowing complex nested parameters and - such). - """ - rval = dict() - for key, value in params.iteritems(): - if key in self.inputs: - value = self.inputs[ key ].value_to_basic( value, app ) - rval[ key ] = str( simplejson.dumps( value ) ) - return rval + return params_to_strings( self.inputs, params, app ) def params_from_strings( self, params, app, ignore_errors=False ): - """ - Convert a dictionary of strings as produced by `params_to_strings` - back into parameter values (decode the json representation and then - allow each parameter to convert the basic types into the parameters - preferred form). - """ - rval = dict() - for key, value in params.iteritems(): - value = json_fix( simplejson.loads( value ) ) - if key in self.inputs: - value = self.inputs[key].value_from_basic( value, app, ignore_errors ) - rval[ key ] = value - return rval + return params_from_strings( self.inputs, params, app, ignore_errors ) def handle_unvalidated_param_values( self, input_values, app ): """ diff --git a/lib/galaxy/tools/parameters/__init__.py b/lib/galaxy/tools/parameters/__init__.py index 2ffcbe37971..2aa3495ac71 100644 --- a/lib/galaxy/tools/parameters/__init__.py +++ b/lib/galaxy/tools/parameters/__init__.py @@ -2,4 +2,62 @@ Classes encapsulating Galaxy tool parameters. """ -from basic import * \ No newline at end of file +from basic import * +from galaxy.util.json import * + +def check_param( trans, param, incoming_value, param_values ): + """ + Check the value of a single parameter `param`. The value in + `incoming_value` is converted from its HTML encoding and validated. + The `param_values` argument contains the processed values of + previous parameters (this may actually be an ExpressionContext + when dealing with grouping scenarios). + """ + value = incoming_value + error = None + try: + if param.name == 'file_data': + pass + elif value is not None or isinstance(param, DataToolParameter): + # Convert value from HTML representation + value = param.from_html( value, trans, param_values ) + # Allow the value to be converted if neccesary + filtered_value = param.filter_value( value, trans, param_values ) + # Then do any further validation on the value + param.validate( filtered_value, trans.history ) + elif value is None and isinstance( param, SelectToolParameter ): + # An empty select list or column list + param.validate( value, trans.history ) + except ValueError, e: + error = str( e ) + return value, error + +def params_to_strings( params, param_values, app ): + """ + Convert a dictionary of parameter values to a dictionary of strings + suitable for persisting. The `value_to_basic` method of each parameter + is called to convert its value to basic types, the result of which + is then json encoded (this allowing complex nested parameters and + such). + """ + rval = dict() + for key, value in param_values.iteritems(): + if key in params: + value = params[ key ].value_to_basic( value, app ) + rval[ key ] = str( to_json_string( value ) ) + return rval + +def params_from_strings( params, param_values, app, ignore_errors=False ): + """ + Convert a dictionary of strings as produced by `params_to_strings` + back into parameter values (decode the json representation and then + allow each parameter to convert the basic types into the parameters + preferred form). + """ + rval = dict() + for key, value in param_values.iteritems(): + value = json_fix( from_json_string( value ) ) + if key in params: + value = params[key].value_from_basic( value, app, ignore_errors ) + rval[ key ] = value + return rval \ No newline at end of file diff --git a/lib/galaxy/util/json.py b/lib/galaxy/util/json.py new file mode 100644 index 00000000000..8e69713bc19 --- /dev/null +++ b/lib/galaxy/util/json.py @@ -0,0 +1,20 @@ + +__all__ = [ "to_json_string", "from_json_string", "json_fix"] + +import pkg_resources +pkg_resources.require( "simplejson" ) + +import simplejson + +to_json_string = simplejson.dumps +from_json_string = simplejson.loads + +def json_fix( val ): + if isinstance( val, list ): + return [ json_fix( v ) for v in val ] + elif isinstance( val, dict ): + return dict( [ ( json_fix( k ), json_fix( v ) ) for ( k, v ) in val.iteritems() ] ) + elif isinstance( val, unicode ): + return val.encode( "utf8" ) + else: + return val \ No newline at end of file diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 06567148db0..7a49dab1297 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -2,11 +2,12 @@ from galaxy.web.base.controller import * import simplejson -from galaxy.tools.parameters import DataToolParameter +from galaxy.tools.parameters import DataToolParameter, check_param from galaxy.tools import DefaultToolState from galaxy.tools.parameters.grouping import Repeat, Conditional from galaxy.datatypes.data import Data from galaxy.util.odict import odict +from galaxy.util.bunch import Bunch from galaxy.util.topsort import topsort, topsort_levels, CycleError class WorkflowController( BaseController ): @@ -60,7 +61,6 @@ class WorkflowController( BaseController ): # Display the management page return self.index( trans ) - @web.expose @web.require_login( "edit workflows" ) def editor( self, trans, id=None ): @@ -131,9 +131,11 @@ class WorkflowController( BaseController ): rval['name'] = module.name rval['type'] = module.type rval['tool_state'] = state = module.get_state() - rval['data_inputs'] = module.get_data_inputs( state ) - rval['data_outputs'] = module.get_data_outputs( state ) - rval['form_html'] = module.get_config_form_html( trans, state ) + rval['data_inputs'] = module.get_data_inputs() + rval['data_outputs'] = module.get_data_outputs() + rval['form_html'] = trans.fill_template( + "workflow/editor_generic_form.mako", + form = module.get_config_form() ) return rval @web.json @@ -184,9 +186,11 @@ class WorkflowController( BaseController ): module = module_types[step.type].from_workflow_step( step ) step_dict['name'] = module.name step_dict['tool_state'] = state = module.get_state() - step_dict['data_inputs'] = module.get_data_inputs( state ) - step_dict['data_outputs'] = module.get_data_outputs( state ) - step_dict['form_html'] = module.get_config_form_html( trans, state ) + step_dict['data_inputs'] = module.get_data_inputs() + step_dict['data_outputs'] = module.get_data_outputs() + step_dict['form_html'] = trans.fill_template( + "workflow/editor_generic_form.mako", + form = module.get_config_form() ) # Connections input_conn_dict = {} for conn in step.input_connections: @@ -319,9 +323,12 @@ class WorkflowController( BaseController ): if type( job_ids ) == str: job_ids = [ job_ids ] job_ids = [ int( id ) for id in job_ids ] - if type( dataset_ids ) == str: - dataset_ids = [ job_ids ] - dataset_ids = [ int( id ) for id in dataset_ids ] + if dataset_ids: + if type( dataset_ids ) == str: + dataset_ids = [ job_ids ] + dataset_ids = [ int( id ) for id in dataset_ids ] + else: + dataset_ids = [] # Find each job, for security we (implicately) check that they are # associated witha job in the current history. jobs, warnings = get_job_dict( trans ) @@ -335,7 +342,6 @@ class WorkflowController( BaseController ): step.type = 'data_input' hid_to_output_pair[ hid ] = ( step, 'output' ) steps.append( step ) - print hid_to_output_pair # Tool steps for job_id in job_ids: assert job_id in jobs_by_id, "Attempt to create workflow with job not connected to current history" @@ -344,6 +350,7 @@ class WorkflowController( BaseController ): param_values = job.get_param_values( trans.app ) associations = cleanup_param_values( tool.inputs, param_values ) step = model.WorkflowStep() + step.type = 'tool' step.tool_id = job.tool_id step.tool_inputs = tool.params_to_strings( param_values, trans.app ) # NOTE: We shouldn't need to do two passes here since only @@ -363,7 +370,6 @@ class WorkflowController( BaseController ): # Store created dataset hids for assoc in job.output_datasets: hid_to_output_pair[ assoc.dataset.hid ] = ( step, assoc.name ) - print hid_to_output_pair # Workflow to populate workflow = model.Workflow() workflow.name = workflow_name @@ -418,54 +424,82 @@ class WorkflowController( BaseController ): p = "%s|" % step.id l = len(p) step_args = dict( ( k[l:], v ) for ( k, v ) in kwargs.iteritems() if k.startswith( p ) ) - if step.type == 'tool': + step_errors = None + if step.type == 'tool' or step.type is None: # Get the tool tool = trans.app.toolbox.tools_by_id[ step.tool_id ] # Get the state state = DefaultToolState() state.decode( step_args.pop("tool_state"), tool, trans.app ) step.state = state - # Connections by input name - step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) # Get old errors old_errors = state.inputs.pop( "__errors__", {} ) # Update the state step_errors = tool.update_state( trans, tool.inputs, step.state.inputs, step_args, update_only=True, old_errors=old_errors ) - if step_errors: - errors[step.id] = state.inputs["__errors__"] = step_errors else: - return error( "Modules not yet supported for running" ) + module = step.module = WorkflowModule.from_workflow_step( step ) + state = step.state = module.decode_runtime_state( trans, step_args.pop( "tool_state" ) ) + step_errors = module.update_runtime_state( trans, state, step_args ) + if step_errors: + errors[step.id] = state.inputs["__errors__"] = step_errors + # Connections by input name + step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) if not errors: # Run each step, connecting outputs to inputs outputs = {} for step in workflow.steps: - tool = trans.app.toolbox.tools_by_id[ step.tool_id ] - inputs = step.state.inputs - # Connect up - for conn in step.input_connections: - inputs[ conn.input_name ] = outputs[ conn.output_step.id ][ conn.output_name ] - outputs[ step.id ] = tool.execute( trans, step.state.inputs ) + if step.type == 'tool' or step.type is None: + tool = trans.app.toolbox.tools_by_id[ step.tool_id ] + input_values = step.state.inputs + # Connect up + # TODO: Generalize out visitor + def visitor( inputs, input_values, prefix ): + for input in inputs.itervalues(): + if isinstance( input, Repeat ): + for i, d in enumerate( input_values[ input.name ] ): + index = d['__index__'] + new_prefix = prefix + "%s_%d|" % ( input.name, index ) + visitor( input.inputs, d, new_prefix) + elif isinstance( input, Conditional ): + values = input_values[ input.name ] + current = values["__current_case__"] + label_prefix = label_prefix + prefix = prefix + "|" + input.name + visitor( input.cases[current].inputs, values, prefix ) + else: + if isinstance( input, DataToolParameter ): + prefixed_name = prefix + input.name + if prefixed_name in step.input_connections_by_name: + conn = step.input_connections_by_name[ prefixed_name ] + input_values[ input.name ] = outputs[ conn.output_step.id ][ conn.output_name ] + visitor( tool.inputs, input_values, "" ) + # Execute it + outputs[ step.id ] = tool.execute( trans, step.state.inputs ) + else: + outputs[ step.id ] = step.module.execute( trans, step.state ) return trans.fill_template( "workflow/run_complete.mako", workflow=stored, outputs=outputs ) else: for step in workflow.steps: - if step.type == 'tool': + if step.type == 'tool' or step.type is None: # Build a new tool state for the step tool = trans.app.toolbox.tools_by_id[ step.tool_id ] state = DefaultToolState() state.inputs = tool.params_from_strings( step.tool_inputs, trans.app ) # Store state with the step step.state = state - # Connections by input name - step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) # This should never actually happen since we don't allow # running workflows with errors (yet?) if step.tool_errors: errors[step.id] = step.tool_errors else: - return error( "Modules not yet supported for running" ) + ## Non-tool specific stuff? + step.module = WorkflowModule.from_workflow_step( step ) + step.state = step.module.get_runtime_state() + # Connections by input name + step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) # Render the form return trans.fill_template( "workflow/run.mako", @@ -475,29 +509,66 @@ class WorkflowController( BaseController ): ## ---- Workflow modules (to be factored out) --------------------------------- +from elementtree.ElementTree import Element + ## TODO: 'Tool' should be a module rather than a special case -class InputDataModule( object ): +class WorkflowModule( object ): + @classmethod + def from_workflow_step( cls, step ): + assert step.type in module_types, "Unknown module type" + return module_types[ step.type ].from_workflow_step( step ) + +class InputDataModule( WorkflowModule ): type = "data_input" name = "Input dataset" + _inputs = { + 'input' : DataToolParameter( None, Element( "param", name="input", label="Input Dataset", type="data", format="data" ) ) + } @classmethod def from_state( cls, state ): return cls() @classmethod - def from_workflow_step( cls, state ): + def from_workflow_step( cls, step ): return cls() def get_state( self ): return None - def get_data_inputs( self, state ): - return [] - def get_data_outputs( self, state ): - return [ dict( name='output', extension='input' ) ] - def get_config_form_html( self, trans, state ): - form = web.FormBuilder( title=self.name ) - return trans.fill_template( "workflow/editor_generic_form.mako", form=form ) def save_to_step( self, step ): pass + def get_data_inputs( self ): + return [] + def get_data_outputs( self ): + return [ dict( name='output', extension='input' ) ] + def get_config_form( self ): + return web.FormBuilder( title=self.name ) + + def get_runtime_inputs( self ): + return self._inputs + def get_runtime_state( self ): + state = DefaultToolState() + state.inputs = dict( input=None ) + return state + def encode_runtime_state( self, trans, state ): + fake_tool = Bunch( inputs = self.get_runtime_inputs() ) + return state.encode( fake_tool, trans.app ) + def decode_runtime_state( self, trans, string ): + fake_tool = Bunch( inputs = self.get_runtime_inputs() ) + state = DefaultToolState() + state.decode( string, fake_tool, trans.app ) + return state + def update_runtime_state( self, trans, state, values ): + errors = {} + for name, param in self._inputs.iteritems(): + value, error = check_param( trans, param, values[name], values ) + state.inputs[ name ] = value + if error: + errors[ name ] = error + return errors + + def execute( self, trans, state ): + return dict( output=state.inputs['input']) + module_types = dict( data_input=InputDataModule ) ## ---- Utility methods ------------------------------------------------------- diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index 02f454ba421..57ae792cb62 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -257,7 +257,9 @@ $.extend( Node.prototype, { }, init_field_data : function ( data ) { var f = this.element; - this.type = data.type + if ( data.type ) { + this.type = data.type; + } this.form_html = data.form_html; this.tool_state = data.tool_state; this.tool_errors = data.tool_errors; @@ -281,7 +283,11 @@ $.extend( Node.prototype, { $.each( data.data_outputs, function( i, output ) { var t = $( "
" ); node.enable_output_terminal( t, output.name, output.extension ); - b.append( $("