diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py
index 4a9ee3e490b..cdea79f4323 100644
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -67,6 +67,15 @@ class Job( object ):
# For historical reasons state propogates down to datasets
for da in self.output_datasets:
da.dataset.state = state
+ def get_param_values( self, app ):
+ """
+ Read encoded parameter values from the database and turn back into a
+ dict of tool parameter values.
+ """
+ param_dict = dict( [ ( p.name, p.value ) for p in self.parameters ] )
+ tool = app.toolbox.tools_by_id[self.tool_id]
+ param_dict = tool.params_from_strings( param_dict, app )
+ return param_dict
class JobParameter( object ):
def __init__( self, name, value ):
diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py
index 1cb3c5cd174..68e674d66bf 100644
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -280,6 +280,9 @@ class Tool:
log.exception( "Failed to parse tool tests" )
else:
self.tests = None
+ # Determine if this tool can be used in workflows
+ self.is_workflow_compatible = self.check_workflow_compatible()
+
def parse_inputs( self, root ):
"""
@@ -459,6 +462,19 @@ class Tool:
for name in param.get_dependencies():
context[ name ].refresh_on_change = True
return param
+
+ def check_workflow_compatible( self ):
+ # This is probably the best bet for detecting external web tools
+ # right now
+ if self.action != "/tool_runner/index":
+ return False
+ # HACK: upload is (as always) a special case becuase file parameters
+ # can't be persisted.
+ if self.id == "upload1":
+ return False
+ # TODO: Anyway to capture tools that dynamically change their own
+ # outputs?
+ return True
def new_state( self, trans, all_pages=False ):
"""
diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py
index 3169afe09a4..f77a5379d92 100644
--- a/lib/galaxy/tools/actions/__init__.py
+++ b/lib/galaxy/tools/actions/__init__.py
@@ -44,7 +44,8 @@ class DefaultToolAction( object ):
# FIXME: does this need to modify 'incoming' or should this be
# moved into 'build_param_dict'? Is this just about getting the
- # metadata into the command line?
+ # metadata into the command line?
+ # NEED TO FIX THIS SOON.
input_names = []
input_ext = 'data'
input_dbkey = incoming.get( "dbkey", "?" )
diff --git a/lib/galaxy/util/topsort.py b/lib/galaxy/util/topsort.py
index f187155d6b9..15fc988727e 100644
--- a/lib/galaxy/util/topsort.py
+++ b/lib/galaxy/util/topsort.py
@@ -161,4 +161,49 @@ def topsort(pairlist):
for x in numpreds.keys():
assert numpreds[x] > 0
raise CycleError(answer, numpreds, successors)
- return answer
\ No newline at end of file
+ return answer
+
+def topsort_levels(pairlist):
+ numpreds = {} # elt -> # of predecessors
+ successors = {} # elt -> list of successors
+ for first, second in pairlist:
+ # make sure every elt is a key in numpreds
+ if not numpreds.has_key(first):
+ numpreds[first] = 0
+ if not numpreds.has_key(second):
+ numpreds[second] = 0
+
+ # if they're the same, there's no real dependence
+ if first == second:
+ continue
+
+ # since first < second, second gains a pred ...
+ numpreds[second] = numpreds[second] + 1
+
+ # ... and first gains a succ
+ if successors.has_key(first):
+ successors[first].append(second)
+ else:
+ successors[first] = [second]
+
+ answer = []
+
+ while 1:
+ # Suck up everything without a predecessor.
+ levparents = [x for x in numpreds.keys() if numpreds[x] == 0]
+ if not levparents:
+ break
+ answer.append( levparents )
+ for levparent in levparents:
+ del numpreds[levparent]
+ if successors.has_key(levparent):
+ for levparentsucc in successors[levparent]:
+ numpreds[levparentsucc] -= 1
+ del successors[levparent]
+
+ if numpreds:
+ # Everything in num_parents has at least one child ->
+ # there's a cycle.
+ raise CycleError( answer, numpreds, successors )
+
+ return answer
diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py
new file mode 100644
index 00000000000..7c3f03a530b
--- /dev/null
+++ b/lib/galaxy/web/controllers/history.py
@@ -0,0 +1,158 @@
+from galaxy.web.base.controller import *
+
+from galaxy.tools.parameters import DataToolParameter
+from galaxy.tools import DefaultToolState
+from galaxy.tools.grouping import Repeat, Conditional
+from galaxy.datatypes.data import Data
+from galaxy.workflow import Workflow, WorkflowStep
+
+from galaxy.util.odict import odict
+
+import simplejson
+
+class HistoryController( BaseController ):
+
+ def __get_job_dict( self, trans ):
+ """
+ Return a dictionary of Job -> [ Dataset ] mappings, for all finished
+ active Datasets in the current history and the jobs that created them.
+ """
+ history = trans.get_history()
+ # Get the jobs that created the datasets
+ warnings = set()
+ jobs = odict()
+ for dataset in history.active_datasets:
+ # FIXME: Create "Dataset.is_finished"
+ if dataset.state in ( 'new', 'running', 'queued' ):
+ warnings.add( "Some datasets still queued or running were ignored" )
+ continue
+ for assoc in dataset.creating_job_associations:
+ job = assoc.job
+ if job in jobs:
+ jobs[ job ].append( ( assoc.name, dataset ) )
+ else:
+ jobs[ job ] = [ ( assoc.name, dataset ) ]
+ return jobs, warnings
+
+ def __cleanup_param_values( self, inputs, values ):
+ """
+ Remove 'Data' values from `param_values` but track the associations
+ """
+ associations = []
+ names_to_clean = []
+ # dbkey is pushed in by the framework
+ if 'dbkey' in values:
+ del values['dbkey']
+ root_values = values
+ # Cleanup all data inputs
+ def cleanup( prefix, inputs, values ):
+ for key, input in inputs.items():
+ if isinstance( input, DataToolParameter ):
+ tmp = values[key]
+ 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 ) )
+ # 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
+ key = prefix + key + "_"
+ for k in root_values.keys():
+ if k.startswith( key ):
+ del root_values[k]
+ elif isinstance( input, Repeat ):
+ group_values = values[key]
+ for i in range( len( group_values ) ):
+ prefix = "%s_%d|" % ( key, i )
+ cleanup( prefix, input.inputs, group_values[i] )
+ elif isinstance( input, Conditional ):
+ group_values = values[input.name]
+ current_case = group_values['__current_case__']
+ prefix = "%s|" % ( key )
+ cleanup( prefix, input.cases[current_case].inputs, group_values )
+ cleanup( "", inputs, values )
+ return associations
+
+ @web.expose
+ def workflow_from_current_history( self, trans, job_ids=None, workflow_name=None ):
+ if trans.request.method == 'POST':
+ return self.workflow_from_current_history_post( trans, job_ids, workflow_name )
+ user = trans.get_user()
+ if not user:
+ return trans.show_error_message( "Must be logged in to create workflows" )
+ jobs, warnings = self.__get_job_dict( trans )
+ # Render
+ return trans.fill_template(
+ "history/workflow_from_current_history.mako",
+ jobs=jobs,
+ warnings=warnings )
+
+ def workflow_from_current_history_post( self, trans, job_ids, workflow_name ):
+ user = trans.get_user()
+ if not user:
+ return trans.show_error_message( "Must be logged in to create workflows" )
+ # Ensure a list
+ if type( job_ids ) == str:
+ job_ids = [ job_ids ]
+ job_ids = [ int( id ) for id in job_ids ]
+ # Find each job, for security we (implicately) check that they are
+ # associated witha job in the current history.
+ jobs, warnings = self.__get_job_dict( trans )
+ # Create a mapping from hid to ( job_id, output_name )
+ hid_to_output_pair = {}
+ for job, datasets in jobs.iteritems():
+ for assoc_name, data in datasets:
+ hid_to_output_pair[ data.hid ] = ( job.id, assoc_name )
+ # Mapping from job ids to workflow step ids (0, 1, 2, ...)
+ job_id_to_step_id = dict( ( job_id, i ) for ( i, job_id ) in enumerate( job_ids ) )
+ # Workflow to populate
+ workflow = Workflow()
+ # Back-translate each job
+ jobs_by_id = dict( ( job.id, job ) for job in jobs.keys() )
+ for step_id, job_id in enumerate( job_ids ):
+ assert job_id in jobs_by_id, "Attempt to create workflow with job not connected to current history"
+ job = jobs_by_id[ job_id ]
+ tool = trans.app.toolbox.tools_by_id[ job.tool_id ]
+ param_values = job.get_param_values( trans.app )
+ associations = self.__cleanup_param_values( tool.inputs, param_values )
+ step = WorkflowStep()
+ step.id = step_id
+ step.tool_id = job.tool_id
+ step.tool_inputs = param_values
+ for other_hid, input_name in associations:
+ other_job_id, other_name = hid_to_output_pair[ 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_id:
+ step.input_connections[input_name] = ( job_id_to_step_id[ other_job_id ], other_name )
+ else:
+ step.input_connections[input_name] = None
+ workflow.steps[ step_id ] = step
+ # Try to order the nodes
+ workflow.order_nodes()
+ # And let's try to set up some reasonable locations
+ levorder = workflow.order_nodes_levels()
+ base_pos = 2510
+ for i, steps_at_level in enumerate( levorder ):
+ for j, step_id in enumerate( steps_at_level ):
+ step = workflow.steps[step_id]
+ step.position = dict( top = ( base_pos + 120 * j ),
+ left = ( base_pos + 220 * i ) )
+ # Store it
+ stored = model.StoredWorkflow.get_by( user = user, name = workflow_name )
+ if stored is None:
+ stored = model.StoredWorkflow()
+ stored.user = user
+ stored.name = workflow_name
+ stored.encoded_value = simplejson.dumps( workflow.to_simple() )
+ stored.flush()
+ #
+ return trans.show_ok_message( "Workflow '%s' created.
Click to load in workflow editor"
+ % ( workflow_name, web.url_for( controller='workflow_editor', action=None, workflow_name=workflow_name ) ) )
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/galaxy/web/controllers/workflow_editor.py b/lib/galaxy/web/controllers/workflow_editor.py
index 5b0f7dc2d3d..2f33730cbcb 100644
--- a/lib/galaxy/web/controllers/workflow_editor.py
+++ b/lib/galaxy/web/controllers/workflow_editor.py
@@ -11,11 +11,12 @@ class WorkflowEditor( BaseController ):
beta = True
@web.expose
- def index( self, trans ):
+ def index( self, trans, workflow_name=None ):
user = trans.get_user()
if not user:
return trans.show_error_message( "Must be logged in to create or modify workflows" )
- return trans.fill_template( "workflow_editor/index.mako" )
+ return trans.fill_template( "workflow_editor/index.mako",
+ workflow_name=workflow_name )
@web.expose
def canvas( self, trans ):
@@ -67,6 +68,7 @@ class WorkflowEditor( BaseController ):
decode_state( node, trans.app )
# Create workflow from json data
workflow = Workflow.from_simple( data )
+ workflow.order_nodes()
# Store it
stored = model.StoredWorkflow.get_by( user = user, name = workflow_name )
if stored is None:
diff --git a/lib/galaxy/workflow/__init__.py b/lib/galaxy/workflow/__init__.py
index 11f9236d0c6..a42f3e11d7d 100644
--- a/lib/galaxy/workflow/__init__.py
+++ b/lib/galaxy/workflow/__init__.py
@@ -1,5 +1,5 @@
from galaxy.tools import DefaultToolState
-from galaxy.util.topsort import topsort, CycleError
+from galaxy.util.topsort import topsort, topsort_levels, CycleError
class Workflow( object ):
@@ -21,7 +21,6 @@ class Workflow( object ):
if step.has_errors:
workflow.has_errors = True
workflow.steps[ id ] = step
- workflow.node_order = workflow.order_nodes()
return workflow
def to_simple( self ):
@@ -35,27 +34,40 @@ class Workflow( object ):
has_cycles=self.has_cycles,
has_errors=self.has_errors )
+ def edge_list( self ):
+ edges = []
+ all_ids = set( self.steps.keys() )
+ for step in self.steps.values():
+ edges.append( ( step.id, step.id ) )
+ for name, conn in step.input_connections.iteritems():
+ if conn is not None:
+ other_node_id, _ = conn
+ edges.append( ( other_node_id, step.id ) )
+ return edges
+
def order_nodes( self ):
"""
Perform topological sort of the steps, return an ordered list of ids
"""
self.has_cycles = False
- edges = []
- all_ids = set( self.steps.keys() )
- for step in self.steps.values():
- for name, conn in step.input_connections.iteritems():
- if conn is not None:
- other_node_id, _ = conn
- edges.append( ( other_node_id, step.id ) )
+ edges = self.edge_list()
try:
node_order = topsort( edges )
- node_order_set = set( node_order )
- node_order.extend( [ id for id in all_ids if id not in node_order ] )
+ #node_order_set = set( node_order )
+ #node_order.extend( [ id for id in all_ids if id not in node_order ] )
self.node_order = node_order
+ return self.node_order
except CycleError:
self.has_cycles = True
self.node_order = None
- return self.node_order
+ return None
+
+ def order_nodes_levels( self ):
+ edges = self.edge_list()
+ try:
+ return topsort_levels( edges )
+ except CycleError:
+ return None
class WorkflowStep( object ):
diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl
index dccd05a10e5..0c7a250edcc 100644
--- a/static/june_2007_style/base.css.tmpl
+++ b/static/june_2007_style/base.css.tmpl
@@ -76,6 +76,15 @@ div.toolFormBody div.toolFormTitle
margin-bottom: 5px;
}
+div.toolFormDisabled div.toolFormTitle {
+ background: ${layout_bg};
+ border-color: ${layout_border};
+}
+
+div.toolFormDisabled {
+ border-color: ${layout_border};
+}
+
div.toolHelp
{
}
@@ -86,6 +95,13 @@ div.toolHelpBody
overflow: auto;
}
+div.titleRow {
+ font-weight: bold;
+ border-bottom: dotted gray 1px;
+ margin-bottom: 0.5em;
+ padding-bottom: 0.25em;
+}
+
/* Forms */
div.form-row
diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css
index 53c51eccb63..ad87a6337cc 100644
--- a/static/june_2007_style/blue/base.css
+++ b/static/june_2007_style/blue/base.css
@@ -76,6 +76,15 @@ div.toolFormBody div.toolFormTitle
margin-bottom: 5px;
}
+div.toolFormDisabled div.toolFormTitle {
+ background: #eee;
+ border-color: #999;
+}
+
+div.toolFormDisabled {
+ border-color: #999;
+}
+
div.toolHelp
{
}
@@ -86,6 +95,13 @@ div.toolHelpBody
overflow: auto;
}
+div.titleRow {
+ font-weight: bold;
+ border-bottom: dotted gray 1px;
+ margin-bottom: 0.5em;
+ padding-bottom: 0.25em;
+}
+
/* Forms */
div.form-row
@@ -129,7 +145,7 @@ select, textarea, input[type="text"], input[type="file"]
/* Messages */
-.errormessage, .warningmessage, .donemessage, .infomessage, .welcomeBlue, .welcomeRed
+.errormessage, .warningmessage, .donemessage, .infomessage, .welcomemessage
{
padding: 10px;
padding-left: 52px;
@@ -162,20 +178,13 @@ select, textarea, input[type="text"], input[type="file"]
background-color: #CCCCFF;
}
-.welcomeBlue {
+.welcomemessage {
padding-left: 10px;
border-color: #6666AA;
background-color: #CCCCFF;
background-image: none;
}
-.welcomeRed {
- padding-left: 10px;
- border-color: #AA6666;
- background-color: #FFCCCC;
- background-image: none;
-}
-
.errormessagesmall, .warningmessagesmall, .donemessagesmall, .infomessagesmall
{
padding: 5px;
diff --git a/static/june_2007_style/blue/base_bg.png b/static/june_2007_style/blue/base_bg.png
index 2b1fa2e7f84..882f8d35971 100644
Binary files a/static/june_2007_style/blue/base_bg.png and b/static/june_2007_style/blue/base_bg.png differ
diff --git a/static/june_2007_style/blue/error_bg.png b/static/june_2007_style/blue/error_bg.png
index 355651aec82..f5c4edddef8 100644
Binary files a/static/june_2007_style/blue/error_bg.png and b/static/june_2007_style/blue/error_bg.png differ
diff --git a/static/june_2007_style/blue/footer_title_bg.png b/static/june_2007_style/blue/footer_title_bg.png
index fe2cf633d92..5b5bea63ca8 100644
Binary files a/static/june_2007_style/blue/footer_title_bg.png and b/static/june_2007_style/blue/footer_title_bg.png differ
diff --git a/static/june_2007_style/blue/form_body_bg.png b/static/june_2007_style/blue/form_body_bg.png
index 93799a55cb6..584f9a18132 100644
Binary files a/static/june_2007_style/blue/form_body_bg.png and b/static/june_2007_style/blue/form_body_bg.png differ
diff --git a/static/june_2007_style/blue/form_title_bg.png b/static/june_2007_style/blue/form_title_bg.png
index 6ad6a4d884b..f83638cfc88 100644
Binary files a/static/june_2007_style/blue/form_title_bg.png and b/static/june_2007_style/blue/form_title_bg.png differ
diff --git a/static/june_2007_style/blue/gray_bg.png b/static/june_2007_style/blue/gray_bg.png
index 67bfe6a1af7..ace3e79878b 100644
Binary files a/static/june_2007_style/blue/gray_bg.png and b/static/june_2007_style/blue/gray_bg.png differ
diff --git a/static/june_2007_style/blue/masthead_bg.png b/static/june_2007_style/blue/masthead_bg.png
index 2f1e47ac4ee..cd6ce134b44 100644
Binary files a/static/june_2007_style/blue/masthead_bg.png and b/static/june_2007_style/blue/masthead_bg.png differ
diff --git a/static/june_2007_style/blue/menu_bg.png b/static/june_2007_style/blue/menu_bg.png
index dc2ef23c4df..16c4d5f170f 100644
Binary files a/static/june_2007_style/blue/menu_bg.png and b/static/june_2007_style/blue/menu_bg.png differ
diff --git a/static/june_2007_style/blue/ok_bg.png b/static/june_2007_style/blue/ok_bg.png
index da05f23a96e..3ba07fa901c 100644
Binary files a/static/june_2007_style/blue/ok_bg.png and b/static/june_2007_style/blue/ok_bg.png differ
diff --git a/static/june_2007_style/blue/panel_header_bg.png b/static/june_2007_style/blue/panel_header_bg.png
index 8bad7f6c815..39f9520cb1f 100644
Binary files a/static/june_2007_style/blue/panel_header_bg.png and b/static/june_2007_style/blue/panel_header_bg.png differ
diff --git a/static/june_2007_style/blue/popupmenu_callout_top.png b/static/june_2007_style/blue/popupmenu_callout_top.png
index ec09a444ada..4318e8eea51 100644
Binary files a/static/june_2007_style/blue/popupmenu_callout_top.png and b/static/june_2007_style/blue/popupmenu_callout_top.png differ
diff --git a/static/june_2007_style/blue/warn_bg.png b/static/june_2007_style/blue/warn_bg.png
index 2d07665abe6..e621dc039a4 100644
Binary files a/static/june_2007_style/blue/warn_bg.png and b/static/june_2007_style/blue/warn_bg.png differ
diff --git a/templates/history/workflow_from_current_history.mako b/templates/history/workflow_from_current_history.mako
new file mode 100644
index 00000000000..e619719e688
--- /dev/null
+++ b/templates/history/workflow_from_current_history.mako
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+ ![]() |
+ %endif
+ + + | +
The following list contains each tool that was run to create the +datasets in your current history. Please select those that you wish +to include in the workflow.
+ +Tools which cannot be run interactively and thus cannot be incorporated +into a workflow will be shown in gray.
+ +%for warning in warnings: +