diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 53794e8b186..5c503787789 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1328,7 +1328,7 @@ class CloudUserCredentials( object ): self.accessKey = None self.secretKey = None self.credentials = [] - + class StoredWorkflow( object ): def __init__( self ): self.id = None @@ -1364,6 +1364,11 @@ class WorkflowStepConnection( object ): self.output_name = None self.input_step_id = None self.input_name = None + +class WorkflowOutput(object): + def __init__( self, workflow_step, output_name): + self.workflow_step = workflow_step + self.output_name = output_name class StoredWorkflowUserShareAssociation( object ): def __init__( self ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index f5d973a513e..28c15f4c97f 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -463,6 +463,12 @@ WorkflowStepConnection.table = Table( "workflow_step_connection", metadata, Column( "input_name", TEXT) ) +WorkflowOutput.table = Table( "workflow_output", metadata, + Column( "id", Integer, primary_key=True ), + Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False), + Column( "output_name", String(255), nullable=True) + ) + WorkflowInvocation.table = Table( "workflow_invocation", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -1238,7 +1244,10 @@ assign_mapper( context, Workflow, Workflow.table, properties=dict( steps=relation( WorkflowStep, backref='workflow', order_by=asc(WorkflowStep.table.c.order_index), cascade="all, delete-orphan", - lazy=False ) + lazy=False ), + # outputs = relation( WorkflowOutput, backref='workflow', + # primaryjoin=(Workflow.table.c.id == WorkflowStep.table.c.workflow_id), + # secondaryjoin=(WorkflowStep.table.c.id == WorkflowOutput.table.c.workflow_step_id)) ) ) assign_mapper( context, WorkflowStep, WorkflowStep.table, @@ -1247,6 +1256,9 @@ assign_mapper( context, WorkflowStep, WorkflowStep.table, annotations=relation( WorkflowStepAnnotationAssociation, order_by=WorkflowStepAnnotationAssociation.table.c.id, backref="workflow_steps" ) ) ) +assign_mapper( context, WorkflowOutput, WorkflowOutput.table, + properties=dict(workflow_step = relation( WorkflowStep, backref='workflow_outputs', primaryjoin=(WorkflowStep.table.c.id == WorkflowOutput.table.c.workflow_step_id)))) + assign_mapper( context, WorkflowStepConnection, WorkflowStepConnection.table, properties=dict( input_step=relation( WorkflowStep, backref="input_connections", cascade="all", primaryjoin=( WorkflowStepConnection.table.c.input_step_id == WorkflowStep.table.c.id ) ), diff --git a/lib/galaxy/model/migrate/versions/0056_workflow_outputs.py b/lib/galaxy/model/migrate/versions/0056_workflow_outputs.py new file mode 100644 index 00000000000..1f32ff4b64a --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0056_workflow_outputs.py @@ -0,0 +1,39 @@ +""" +Migration script to create tables for adding explicit workflow outputs. +""" + +from sqlalchemy import * +from sqlalchemy.orm import * +from migrate import * +from migrate.changeset import * + +import logging +logging.basicConfig( level=logging.DEBUG ) +log = logging.getLogger( __name__ ) + +# Need our custom types, but don't import anything else from model +from galaxy.model.custom_types import * + +metadata = MetaData( migrate_engine ) +db_session = scoped_session( sessionmaker( bind=migrate_engine, autoflush=False, autocommit=True ) ) + +WorkflowOutput_table = Table( "workflow_output", metadata, + Column( "id", Integer, primary_key=True ), + Column( "workflow_step_id", Integer, ForeignKey("workflow_step.id"), index=True, nullable=False), + Column( "output_name", String(255), nullable=True)) + +tables = [WorkflowOutput_table] + +def upgrade(): + print __doc__ + metadata.reflect() + for table in tables: + try: + table.create() + except: + log.warn( "Failed to create table '%s', ignoring (might result in wrong schema)" % table.name ) + +def downgrade(): + metadata.reflect() + for table in tables: + table.drop() \ No newline at end of file diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index f7e3a221b1e..cc1ab1cce57 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -1407,7 +1407,6 @@ class Admin( object ): @web.expose @web.require_admin def jobs( self, trans, stop = [], stop_msg = None, cutoff = 180, job_lock = None, **kwd ): - # DBTODO admin job lock. deleted = [] msg = None status = None diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 07ef6b21189..0e93ca783fe 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -358,7 +358,6 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno stored = self.get_stored_workflow( trans, id ) if not stored: error( "You do not own this workflow or workflow ID is invalid." ) - # Update workflow attributes if new values submitted. if 'name' in kwargs: # Rename workflow. @@ -368,7 +367,6 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno annotation = sanitize_html( kwargs[ 'annotation' ], 'utf-8', 'text/html' ) self.add_item_annotation( trans, stored, annotation ) trans.sa_session.flush() - return trans.fill_template( 'workflow/edit_attributes.mako', stored=stored, annotation=self.get_item_annotation_str( trans, trans.user, stored ) @@ -773,7 +771,9 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), 'form_html': module.get_config_form(), - 'annotation' : annotation_str + 'annotation' : annotation_str, + 'post_job_actions' : {}, + 'workflow_outputs' : [] } # Connections input_connections = step.input_connections @@ -794,6 +794,11 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno output_name = pja.output_name, action_arguments = pja.action_arguments) step_dict['post_job_actions'] = pja_dict + #workflow outputs + outputs = [] + for output in step.workflow_outputs: + outputs.append(output.output_name) + step_dict['workflow_outputs'] = outputs # Encode input connections as dictionary input_conn_dict = {} for conn in input_connections: @@ -838,6 +843,10 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno step.position = step_dict['position'] module = module_factory.from_dict( trans, step_dict ) module.save_to_step( step ) + if step_dict.has_key('workflow_outputs'): + for output_name in step_dict['workflow_outputs']: + m = model.WorkflowOutput(workflow_step = step, output_name = output_name) + trans.sa_session.add(m) if step.tool_errors: workflow.has_errors = True # Stick this in the step temporarily @@ -1278,6 +1287,84 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno errors=errors, incoming=kwargs ) + @web.expose + def tag_outputs( self, trans, id, check_user=True, **kwargs ): + stored = self.get_stored_workflow( trans, id, check_ownership=False ) + if check_user: + user = trans.get_user() + if stored.user != user: + if trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ + .filter_by( user=user, stored_workflow=stored ).count() == 0: + error( "Workflow is not owned by or shared with current user" ) + # Get the latest revision + workflow = stored.latest_workflow + # It is possible for a workflow to have 0 steps + if len( workflow.steps ) == 0: + error( "Workflow cannot be tagged for outputs because it does not have any steps" ) + if workflow.has_cycles: + error( "Workflow cannot be tagged for outputs because it contains cycles" ) + if workflow.has_errors: + error( "Workflow cannot be tagged for outputs because of validation errors in some steps" ) + # Build the state for each step + errors = {} + has_upgrade_messages = False + has_errors = False + if kwargs: + # If kwargs were provided, the states for each step should have + # been POSTed + for step in workflow.steps: + if step.type == 'tool': + # Extract just the output flags for this step. + p = "%s|otag|" % step.id + l = len(p) + outputs = [k[l:] for ( k, v ) in kwargs.iteritems() if k.startswith( p )] + if step.workflow_outputs: + for existing_output in step.workflow_outputs: + if existing_output.output_name not in outputs: + # print "Deleting action %s on %s" % (step.id, existing_output.output_name) + trans.sa_session.delete(existing_output) + else: + outputs.remove(existing_output.output_name) + for outputname in outputs: + # print "Creating new workflow output at step %s on output name %s" %(step.id, outputname) + m = model.WorkflowOutput(workflow_step_id = int(step.id), output_name = outputname) + trans.sa_session.add(m) + # Prepare each step + trans.sa_session.flush() + for step in workflow.steps: + step.upgrade_messages = {} + # Contruct modules + if step.type == 'tool' or step.type is None: + # Restore the tool state for the step + step.module = module_factory.from_workflow_step( trans, step ) + # Fix any missing parameters + step.upgrade_messages = step.module.check_and_update_state() + if step.upgrade_messages: + has_upgrade_messages = True + # Any connected input needs to have value DummyDataset (these + # are not persisted so we need to do it every time) + step.module.add_dummy_datasets( connections=step.input_connections ) + # Store state with the step + step.state = step.module.state + # Error dict + if step.tool_errors: + has_errors = True + errors[step.id] = step.tool_errors + else: + ## Non-tool specific stuff? + step.module = module_factory.from_workflow_step( trans, 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/tag_outputs.mako", + steps=workflow.steps, + workflow=stored, + has_upgrade_messages=has_upgrade_messages, + errors=errors, + incoming=kwargs ) + @web.expose def configure_menu( self, trans, workflow_ids=None ): user = trans.get_user() diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index 40754422e0f..20d3d4d78d8 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -165,6 +165,7 @@ class ToolModule( WorkflowModule ): self.tool_id = tool_id self.tool = trans.app.toolbox.tools_by_id[ tool_id ] self.post_job_actions = {} + self.workflow_outputs = [] self.state = None self.errors = None @@ -181,10 +182,9 @@ class ToolModule( WorkflowModule ): module.state = DefaultToolState() module.state.decode( d["tool_state"], module.tool, module.trans.app, secure=secure ) module.errors = d.get( "tool_errors", None ) - if 'post_job_actions' in d and d["post_job_actions"] != []: - module.post_job_actions = d["post_job_actions"] - else: - module.post_job_actions = {} + module.post_job_actions = d.get("post_job_actions", {}) + module.workflow_outputs = d.get("workflow_outputs", []) + return module @classmethod @@ -195,6 +195,7 @@ class ToolModule( WorkflowModule ): module.state.inputs = module.tool.params_from_strings( step.tool_inputs, trans.app, ignore_errors=True ) module.errors = step.tool_errors # module.post_job_actions = step.post_job_actions + module.workflow_outputs = step.workflow_outputs pjadict = {} for pja in step.post_job_actions: pjadict[pja.action_type] = pja diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index 28021dda76d..ba30aa8276a 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -275,6 +275,7 @@ $.extend( Node.prototype, { this.tooltip = data.tooltip ? data.tooltip : "" this.annotation = data.annotation; this.post_job_actions = data.post_job_actions; + this.workflow_outputs = data.workflow_outputs if ( this.tool_errors ) { f.addClass( "tool-node-error" ); @@ -416,6 +417,10 @@ $.extend( Workflow.prototype, { post_job_actions[ act.type + act.output_name ] = pja; }); } + if (!node.workflow_outputs){ + node.workflow_outputs = []; + // Just in case. + } var node_data = { id : node.id, type : node.type, @@ -425,7 +430,8 @@ $.extend( Workflow.prototype, { input_connections : input_connections, position : $(node.element).position(), annotation: node.annotation, - post_job_actions: node.post_job_actions + post_job_actions: node.post_job_actions, + workflow_outputs: node.workflow_outputs }; nodes[ node.id ] = node_data; }); diff --git a/templates/workflow/editor.mako b/templates/workflow/editor.mako index f6dac6080d4..cf0f0bdd997 100644 --- a/templates/workflow/editor.mako +++ b/templates/workflow/editor.mako @@ -132,15 +132,51 @@ make_popupmenu( $("#workflow-options-button"), { ##"Create New" : create_new_workflow_dialog, "Edit Attributes" : edit_workflow_attributes, + "Edit Workflow Outputs": edit_workflow_outputs, "Layout": layout_editor, "Save" : save_current_workflow, ##"Load a Workflow" : load_workflow, "Close": close_editor, - "Hide Intermediate Steps": hide_intermediate_steps }); - function hide_intermediate_steps(){ - + function edit_workflow_outputs(){ + workflow.clear_active_node(); + $('.right-content').hide(); + var new_content = ""; + for (var node_key in workflow.nodes){ + var node = workflow.nodes[node_key]; + if(node.type == 'tool'){ + new_content += "
"+output.name +"
"; + } + else{ + new_content += ""+output.name +"
"; + } + } + new_content += "
${error_dict[param.name]}Select the checkboxes for step outputs you would like to mark as whole-workflow outputs.
+%if has_upgrade_messages: + +%endif + + \ No newline at end of file