/lib/galaxy/web/controllers/workflow.py
Python | 1350 lines | 1324 code | 14 blank | 12 comment | 19 complexity | ef1eb098c8bcc8d79dd0e6cdbdb2407f MD5 | raw file
Large files files are truncated, but you can click here to view the full file
- from galaxy.web.base.controller import *
- import pkg_resources
- pkg_resources.require( "simplejson" )
- pkg_resources.require( "SVGFig" )
- import simplejson
- import base64, httplib, urllib2, sgmllib, svgfig
- import math
- from galaxy.web.framework.helpers import time_ago, grids
- from galaxy.tools.parameters import *
- 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.sanitize_html import sanitize_html
- from galaxy.util.topsort import topsort, topsort_levels, CycleError
- from galaxy.workflow.modules import *
- from galaxy import model
- from galaxy.model.mapping import desc
- from galaxy.model.orm import *
- from galaxy.model.item_attrs import *
- from galaxy.web.framework.helpers import to_unicode
- from galaxy.jobs.actions.post import ActionBox
- class StoredWorkflowListGrid( grids.Grid ):
- class StepsColumn( grids.GridColumn ):
- def get_value(self, trans, grid, workflow):
- return len( workflow.latest_workflow.steps )
-
- # Grid definition
- use_panels = True
- title = "Saved Workflows"
- model_class = model.StoredWorkflow
- default_filter = { "name" : "All", "tags": "All" }
- default_sort_key = "-update_time"
- columns = [
- grids.TextColumn( "Name", key="name", attach_popup=True, filterable="advanced" ),
- grids.IndividualTagsColumn( "Tags", "tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="StoredWorkflowListGrid" ),
- StepsColumn( "Steps" ),
- grids.GridColumn( "Created", key="create_time", format=time_ago ),
- grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
- ]
- columns.append(
- grids.MulticolFilterColumn(
- "Search",
- cols_to_filter=[ columns[0], columns[1] ],
- key="free-text-search", visible=False, filterable="standard" )
- )
- operations = [
- grids.GridOperation( "Edit", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ),
- grids.GridOperation( "Run", condition=( lambda item: not item.deleted ), async_compatible=False ),
- grids.GridOperation( "Clone", condition=( lambda item: not item.deleted ), async_compatible=False ),
- grids.GridOperation( "Rename", condition=( lambda item: not item.deleted ), async_compatible=False ),
- grids.GridOperation( "Sharing", condition=( lambda item: not item.deleted ), async_compatible=False ),
- grids.GridOperation( "Delete", condition=( lambda item: item.deleted ), async_compatible=True ),
- ]
- def apply_query_filter( self, trans, query, **kwargs ):
- return query.filter_by( user=trans.user, deleted=False )
- class StoredWorkflowAllPublishedGrid( grids.Grid ):
- title = "Published Workflows"
- model_class = model.StoredWorkflow
- default_sort_key = "update_time"
- default_filter = dict( public_url="All", username="All", tags="All" )
- use_async = True
- columns = [
- grids.PublicURLColumn( "Name", key="name", filterable="advanced" ),
- grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.StoredWorkflowAnnotationAssociation, filterable="advanced" ),
- grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced" ),
- grids.CommunityRatingColumn( "Community Rating", key="rating" ),
- grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="PublicWorkflowListGrid" ),
- grids.ReverseSortColumn( "Last Updated", key="update_time", format=time_ago )
- ]
- columns.append(
- grids.MulticolFilterColumn(
- "Search name, annotation, owner, and tags",
- cols_to_filter=[ columns[0], columns[1], columns[2], columns[4] ],
- key="free-text-search", visible=False, filterable="standard" )
- )
- operations = []
- def build_initial_query( self, trans, **kwargs ):
- # Join so that searching stored_workflow.user makes sense.
- return trans.sa_session.query( self.model_class ).join( model.User.table )
- def apply_query_filter( self, trans, query, **kwargs ):
- # A public workflow is published, has a slug, and is not deleted.
- return query.filter( self.model_class.published==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False )
-
- # Simple SGML parser to get all content in a single tag.
- class SingleTagContentsParser( sgmllib.SGMLParser ):
-
- def __init__( self, target_tag ):
- sgmllib.SGMLParser.__init__( self )
- self.target_tag = target_tag
- self.cur_tag = None
- self.tag_content = ""
-
- def unknown_starttag( self, tag, attrs ):
- """ Called for each start tag. """
- self.cur_tag = tag
-
- def handle_data( self, text ):
- """ Called for each block of plain text. """
- if self.cur_tag == self.target_tag:
- self.tag_content += text
- class WorkflowController( BaseUIController, Sharable, UsesStoredWorkflow, UsesAnnotations, UsesItemRatings ):
- stored_list_grid = StoredWorkflowListGrid()
- published_list_grid = StoredWorkflowAllPublishedGrid()
-
- __myexp_url = "www.myexperiment.org:80"
-
- @web.expose
- def index( self, trans ):
- return self.list( trans )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def list_grid( self, trans, **kwargs ):
- """ List user's stored workflows. """
- status = message = None
- if 'operation' in kwargs:
- operation = kwargs['operation'].lower()
- if operation == "rename":
- return self.rename( trans, **kwargs )
- history_ids = util.listify( kwargs.get( 'id', [] ) )
- if operation == "sharing":
- return self.sharing( trans, id=history_ids )
- return self.stored_list_grid( trans, **kwargs )
-
- @web.expose
- @web.require_login( "use Galaxy workflows", use_panels=True )
- def list( self, trans ):
- """
- Render workflow main page (management of existing workflows)
- """
- user = trans.get_user()
- workflows = trans.sa_session.query( model.StoredWorkflow ) \
- .filter_by( user=user, deleted=False ) \
- .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \
- .all()
- shared_by_others = trans.sa_session \
- .query( model.StoredWorkflowUserShareAssociation ) \
- .filter_by( user=user ) \
- .join( 'stored_workflow' ) \
- .filter( model.StoredWorkflow.deleted == False ) \
- .order_by( desc( model.StoredWorkflow.update_time ) ) \
- .all()
-
- # Legacy issue: all shared workflows must have slugs.
- slug_set = False
- for workflow_assoc in shared_by_others:
- slug_set = self.create_item_slug( trans.sa_session, workflow_assoc.stored_workflow )
- if slug_set:
- trans.sa_session.flush()
- return trans.fill_template( "workflow/list.mako",
- workflows = workflows,
- shared_by_others = shared_by_others )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def list_for_run( self, trans ):
- """
- Render workflow list for analysis view (just allows running workflow
- or switching to management view)
- """
- user = trans.get_user()
- workflows = trans.sa_session.query( model.StoredWorkflow ) \
- .filter_by( user=user, deleted=False ) \
- .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \
- .all()
- shared_by_others = trans.sa_session \
- .query( model.StoredWorkflowUserShareAssociation ) \
- .filter_by( user=user ) \
- .filter( model.StoredWorkflow.deleted == False ) \
- .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \
- .all()
- return trans.fill_template( "workflow/list_for_run.mako",
- workflows = workflows,
- shared_by_others = shared_by_others )
-
- @web.expose
- def list_published( self, trans, **kwargs ):
- grid = self.published_list_grid( trans, **kwargs )
- if 'async' in kwargs:
- return grid
- else:
- # Render grid wrapped in panels
- return trans.fill_template( "workflow/list_published.mako", grid=grid )
-
- @web.expose
- def display_by_username_and_slug( self, trans, username, slug ):
- """ Display workflow based on a username and slug. """
-
- # Get workflow.
- session = trans.sa_session
- user = session.query( model.User ).filter_by( username=username ).first()
- stored_workflow = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, slug=slug, deleted=False ).first()
- if stored_workflow is None:
- raise web.httpexceptions.HTTPNotFound()
- # Security check raises error if user cannot access workflow.
- self.security_check( trans, stored_workflow, False, True)
-
- # Get data for workflow's steps.
- self.get_stored_workflow_steps( trans, stored_workflow )
- # Get annotations.
- stored_workflow.annotation = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, stored_workflow )
- for step in stored_workflow.latest_workflow.steps:
- step.annotation = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, step )
- # Get rating data.
- user_item_rating = 0
- if trans.get_user():
- user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), stored_workflow )
- if user_item_rating:
- user_item_rating = user_item_rating.rating
- else:
- user_item_rating = 0
- ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, stored_workflow )
- return trans.fill_template_mako( "workflow/display.mako", item=stored_workflow, item_data=stored_workflow.latest_workflow.steps,
- user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings )
- @web.expose
- def get_item_content_async( self, trans, id ):
- """ Returns item content in HTML format. """
-
- stored = self.get_stored_workflow( trans, id, False, True )
- if stored is None:
- raise web.httpexceptions.HTTPNotFound()
-
- # Get data for workflow's steps.
- self.get_stored_workflow_steps( trans, stored )
- # Get annotations.
- stored.annotation = self.get_item_annotation_str( trans.sa_session, stored.user, stored )
- for step in stored.latest_workflow.steps:
- step.annotation = self.get_item_annotation_str( trans.sa_session, stored.user, step )
- return trans.stream_template_mako( "/workflow/item_content.mako", item = stored, item_data = stored.latest_workflow.steps )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def share( self, trans, id, email="", use_panels=False ):
- msg = mtype = None
- # Load workflow from database
- stored = self.get_stored_workflow( trans, id )
- if email:
- other = trans.sa_session.query( model.User ) \
- .filter( and_( model.User.table.c.email==email,
- model.User.table.c.deleted==False ) ) \
- .first()
- if not other:
- mtype = "error"
- msg = ( "User '%s' does not exist" % email )
- elif other == trans.get_user():
- mtype = "error"
- msg = ( "You cannot share a workflow with yourself" )
- elif trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \
- .filter_by( user=other, stored_workflow=stored ).count() > 0:
- mtype = "error"
- msg = ( "Workflow already shared with '%s'" % email )
- else:
- share = model.StoredWorkflowUserShareAssociation()
- share.stored_workflow = stored
- share.user = other
- session = trans.sa_session
- session.add( share )
- self.create_item_slug( session, stored )
- session.flush()
- trans.set_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) )
- return trans.response.send_redirect( url_for( controller='workflow', action='sharing', id=id ) )
- return trans.fill_template( "/ind_share_base.mako",
- message = msg,
- messagetype = mtype,
- item=stored,
- email=email,
- use_panels=use_panels )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def sharing( self, trans, id, **kwargs ):
- """ Handle workflow sharing. """
-
- # Get session and workflow.
- session = trans.sa_session
- stored = self.get_stored_workflow( trans, id )
- session.add( stored )
-
- # Do operation on workflow.
- if 'make_accessible_via_link' in kwargs:
- self._make_item_accessible( trans.sa_session, stored )
- elif 'make_accessible_and_publish' in kwargs:
- self._make_item_accessible( trans.sa_session, stored )
- stored.published = True
- elif 'publish' in kwargs:
- stored.published = True
- elif 'disable_link_access' in kwargs:
- stored.importable = False
- elif 'unpublish' in kwargs:
- stored.published = False
- elif 'disable_link_access_and_unpublish' in kwargs:
- stored.importable = stored.published = False
- elif 'unshare_user' in kwargs:
- user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) )
- if not user:
- error( "User not found for provided id" )
- association = session.query( model.StoredWorkflowUserShareAssociation ) \
- .filter_by( user=user, stored_workflow=stored ).one()
- session.delete( association )
-
- # Legacy issue: workflows made accessible before recent updates may not have a slug. Create slug for any workflows that need them.
- if stored.importable and not stored.slug:
- self._make_item_accessible( trans.sa_session, stored )
-
- session.flush()
-
- return trans.fill_template( "/workflow/sharing.mako", use_panels=True, item=stored )
- @web.expose
- @web.require_login( "to import a workflow", use_panels=True )
- def imp( self, trans, id, **kwargs ):
- # Set referer message.
- referer = trans.request.referer
- if referer is not "":
- referer_message = "<a href='%s'>return to the previous page</a>" % referer
- else:
- referer_message = "<a href='%s'>go to Galaxy's start page</a>" % url_for( '/' )
-
- # Do import.
- session = trans.sa_session
- stored = self.get_stored_workflow( trans, id, check_ownership=False )
- if stored.importable == False:
- return trans.show_error_message( "The owner of this workflow has disabled imports via this link.<br>You can %s" % referer_message, use_panels=True )
- elif stored.deleted:
- return trans.show_error_message( "You can't import this workflow because it has been deleted.<br>You can %s" % referer_message, use_panels=True )
- else:
- # Copy workflow.
- imported_stored = model.StoredWorkflow()
- imported_stored.name = "imported: " + stored.name
- imported_stored.latest_workflow = stored.latest_workflow
- imported_stored.user = trans.user
- # Save new workflow.
- session = trans.sa_session
- session.add( imported_stored )
- session.flush()
-
- # Copy annotations.
- self.copy_item_annotation( session, stored.user, stored, imported_stored.user, imported_stored )
- for order_index, step in enumerate( stored.latest_workflow.steps ):
- self.copy_item_annotation( session, stored.user, step, \
- imported_stored.user, imported_stored.latest_workflow.steps[order_index] )
- session.flush()
-
- # Redirect to load galaxy frames.
- return trans.show_ok_message(
- message="""Workflow "%s" has been imported. <br>You can <a href="%s">start using this workflow</a> or %s."""
- % ( stored.name, web.url_for( controller='workflow' ), referer_message ), use_panels=True )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def edit_attributes( self, trans, id, **kwargs ):
- # Get workflow and do error checking.
- 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.
- stored.name = sanitize_html( kwargs['name'] )
- if 'annotation' in kwargs:
- # Set workflow annotation; sanitize annotation before adding it.
- annotation = sanitize_html( kwargs[ 'annotation' ], 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), stored, annotation )
- trans.sa_session.flush()
- return trans.fill_template( 'workflow/edit_attributes.mako',
- stored=stored,
- annotation=self.get_item_annotation_str( trans.sa_session, trans.user, stored )
- )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def rename( self, trans, id, new_name=None, **kwargs ):
- stored = self.get_stored_workflow( trans, id )
- if new_name is not None:
- san_new_name = sanitize_html( new_name )
- stored.name = san_new_name
- stored.latest_workflow.name = san_new_name
- trans.sa_session.flush()
- # For current workflows grid:
- trans.set_message ( "Workflow renamed to '%s'." % new_name )
- return self.list( trans )
- # For new workflows grid:
- #message = "Workflow renamed to '%s'." % new_name
- #return self.list_grid( trans, message=message, status='done' )
- else:
- return form( url_for( action='rename', id=trans.security.encode_id(stored.id) ),
- "Rename workflow", submit_text="Rename", use_panels=True ) \
- .add_text( "new_name", "Workflow Name", value=to_unicode( stored.name ) )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def rename_async( self, trans, id, new_name=None, **kwargs ):
- stored = self.get_stored_workflow( trans, id )
- if new_name:
- san_new_name = sanitize_html( new_name )
- stored.name = san_new_name
- stored.latest_workflow.name = san_new_name
- trans.sa_session.flush()
- return stored.name
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def annotate_async( self, trans, id, new_annotation=None, **kwargs ):
- stored = self.get_stored_workflow( trans, id )
- if new_annotation:
- # Sanitize annotation before adding it.
- new_annotation = sanitize_html( new_annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), stored, new_annotation )
- trans.sa_session.flush()
- return new_annotation
-
- @web.expose
- @web.require_login( "rate items" )
- @web.json
- def rate_async( self, trans, id, rating ):
- """ Rate a workflow asynchronously and return updated community data. """
- stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True )
- if not stored:
- return trans.show_error_message( "The specified workflow does not exist." )
- # Rate workflow.
- stored_rating = self.rate_item( trans.sa_session, trans.get_user(), stored, rating )
- return self.get_ave_item_rating_data( trans.sa_session, stored )
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def set_accessible_async( self, trans, id=None, accessible=False ):
- """ Set workflow's importable attribute and slug. """
- stored = self.get_stored_workflow( trans, id )
- # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed.
- importable = accessible in ['True', 'true', 't', 'T'];
- if stored and stored.importable != importable:
- if importable:
- self._make_item_accessible( trans.sa_session, stored )
- else:
- stored.importable = importable
- trans.sa_session.flush()
- return
-
- @web.expose
- @web.require_login( "modify Galaxy items" )
- def set_slug_async( self, trans, id, new_slug ):
- stored = self.get_stored_workflow( trans, id )
- if stored:
- stored.slug = new_slug
- trans.sa_session.flush()
- return stored.slug
-
- @web.expose
- def get_embed_html_async( self, trans, id ):
- """ Returns HTML for embedding a workflow in a page. """
- # TODO: user should be able to embed any item he has access to. see display_by_username_and_slug for security code.
- stored = self.get_stored_workflow( trans, id )
- if stored:
- return "Embedded Workflow '%s'" % stored.name
-
- @web.expose
- @web.json
- @web.require_login( "use Galaxy workflows" )
- def get_name_and_link_async( self, trans, id=None ):
- """ Returns workflow's name and link. """
- stored = self.get_stored_workflow( trans, id )
- if self.create_item_slug( trans.sa_session, stored ):
- trans.sa_session.flush()
- return_dict = { "name" : stored.name, "link" : url_for( action="display_by_username_and_slug", username=stored.user.username, slug=stored.slug ) }
- return return_dict
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def gen_image( self, trans, id ):
- stored = self.get_stored_workflow( trans, id, check_ownership=True )
- session = trans.sa_session
-
- workflow = stored.latest_workflow
- data = []
-
- canvas = svgfig.canvas(style="stroke:black; fill:none; stroke-width:1px; stroke-linejoin:round; text-anchor:left")
- text = svgfig.SVG("g")
- connectors = svgfig.SVG("g")
- boxes = svgfig.SVG("g")
- svgfig.Text.defaults["font-size"] = "10px"
-
- in_pos = {}
- out_pos = {}
- margin = 5
- line_px = 16 # how much spacing between input/outputs
- widths = {} # store px width for boxes of each step
- max_width, max_x, max_y = 0, 0, 0
-
- for step in workflow.steps:
- # Load from database representation
- module = module_factory.from_workflow_step( trans, step )
-
- # Pack attributes into plain dictionary
- step_dict = {
- 'id': step.order_index,
- 'data_inputs': module.get_data_inputs(),
- 'data_outputs': module.get_data_outputs(),
- 'position': step.position
- }
-
- input_conn_dict = {}
- for conn in step.input_connections:
- input_conn_dict[ conn.input_name ] = \
- dict( id=conn.output_step.order_index, output_name=conn.output_name )
- step_dict['input_connections'] = input_conn_dict
-
- data.append(step_dict)
-
- x, y = step.position['left'], step.position['top']
- count = 0
-
- max_len = len(module.get_name()) * 1.5
- text.append( svgfig.Text(x, y + 20, module.get_name(), **{"font-size": "14px"} ).SVG() )
-
- y += 45
- for di in module.get_data_inputs():
- cur_y = y+count*line_px
- if step.order_index not in in_pos:
- in_pos[step.order_index] = {}
- in_pos[step.order_index][di['name']] = (x, cur_y)
- text.append( svgfig.Text(x, cur_y, di['label']).SVG() )
- count += 1
- max_len = max(max_len, len(di['label']))
-
-
- if len(module.get_data_inputs()) > 0:
- y += 15
-
- for do in module.get_data_outputs():
- cur_y = y+count*line_px
- if step.order_index not in out_pos:
- out_pos[step.order_index] = {}
- out_pos[step.order_index][do['name']] = (x, cur_y)
- text.append( svgfig.Text(x, cur_y, do['name']).SVG() )
- count += 1
- max_len = max(max_len, len(do['name']))
-
- widths[step.order_index] = max_len*5.5
- max_x = max(max_x, step.position['left'])
- max_y = max(max_y, step.position['top'])
- max_width = max(max_width, widths[step.order_index])
-
- for step_dict in data:
- width = widths[step_dict['id']]
- x, y = step_dict['position']['left'], step_dict['position']['top']
- boxes.append( svgfig.Rect(x-margin, y, x+width-margin, y+30, fill="#EBD9B2").SVG() )
- box_height = (len(step_dict['data_inputs']) + len(step_dict['data_outputs'])) * line_px + margin
- # Draw separator line
- if len(step_dict['data_inputs']) > 0:
- box_height += 15
- sep_y = y + len(step_dict['data_inputs']) * line_px + 40
- text.append( svgfig.Line(x-margin, sep_y, x+width-margin, sep_y).SVG() ) #
-
- # input/output box
- boxes.append( svgfig.Rect(x-margin, y+30, x+width-margin, y+30+box_height, fill="#ffffff").SVG() )
-
- for conn, output_dict in step_dict['input_connections'].iteritems():
- in_coords = in_pos[step_dict['id']][conn]
- out_conn_pos = out_pos[output_dict['id']][output_dict['output_name']]
- adjusted = (out_conn_pos[0] + widths[output_dict['id']], out_conn_pos[1])
- text.append( svgfig.SVG("circle", cx=out_conn_pos[0]+widths[output_dict['id']]-margin, cy=out_conn_pos[1]-margin, r=5, fill="#ffffff" ) )
- connectors.append( svgfig.Line(adjusted[0], adjusted[1]-margin, in_coords[0]-10, in_coords[1], arrow_end="true" ).SVG() )
-
- canvas.append(connectors)
- canvas.append(boxes)
- canvas.append(text)
- width, height = (max_x + max_width + 50), max_y + 300
- canvas['width'] = "%s px" % width
- canvas['height'] = "%s px" % height
- canvas['viewBox'] = "0 0 %s %s" % (width, height)
- trans.response.set_content_type("image/svg+xml")
- return canvas.standalone_xml()
-
-
- @web.expose
- @web.require_login( "use Galaxy workflows" )
- def clone( self, trans, id ):
- # Get workflow to clone.
- stored = self.get_stored_workflow( trans, id, check_ownership=False )
- user = trans.get_user()
- if stored.user == user:
- owner = True
- else:
- 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" )
- owner = False
-
- # Clone.
- new_stored = model.StoredWorkflow()
- new_stored.name = "Clone of '%s'" % stored.name
- new_stored.latest_workflow = stored.latest_workflow
- # Clone annotation.
- annotation_obj = self.get_item_annotation_obj( trans.sa_session, stored.user, stored )
- if annotation_obj:
- self.add_item_annotation( trans.sa_session, trans.get_user(), new_stored, annotation_obj.annotation )
- # Clone tags.
- for swta in stored.owner_tags:
- new_swta = model.StoredWorkflowTagAssociation()
- new_swta.tag = swta.tag
- new_swta.user = trans.user
- new_swta.user_tname = swta.user_tname
- new_swta.user_value = swta.user_value
- new_swta.value = swta.value
- new_stored.tags.append( new_swta )
- if not owner:
- new_stored.name += " shared by '%s'" % stored.user.email
- new_stored.user = user
- # Persist
- session = trans.sa_session
- session.add( new_stored )
- session.flush()
- # Display the management page
- trans.set_message( 'Clone created with name "%s"' % new_stored.name )
- return self.list( trans )
-
- @web.expose
- @web.require_login( "create workflows" )
- def create( self, trans, workflow_name=None, workflow_annotation="" ):
- """
- Create a new stored workflow with name `workflow_name`.
- """
- user = trans.get_user()
- if workflow_name is not None:
- # Create the new stored workflow
- stored_workflow = model.StoredWorkflow()
- stored_workflow.name = workflow_name
- stored_workflow.user = user
- # And the first (empty) workflow revision
- workflow = model.Workflow()
- workflow.name = workflow_name
- workflow.stored_workflow = stored_workflow
- stored_workflow.latest_workflow = workflow
- # Add annotation.
- workflow_annotation = sanitize_html( workflow_annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), stored_workflow, workflow_annotation )
- # Persist
- session = trans.sa_session
- session.add( stored_workflow )
- session.flush()
- # Display the management page
- trans.set_message( "Workflow '%s' created" % stored_workflow.name )
- return self.list( trans )
- else:
- return form( url_for(), "Create New Workflow", submit_text="Create", use_panels=True ) \
- .add_text( "workflow_name", "Workflow Name", value="Unnamed workflow" ) \
- .add_text( "workflow_annotation", "Workflow Annotation", value="", help="A description of the workflow; annotation is shown alongside shared or published workflows." )
-
- @web.expose
- def delete( self, trans, id=None ):
- """
- Mark a workflow as deleted
- """
- # Load workflow from database
- stored = self.get_stored_workflow( trans, id )
- # Marke as deleted and save
- stored.deleted = True
- trans.sa_session.add( stored )
- trans.sa_session.flush()
- # Display the management page
- trans.set_message( "Workflow '%s' deleted" % stored.name )
- return self.list( trans )
-
- @web.expose
- @web.require_login( "edit workflows" )
- def editor( self, trans, id=None ):
- """
- Render the main workflow editor interface. The canvas is embedded as
- an iframe (necessary for scrolling to work properly), which is
- rendered by `editor_canvas`.
- """
- if not id:
- error( "Invalid workflow id" )
- stored = self.get_stored_workflow( trans, id )
- return trans.fill_template( "workflow/editor.mako", stored=stored, annotation=self.get_item_annotation_str( trans.sa_session, trans.user, stored ) )
-
- @web.json
- def editor_form_post( self, trans, type='tool', tool_id=None, annotation=None, **incoming ):
- """
- Accepts a tool state and incoming values, and generates a new tool
- form and some additional information, packed into a json dictionary.
- This is used for the form shown in the right pane when a node
- is selected.
- """
-
- trans.workflow_building_mode = True
- module = module_factory.from_dict( trans, {
- 'type': type,
- 'tool_id': tool_id,
- 'tool_state': incoming.pop("tool_state")
- } )
- module.update_state( incoming )
-
- if type=='tool':
- return {
- 'tool_state': module.get_state(),
- 'data_inputs': module.get_data_inputs(),
- 'data_outputs': module.get_data_outputs(),
- 'tool_errors': module.get_errors(),
- 'form_html': module.get_config_form(),
- 'annotation': annotation,
- 'post_job_actions': module.get_post_job_actions()
- }
- else:
- return {
- 'tool_state': module.get_state(),
- 'data_inputs': module.get_data_inputs(),
- 'data_outputs': module.get_data_outputs(),
- 'tool_errors': module.get_errors(),
- 'form_html': module.get_config_form(),
- 'annotation': annotation
- }
-
- @web.json
- def get_new_module_info( self, trans, type, **kwargs ):
- """
- Get the info for a new instance of a module initialized with default
- parameters (any keyword arguments will be passed along to the module).
- Result includes data inputs and outputs, html representation
- of the initial form, and the initial tool state (with default values).
- This is called asynchronously whenever a new node is added.
- """
- trans.workflow_building_mode = True
- module = module_factory.new( trans, type, **kwargs )
- return {
- 'type': module.type,
- 'name': module.get_name(),
- 'tool_id': module.get_tool_id(),
- 'tool_state': module.get_state(),
- 'tooltip': module.get_tooltip(),
- 'data_inputs': module.get_data_inputs(),
- 'data_outputs': module.get_data_outputs(),
- 'form_html': module.get_config_form(),
- 'annotation': ""
- }
- @web.json
- def load_workflow( self, trans, id ):
- """
- Get the latest Workflow for the StoredWorkflow identified by `id` and
- encode it as a json string that can be read by the workflow editor
- web interface.
- """
- user = trans.get_user()
- id = trans.security.decode_id( id )
- trans.workflow_building_mode = True
- # Load encoded workflow from database
- stored = trans.sa_session.query( model.StoredWorkflow ).get( id )
- assert stored.user == user
- workflow = stored.latest_workflow
- # Pack workflow data into a dictionary and return
- data = {}
- data['name'] = workflow.name
- data['steps'] = {}
- data['upgrade_messages'] = {}
- # For each step, rebuild the form and encode the state
- for step in workflow.steps:
- # Load from database representation
- module = module_factory.from_workflow_step( trans, step )
- if not module:
- step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
- annotation_str = ""
- if step_annotation:
- annotation_str = step_annotation.annotation
- invalid_tool_form_html = """<div class="toolForm tool-node-error"><div class="toolFormTitle form-row-error">Unrecognized Tool: %s</div><div class="toolFormBody"><div class="form-row">
- The tool id '%s' for this tool is unrecognized.<br/><br/>To save this workflow, you will need to delete this step or enable the tool.
- </div></div></div>""" % (step.tool_id, step.tool_id)
- step_dict = {
- 'id': step.order_index,
- 'type': 'invalid',
- 'tool_id': step.tool_id,
- 'name': 'Unrecognized Tool: %s' % step.tool_id,
- 'tool_state': None,
- 'tooltip': None,
- 'tool_errors': ["Unrecognized Tool Id: %s" % step.tool_id],
- 'data_inputs': [],
- 'data_outputs': [],
- 'form_html': invalid_tool_form_html,
- 'annotation' : annotation_str,
- 'post_job_actions' : {},
- 'workflow_outputs' : []
- }
- step_dict['input_connections'] = input_conn_dict
- # Position
- step_dict['position'] = step.position
- # Add to return value
- data['steps'][step.order_index] = step_dict
- continue
- # Fix any missing parameters
- upgrade_message = module.check_and_update_state()
- if upgrade_message:
- # FIXME: Frontend should be able to handle workflow messages
- # as a dictionary not just the values
- data['upgrade_messages'][step.order_index] = upgrade_message.values()
- # Get user annotation.
- step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
- annotation_str = ""
- if step_annotation:
- annotation_str = step_annotation.annotation
- # Pack attributes into plain dictionary
- step_dict = {
- 'id': step.order_index,
- 'type': module.type,
- 'tool_id': module.get_tool_id(),
- 'name': module.get_name(),
- 'tool_state': module.get_state(),
- 'tooltip': module.get_tooltip(),
- 'tool_errors': module.get_errors(),
- 'data_inputs': module.get_data_inputs(),
- 'data_outputs': module.get_data_outputs(),
- 'form_html': module.get_config_form(),
- 'annotation' : annotation_str,
- 'post_job_actions' : {},
- 'workflow_outputs' : []
- }
- # Connections
- input_connections = step.input_connections
- if step.type is None or step.type == 'tool':
- # Determine full (prefixed) names of valid input datasets
- data_input_names = {}
- def callback( input, value, prefixed_name, prefixed_label ):
- if isinstance( input, DataToolParameter ):
- data_input_names[ prefixed_name ] = True
- visit_input_values( module.tool.inputs, module.state.inputs, callback )
- # Filter
- # FIXME: this removes connection without displaying a message currently!
- input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
- # post_job_actions
- pja_dict = {}
- for pja in step.post_job_actions:
- pja_dict[pja.action_type+pja.output_name] = dict(action_type = pja.action_type,
- 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:
- input_conn_dict[ conn.input_name ] = \
- dict( id=conn.output_step.order_index, output_name=conn.output_name )
- step_dict['input_connections'] = input_conn_dict
- # Position
- step_dict['position'] = step.position
- # Add to return value
- data['steps'][step.order_index] = step_dict
- return data
- @web.json
- def save_workflow( self, trans, id, workflow_data ):
- """
- Save the workflow described by `workflow_data` with id `id`.
- """
- # Get the stored workflow
- stored = self.get_stored_workflow( trans, id )
- # Put parameters in workflow mode
- trans.workflow_building_mode = True
- # Convert incoming workflow data from json
- data = simplejson.loads( workflow_data )
- # Create new workflow from incoming data
- workflow = model.Workflow()
- # Just keep the last name (user can rename later)
- workflow.name = stored.name
- # Assume no errors until we find a step that has some
- workflow.has_errors = False
- # Create each step
- steps = []
- # The editor will provide ids for each step that we don't need to save,
- # but do need to use to make connections
- steps_by_external_id = {}
- errors = []
- for key, step_dict in data['steps'].iteritems():
- if step_dict['type'] != 'data_input' and step_dict['tool_id'] not in trans.app.toolbox.tools_by_id:
- errors.append("Step %s requires tool '%s'." % (step_dict['id'], step_dict['tool_id']))
- if errors:
- return dict( name=workflow.name,
- message="This workflow includes missing or invalid tools. It cannot be saved until the following steps are removed or the missing tools are enabled.",
- errors=errors)
- # First pass to build step objects and populate basic values
- for key, step_dict in data['steps'].iteritems():
- # Create the model class for the step
- step = model.WorkflowStep()
- steps.append( step )
- steps_by_external_id[ step_dict['id' ] ] = step
- # FIXME: Position should be handled inside module
- 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:
- # DBTODO Check for conditional inputs here.
- workflow.has_errors = True
- # Stick this in the step temporarily
- step.temp_input_connections = step_dict['input_connections']
- # Save step annotation.
- annotation = step_dict[ 'annotation' ]
- if annotation:
- annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
- self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
- # Second pass to deal with connections between steps
- for step in steps:
- # Input connections
- for input_name, conn_dict in step.temp_input_connections.iteritems():
- if conn_dict:
- conn = model.WorkflowStepConnection()
- conn.input_step = step
- conn.input_name = input_name
- conn.output_name = conn_dict['output_name']
- conn.output_step = steps_by_external_id[ conn_dict['id'] ]
- del step.temp_input_connections
- # Order the steps if possible
- attach_ordered_steps( workflow, steps )
- # Connect up
- workflow.stored_workflow = stored
- stored.latest_workflow = workflow
- # Persist
- trans.sa_session.flush()
- # Return something informative
- errors = []
- if workflow.has_errors:
- errors.append( "Some steps in this workflow have validation errors" )
- if workflow.has_cycles:
- errors.append( "This workflow contains cycles" )
- if errors:
- rval = dict( message="Workflow saved, but will not be runnable due to the following errors",
- errors=errors )
- else:
- rval = dict( message="Workflow saved" )
- rval['name'] = workflow.name
- return rval
-
- @web.expose
- @web.require_login( "use workflows" )
- def export( self, trans, id=None, **kwd ):
- """
- Handles download/export workflow command.
- """
- stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True )
- return trans.fill_template( "/workflow/export.mako", item=stored, use_panels=True )
-
-
- @web.expose
- @web.require_login( "use workflows" )
- def import_from_myexp( self, trans, myexp_id, myexp_username=None, myexp_password=None ):
- """
- Imports a workflow from the myExperiment website.
- """
-
- #
- # Get workflow XML.
- #
-
- # Get workflow content.
- conn = httplib.HTTPConnection( self.__myexp_url )
- # NOTE: blocks web thread.
- headers = {}
- if myexp_username and myexp_password:
- auth_header = base64.b64encode( '%s:%s' % ( myexp_username, myexp_password ))
- headers = { "Authorization" : "Basic %s" % auth_header }
- conn.request( "GET", "/workflow.xml?id=%s&elements=content" % myexp_id, headers=headers )
- response = conn.getresponse()
- workflow_xml = response.read()
- conn.close()
- parser = SingleTagContentsParser( "content" )
- parser.feed( workflow_xml )
- workflow_content = base64.b64decode( parser.tag_content )
-
- #
- # Process workflow XML and create workflow.
- #
- parser = SingleTagContentsParser( "galaxy_json" )
- parser.feed( workflow_content )
- workflow_dict = from_json_string( parser.tag_content )
-
- # Create workflow.
- workflow = self._workflow_from_dict( trans, workflow_dict, source="myExperiment" ).latest_workflow
-
- # Provide user feedback.
- if workflow.has_errors:
- return trans.show_warn_message( "Imported, but some steps in this workflow have validation errors" )
- if workflow.has_cycles:
- return trans.show_warn_message( "Imported, but this workflow contains cycles" )
- else:
- return trans.show_message( "Workflow '%s' imported" % workflow.name )
-
- @web.expose
- @web.require_login( "use workflows" )
- def export_to_myexp( self, trans, id, myexp_username, myexp_password ):
- """
- Exports a workflow to myExperiment website.
- """
-
- # Load encoded workflow from database
- user = trans.get_user()
- id = trans.security.decode_id( id )
- trans.workflow_building_mode = True
- stored = trans.sa_session.query( model.StoredWorkflow ).get( id )
- self.security_check( trans, stored, False, True )
-
- # Convert workflow to dict.
- workflow_dict = self._workflow_to_dict( trans, stored )
-
- #
- # Create and submit workflow myExperiment request.
- #
-
- # Create workflow content XML.
- workflow_dict_packed = simplejson.dumps( workflow_dict, indent=4, sort_keys=True )
- workflow_content = trans.fill_template( "workflow/myexp_export_content.mako", \
- workflow_dict_packed=workflow_dict_packed, \
- workflow_steps=workflow_dict['steps'] )
-
- # Create myExperiment request.
- request_raw = trans.fill_template( "workflow/myexp_export.mako", \
- workflow_name=workflow_dict['name'], \
- workflow_description=workflow_dict['annotation'], \
- workflow_content=workflow_content
- )
- # strip() b/c myExperiment XML parser doesn't allow white space before XML; utf-8 handles unicode characters.
- request = unicode( request_raw.strip(), 'utf-8' )
-
- # Do request and get result.
- auth_header = base64.b64encode( '%s:%s' % ( myexp_username, myexp_password ))
- headers = { "Content-type": "text/xml", "Accept": "text/xml", "Authorization" : "Basic %s" % auth_header }
- conn = httplib.HTTPConnection( self.__myexp_url )
- # NOTE: blocks web thread.
- conn.request("POST", "/workflow.xml", request, headers)
- response = conn.getresponse()
- response_data = response.read()
- conn.close()
-
- # Do simple parse of response to see if export successful and provide user feedback.
- parser = SingleTagContentsParser( 'id' )
- parser.feed( response_data )
- myexp_workflow_id = pa…
Large files files are truncated, but you can click here to view the full file