PageRenderTime 106ms CodeModel.GetById 16ms app.highlight 78ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/galaxy/web/controllers/workflow.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 1350 lines | 1324 code | 14 blank | 12 comment | 37 complexity | ef1eb098c8bcc8d79dd0e6cdbdb2407f MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1from galaxy.web.base.controller import *
   2
   3import pkg_resources
   4pkg_resources.require( "simplejson" )
   5pkg_resources.require( "SVGFig" )
   6import simplejson
   7import base64, httplib, urllib2, sgmllib, svgfig
   8import math
   9from galaxy.web.framework.helpers import time_ago, grids
  10from galaxy.tools.parameters import *
  11from galaxy.tools import DefaultToolState
  12from galaxy.tools.parameters.grouping import Repeat, Conditional
  13from galaxy.datatypes.data import Data
  14from galaxy.util.odict import odict
  15from galaxy.util.sanitize_html import sanitize_html
  16from galaxy.util.topsort import topsort, topsort_levels, CycleError
  17from galaxy.workflow.modules import *
  18from galaxy import model
  19from galaxy.model.mapping import desc
  20from galaxy.model.orm import *
  21from galaxy.model.item_attrs import *
  22from galaxy.web.framework.helpers import to_unicode
  23from galaxy.jobs.actions.post import ActionBox
  24
  25class StoredWorkflowListGrid( grids.Grid ):    
  26    class StepsColumn( grids.GridColumn ):
  27        def get_value(self, trans, grid, workflow):
  28            return len( workflow.latest_workflow.steps )
  29    
  30    # Grid definition
  31    use_panels = True
  32    title = "Saved Workflows"
  33    model_class = model.StoredWorkflow
  34    default_filter = { "name" : "All", "tags": "All" }
  35    default_sort_key = "-update_time"
  36    columns = [
  37        grids.TextColumn( "Name", key="name", attach_popup=True, filterable="advanced" ),
  38        grids.IndividualTagsColumn( "Tags", "tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="StoredWorkflowListGrid" ),
  39        StepsColumn( "Steps" ),
  40        grids.GridColumn( "Created", key="create_time", format=time_ago ),
  41        grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
  42    ]
  43    columns.append( 
  44        grids.MulticolFilterColumn(  
  45        "Search", 
  46        cols_to_filter=[ columns[0], columns[1] ], 
  47        key="free-text-search", visible=False, filterable="standard" )
  48                )
  49    operations = [
  50        grids.GridOperation( "Edit", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ),
  51        grids.GridOperation( "Run", condition=( lambda item: not item.deleted ), async_compatible=False ),
  52        grids.GridOperation( "Clone", condition=( lambda item: not item.deleted ), async_compatible=False  ),
  53        grids.GridOperation( "Rename", condition=( lambda item: not item.deleted ), async_compatible=False  ),
  54        grids.GridOperation( "Sharing", condition=( lambda item: not item.deleted ), async_compatible=False ),
  55        grids.GridOperation( "Delete", condition=( lambda item: item.deleted ), async_compatible=True ),
  56    ]
  57    def apply_query_filter( self, trans, query, **kwargs ):
  58        return query.filter_by( user=trans.user, deleted=False )
  59
  60class StoredWorkflowAllPublishedGrid( grids.Grid ):
  61    title = "Published Workflows"
  62    model_class = model.StoredWorkflow
  63    default_sort_key = "update_time"
  64    default_filter = dict( public_url="All", username="All", tags="All" )
  65    use_async = True
  66    columns = [
  67        grids.PublicURLColumn( "Name", key="name", filterable="advanced" ),
  68        grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.StoredWorkflowAnnotationAssociation, filterable="advanced" ),
  69        grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced" ),
  70        grids.CommunityRatingColumn( "Community Rating", key="rating" ), 
  71        grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="PublicWorkflowListGrid" ),
  72        grids.ReverseSortColumn( "Last Updated", key="update_time", format=time_ago )
  73    ]
  74    columns.append( 
  75        grids.MulticolFilterColumn(  
  76        "Search name, annotation, owner, and tags", 
  77        cols_to_filter=[ columns[0], columns[1], columns[2], columns[4] ], 
  78        key="free-text-search", visible=False, filterable="standard" )
  79                )
  80    operations = []
  81    def build_initial_query( self, trans, **kwargs ):
  82        # Join so that searching stored_workflow.user makes sense.
  83        return trans.sa_session.query( self.model_class ).join( model.User.table )
  84    def apply_query_filter( self, trans, query, **kwargs ):
  85        # A public workflow is published, has a slug, and is not deleted.
  86        return query.filter( self.model_class.published==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False )
  87        
  88# Simple SGML parser to get all content in a single tag.
  89class SingleTagContentsParser( sgmllib.SGMLParser ):
  90    
  91    def __init__( self, target_tag ):
  92        sgmllib.SGMLParser.__init__( self )
  93        self.target_tag = target_tag
  94        self.cur_tag = None
  95        self.tag_content = ""
  96        
  97    def unknown_starttag( self, tag, attrs ):
  98        """ Called for each start tag. """
  99        self.cur_tag = tag
 100        
 101    def handle_data( self, text ):
 102        """ Called for each block of plain text. """
 103        if self.cur_tag == self.target_tag:
 104            self.tag_content += text
 105
 106class WorkflowController( BaseUIController, Sharable, UsesStoredWorkflow, UsesAnnotations, UsesItemRatings ):
 107    stored_list_grid = StoredWorkflowListGrid()
 108    published_list_grid = StoredWorkflowAllPublishedGrid()
 109    
 110    __myexp_url = "www.myexperiment.org:80"
 111    
 112    @web.expose
 113    def index( self, trans ):
 114        return self.list( trans )
 115        
 116    @web.expose
 117    @web.require_login( "use Galaxy workflows" )
 118    def list_grid( self, trans, **kwargs ):
 119        """ List user's stored workflows. """
 120        status = message = None
 121        if 'operation' in kwargs:
 122            operation = kwargs['operation'].lower()
 123            if operation == "rename":
 124                return self.rename( trans, **kwargs )
 125            history_ids = util.listify( kwargs.get( 'id', [] ) )
 126            if operation == "sharing":
 127                return self.sharing( trans, id=history_ids )
 128        return self.stored_list_grid( trans, **kwargs )
 129                                   
 130    @web.expose
 131    @web.require_login( "use Galaxy workflows", use_panels=True )
 132    def list( self, trans ):
 133        """
 134        Render workflow main page (management of existing workflows)
 135        """
 136        user = trans.get_user()
 137        workflows = trans.sa_session.query( model.StoredWorkflow ) \
 138            .filter_by( user=user, deleted=False ) \
 139            .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \
 140            .all()
 141        shared_by_others = trans.sa_session \
 142            .query( model.StoredWorkflowUserShareAssociation ) \
 143            .filter_by( user=user ) \
 144            .join( 'stored_workflow' ) \
 145            .filter( model.StoredWorkflow.deleted == False ) \
 146            .order_by( desc( model.StoredWorkflow.update_time ) ) \
 147            .all()
 148            
 149        # Legacy issue: all shared workflows must have slugs.
 150        slug_set = False
 151        for workflow_assoc in shared_by_others:
 152            slug_set = self.create_item_slug( trans.sa_session, workflow_assoc.stored_workflow )
 153        if slug_set:
 154            trans.sa_session.flush()
 155
 156        return trans.fill_template( "workflow/list.mako",
 157                                    workflows = workflows,
 158                                    shared_by_others = shared_by_others )
 159    
 160    @web.expose
 161    @web.require_login( "use Galaxy workflows" )
 162    def list_for_run( self, trans ):
 163        """
 164        Render workflow list for analysis view (just allows running workflow
 165        or switching to management view)
 166        """
 167        user = trans.get_user()
 168        workflows = trans.sa_session.query( model.StoredWorkflow ) \
 169            .filter_by( user=user, deleted=False ) \
 170            .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \
 171            .all()
 172        shared_by_others = trans.sa_session \
 173            .query( model.StoredWorkflowUserShareAssociation ) \
 174            .filter_by( user=user ) \
 175            .filter( model.StoredWorkflow.deleted == False ) \
 176            .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \
 177            .all()
 178        return trans.fill_template( "workflow/list_for_run.mako",
 179                                    workflows = workflows,
 180                                    shared_by_others = shared_by_others )
 181                                    
 182    @web.expose
 183    def list_published( self, trans, **kwargs ):
 184        grid = self.published_list_grid( trans, **kwargs )
 185        if 'async' in kwargs:
 186            return grid
 187        else:
 188            # Render grid wrapped in panels
 189            return trans.fill_template( "workflow/list_published.mako", grid=grid )
 190                                    
 191    @web.expose
 192    def display_by_username_and_slug( self, trans, username, slug ):
 193        """ Display workflow based on a username and slug. """ 
 194        
 195        # Get workflow.
 196        session = trans.sa_session
 197        user = session.query( model.User ).filter_by( username=username ).first()
 198        stored_workflow = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, slug=slug, deleted=False ).first()
 199        if stored_workflow is None:
 200           raise web.httpexceptions.HTTPNotFound()
 201        # Security check raises error if user cannot access workflow.
 202        self.security_check( trans, stored_workflow, False, True)
 203        
 204        # Get data for workflow's steps.
 205        self.get_stored_workflow_steps( trans, stored_workflow )
 206        # Get annotations.
 207        stored_workflow.annotation = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, stored_workflow )
 208        for step in stored_workflow.latest_workflow.steps:
 209            step.annotation = self.get_item_annotation_str( trans.sa_session, stored_workflow.user, step )
 210
 211        # Get rating data.
 212        user_item_rating = 0
 213        if trans.get_user():
 214            user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), stored_workflow )
 215            if user_item_rating:
 216                user_item_rating = user_item_rating.rating
 217            else:
 218                user_item_rating = 0
 219        ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, stored_workflow )            
 220        return trans.fill_template_mako( "workflow/display.mako", item=stored_workflow, item_data=stored_workflow.latest_workflow.steps,
 221                                            user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings )
 222
 223    @web.expose
 224    def get_item_content_async( self, trans, id ):
 225        """ Returns item content in HTML format. """
 226        
 227        stored = self.get_stored_workflow( trans, id, False, True )
 228        if stored is None:
 229            raise web.httpexceptions.HTTPNotFound()
 230            
 231        # Get data for workflow's steps.
 232        self.get_stored_workflow_steps( trans, stored )
 233        # Get annotations.
 234        stored.annotation = self.get_item_annotation_str( trans.sa_session, stored.user, stored )
 235        for step in stored.latest_workflow.steps:
 236            step.annotation = self.get_item_annotation_str( trans.sa_session, stored.user, step )
 237        return trans.stream_template_mako( "/workflow/item_content.mako", item = stored, item_data = stored.latest_workflow.steps )
 238                              
 239    @web.expose
 240    @web.require_login( "use Galaxy workflows" )
 241    def share( self, trans, id, email="", use_panels=False ):
 242        msg = mtype = None
 243        # Load workflow from database
 244        stored = self.get_stored_workflow( trans, id )
 245        if email:
 246            other = trans.sa_session.query( model.User ) \
 247                                    .filter( and_( model.User.table.c.email==email,
 248                                                   model.User.table.c.deleted==False ) ) \
 249                                    .first()
 250            if not other:
 251                mtype = "error"
 252                msg = ( "User '%s' does not exist" % email )
 253            elif other == trans.get_user():
 254                mtype = "error"
 255                msg = ( "You cannot share a workflow with yourself" )
 256            elif trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \
 257                    .filter_by( user=other, stored_workflow=stored ).count() > 0:
 258                mtype = "error"
 259                msg = ( "Workflow already shared with '%s'" % email )
 260            else:
 261                share = model.StoredWorkflowUserShareAssociation()
 262                share.stored_workflow = stored
 263                share.user = other
 264                session = trans.sa_session
 265                session.add( share )
 266                self.create_item_slug( session, stored )
 267                session.flush()
 268                trans.set_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) )
 269                return trans.response.send_redirect( url_for( controller='workflow', action='sharing', id=id ) )
 270        return trans.fill_template( "/ind_share_base.mako",
 271                                    message = msg,
 272                                    messagetype = mtype,
 273                                    item=stored,
 274                                    email=email,
 275                                    use_panels=use_panels )
 276    
 277    @web.expose
 278    @web.require_login( "use Galaxy workflows" )
 279    def sharing( self, trans, id, **kwargs ):
 280        """ Handle workflow sharing. """
 281        
 282        # Get session and workflow.
 283        session = trans.sa_session
 284        stored = self.get_stored_workflow( trans, id )
 285        session.add( stored )
 286        
 287        # Do operation on workflow.
 288        if 'make_accessible_via_link' in kwargs:
 289            self._make_item_accessible( trans.sa_session, stored )
 290        elif 'make_accessible_and_publish' in kwargs:
 291            self._make_item_accessible( trans.sa_session, stored )
 292            stored.published = True
 293        elif 'publish' in kwargs:
 294            stored.published = True
 295        elif 'disable_link_access' in kwargs:
 296            stored.importable = False
 297        elif 'unpublish' in kwargs:
 298            stored.published = False
 299        elif 'disable_link_access_and_unpublish' in kwargs:
 300            stored.importable = stored.published = False
 301        elif 'unshare_user' in kwargs:
 302            user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) )
 303            if not user:
 304                error( "User not found for provided id" )
 305            association = session.query( model.StoredWorkflowUserShareAssociation ) \
 306                                 .filter_by( user=user, stored_workflow=stored ).one()
 307            session.delete( association )
 308            
 309        # Legacy issue: workflows made accessible before recent updates may not have a slug. Create slug for any workflows that need them.
 310        if stored.importable and not stored.slug:
 311            self._make_item_accessible( trans.sa_session, stored )
 312            
 313        session.flush()
 314        
 315        return trans.fill_template( "/workflow/sharing.mako", use_panels=True, item=stored )
 316
 317    @web.expose
 318    @web.require_login( "to import a workflow", use_panels=True )
 319    def imp( self, trans, id, **kwargs ):
 320        # Set referer message.
 321        referer = trans.request.referer
 322        if referer is not "":
 323            referer_message = "<a href='%s'>return to the previous page</a>" % referer
 324        else:
 325            referer_message = "<a href='%s'>go to Galaxy's start page</a>" % url_for( '/' )
 326                    
 327        # Do import.
 328        session = trans.sa_session
 329        stored = self.get_stored_workflow( trans, id, check_ownership=False )
 330        if stored.importable == False:
 331            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 )
 332        elif stored.deleted:
 333            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 )
 334        else:
 335            # Copy workflow.
 336            imported_stored = model.StoredWorkflow()
 337            imported_stored.name = "imported: " + stored.name
 338            imported_stored.latest_workflow = stored.latest_workflow
 339            imported_stored.user = trans.user
 340            # Save new workflow.
 341            session = trans.sa_session
 342            session.add( imported_stored )
 343            session.flush()
 344            
 345            # Copy annotations.
 346            self.copy_item_annotation( session, stored.user, stored, imported_stored.user, imported_stored )
 347            for order_index, step in enumerate( stored.latest_workflow.steps ):
 348                self.copy_item_annotation( session, stored.user, step, \
 349                                            imported_stored.user, imported_stored.latest_workflow.steps[order_index] )
 350            session.flush()
 351            
 352            # Redirect to load galaxy frames.
 353            return trans.show_ok_message(
 354                message="""Workflow "%s" has been imported. <br>You can <a href="%s">start using this workflow</a> or %s.""" 
 355                % ( stored.name, web.url_for( controller='workflow' ), referer_message ), use_panels=True )
 356            
 357    @web.expose
 358    @web.require_login( "use Galaxy workflows" )
 359    def edit_attributes( self, trans, id, **kwargs ):
 360        # Get workflow and do error checking.
 361        stored = self.get_stored_workflow( trans, id )
 362        if not stored:
 363            error( "You do not own this workflow or workflow ID is invalid." )
 364        # Update workflow attributes if new values submitted.
 365        if 'name' in kwargs:
 366            # Rename workflow.
 367            stored.name = sanitize_html( kwargs['name'] )
 368        if 'annotation' in kwargs:
 369            # Set workflow annotation; sanitize annotation before adding it.
 370            annotation = sanitize_html( kwargs[ 'annotation' ], 'utf-8', 'text/html' )
 371            self.add_item_annotation( trans.sa_session, trans.get_user(), stored,  annotation )
 372        trans.sa_session.flush()
 373        return trans.fill_template( 'workflow/edit_attributes.mako', 
 374                                    stored=stored, 
 375                                    annotation=self.get_item_annotation_str( trans.sa_session, trans.user, stored ) 
 376                                    )
 377    
 378    @web.expose
 379    @web.require_login( "use Galaxy workflows" )
 380    def rename( self, trans, id, new_name=None, **kwargs ):
 381        stored = self.get_stored_workflow( trans, id )
 382        if new_name is not None:
 383            san_new_name = sanitize_html( new_name )
 384            stored.name = san_new_name
 385            stored.latest_workflow.name = san_new_name
 386            trans.sa_session.flush()
 387            # For current workflows grid:
 388            trans.set_message ( "Workflow renamed to '%s'." % new_name )
 389            return self.list( trans )
 390            # For new workflows grid:
 391            #message = "Workflow renamed to '%s'." % new_name
 392            #return self.list_grid( trans, message=message, status='done' )
 393        else:
 394            return form( url_for( action='rename', id=trans.security.encode_id(stored.id) ), 
 395                         "Rename workflow", submit_text="Rename", use_panels=True ) \
 396                .add_text( "new_name", "Workflow Name", value=to_unicode( stored.name ) )
 397                
 398    @web.expose
 399    @web.require_login( "use Galaxy workflows" )
 400    def rename_async( self, trans, id, new_name=None, **kwargs ):
 401        stored = self.get_stored_workflow( trans, id )
 402        if new_name:
 403            san_new_name = sanitize_html( new_name )
 404            stored.name = san_new_name
 405            stored.latest_workflow.name = san_new_name
 406            trans.sa_session.flush()
 407            return stored.name
 408            
 409    @web.expose
 410    @web.require_login( "use Galaxy workflows" )
 411    def annotate_async( self, trans, id, new_annotation=None, **kwargs ):
 412        stored = self.get_stored_workflow( trans, id )
 413        if new_annotation:
 414            # Sanitize annotation before adding it.
 415            new_annotation = sanitize_html( new_annotation, 'utf-8', 'text/html' )
 416            self.add_item_annotation( trans.sa_session, trans.get_user(), stored, new_annotation )
 417            trans.sa_session.flush()
 418            return new_annotation
 419            
 420    @web.expose
 421    @web.require_login( "rate items" )
 422    @web.json
 423    def rate_async( self, trans, id, rating ):
 424        """ Rate a workflow asynchronously and return updated community data. """
 425
 426        stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True )
 427        if not stored:
 428            return trans.show_error_message( "The specified workflow does not exist." )
 429
 430        # Rate workflow.
 431        stored_rating = self.rate_item( trans.sa_session, trans.get_user(), stored, rating )
 432
 433        return self.get_ave_item_rating_data( trans.sa_session, stored )
 434            
 435    @web.expose
 436    @web.require_login( "use Galaxy workflows" )
 437    def set_accessible_async( self, trans, id=None, accessible=False ):
 438        """ Set workflow's importable attribute and slug. """
 439        stored = self.get_stored_workflow( trans, id )
 440
 441        # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed.
 442        importable = accessible in ['True', 'true', 't', 'T'];
 443        if stored and stored.importable != importable:
 444            if importable:
 445                self._make_item_accessible( trans.sa_session, stored )
 446            else:
 447                stored.importable = importable
 448            trans.sa_session.flush()
 449        return
 450        
 451    @web.expose
 452    @web.require_login( "modify Galaxy items" )
 453    def set_slug_async( self, trans, id, new_slug ):
 454        stored = self.get_stored_workflow( trans, id )
 455        if stored:
 456            stored.slug = new_slug
 457            trans.sa_session.flush()
 458            return stored.slug
 459            
 460    @web.expose
 461    def get_embed_html_async( self, trans, id ):
 462        """ Returns HTML for embedding a workflow in a page. """
 463
 464        # TODO: user should be able to embed any item he has access to. see display_by_username_and_slug for security code.
 465        stored = self.get_stored_workflow( trans, id )
 466        if stored:
 467            return "Embedded Workflow '%s'" % stored.name
 468        
 469    @web.expose
 470    @web.json
 471    @web.require_login( "use Galaxy workflows" )
 472    def get_name_and_link_async( self, trans, id=None ):
 473        """ Returns workflow's name and link. """
 474        stored = self.get_stored_workflow( trans, id )
 475
 476        if self.create_item_slug( trans.sa_session, stored ):
 477            trans.sa_session.flush()
 478        return_dict = { "name" : stored.name, "link" : url_for( action="display_by_username_and_slug", username=stored.user.username, slug=stored.slug ) }
 479        return return_dict
 480    
 481    @web.expose
 482    @web.require_login( "use Galaxy workflows" )
 483    def gen_image( self, trans, id ):
 484        stored = self.get_stored_workflow( trans, id, check_ownership=True )
 485        session = trans.sa_session
 486                
 487        workflow = stored.latest_workflow
 488        data = []
 489        
 490        canvas = svgfig.canvas(style="stroke:black; fill:none; stroke-width:1px; stroke-linejoin:round; text-anchor:left")
 491        text = svgfig.SVG("g")
 492        connectors = svgfig.SVG("g")
 493        boxes = svgfig.SVG("g")
 494        svgfig.Text.defaults["font-size"] = "10px"
 495        
 496        in_pos = {}
 497        out_pos = {}
 498        margin = 5
 499        line_px = 16 # how much spacing between input/outputs
 500        widths = {} # store px width for boxes of each step
 501        max_width, max_x, max_y = 0, 0, 0
 502        
 503        for step in workflow.steps:
 504            # Load from database representation
 505            module = module_factory.from_workflow_step( trans, step )
 506            
 507            # Pack attributes into plain dictionary
 508            step_dict = {
 509                'id': step.order_index,
 510                'data_inputs': module.get_data_inputs(),
 511                'data_outputs': module.get_data_outputs(),
 512                'position': step.position
 513            }
 514            
 515            input_conn_dict = {}
 516            for conn in step.input_connections:
 517                input_conn_dict[ conn.input_name ] = \
 518                    dict( id=conn.output_step.order_index, output_name=conn.output_name )
 519            step_dict['input_connections'] = input_conn_dict
 520                    
 521            data.append(step_dict)
 522            
 523            x, y = step.position['left'], step.position['top']
 524            count = 0
 525            
 526            max_len = len(module.get_name()) * 1.5
 527            text.append( svgfig.Text(x, y + 20, module.get_name(), **{"font-size": "14px"} ).SVG() )
 528            
 529            y += 45
 530            for di in module.get_data_inputs():
 531                cur_y = y+count*line_px
 532                if step.order_index not in in_pos:
 533                    in_pos[step.order_index] = {}
 534                in_pos[step.order_index][di['name']] = (x, cur_y)
 535                text.append( svgfig.Text(x, cur_y, di['label']).SVG() )
 536                count += 1
 537                max_len = max(max_len, len(di['label']))
 538                
 539            
 540            if len(module.get_data_inputs()) > 0:
 541                y += 15
 542                
 543            for do in module.get_data_outputs():
 544                cur_y = y+count*line_px
 545                if step.order_index not in out_pos:
 546                    out_pos[step.order_index] = {}
 547                out_pos[step.order_index][do['name']] = (x, cur_y)
 548                text.append( svgfig.Text(x, cur_y, do['name']).SVG() )
 549                count += 1
 550                max_len = max(max_len, len(do['name']))
 551            
 552            widths[step.order_index] = max_len*5.5
 553            max_x = max(max_x, step.position['left'])
 554            max_y = max(max_y, step.position['top'])
 555            max_width = max(max_width, widths[step.order_index])
 556            
 557        for step_dict in data:
 558            width = widths[step_dict['id']]
 559            x, y = step_dict['position']['left'], step_dict['position']['top']
 560            boxes.append( svgfig.Rect(x-margin, y, x+width-margin, y+30, fill="#EBD9B2").SVG() )
 561            box_height = (len(step_dict['data_inputs']) + len(step_dict['data_outputs'])) * line_px + margin
 562
 563            # Draw separator line
 564            if len(step_dict['data_inputs']) > 0:
 565                box_height += 15
 566                sep_y = y + len(step_dict['data_inputs']) * line_px + 40
 567                text.append( svgfig.Line(x-margin, sep_y, x+width-margin, sep_y).SVG() ) # 
 568                
 569            # input/output box
 570            boxes.append( svgfig.Rect(x-margin, y+30, x+width-margin, y+30+box_height, fill="#ffffff").SVG() )
 571                        
 572            for conn, output_dict in step_dict['input_connections'].iteritems():
 573                in_coords = in_pos[step_dict['id']][conn]
 574                out_conn_pos = out_pos[output_dict['id']][output_dict['output_name']]
 575                adjusted = (out_conn_pos[0] + widths[output_dict['id']], out_conn_pos[1])
 576                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" ) )
 577                connectors.append( svgfig.Line(adjusted[0], adjusted[1]-margin, in_coords[0]-10, in_coords[1], arrow_end="true" ).SVG() )
 578            
 579        canvas.append(connectors)
 580        canvas.append(boxes)
 581        canvas.append(text)
 582        width, height = (max_x + max_width + 50), max_y + 300
 583        canvas['width'] = "%s px" % width
 584        canvas['height'] = "%s px" % height
 585        canvas['viewBox'] = "0 0 %s %s" % (width, height)
 586        trans.response.set_content_type("image/svg+xml")
 587        return canvas.standalone_xml()
 588        
 589        
 590    @web.expose
 591    @web.require_login( "use Galaxy workflows" )
 592    def clone( self, trans, id ):
 593        # Get workflow to clone.
 594        stored = self.get_stored_workflow( trans, id, check_ownership=False )
 595        user = trans.get_user()
 596        if stored.user == user:
 597            owner = True
 598        else:
 599            if trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \
 600                    .filter_by( user=user, stored_workflow=stored ).count() == 0:
 601                error( "Workflow is not owned by or shared with current user" )
 602            owner = False
 603            
 604        # Clone.
 605        new_stored = model.StoredWorkflow()
 606        new_stored.name = "Clone of '%s'" % stored.name
 607        new_stored.latest_workflow = stored.latest_workflow
 608        # Clone annotation.
 609        annotation_obj = self.get_item_annotation_obj( trans.sa_session, stored.user, stored )
 610        if annotation_obj:
 611            self.add_item_annotation( trans.sa_session, trans.get_user(), new_stored, annotation_obj.annotation )
 612        # Clone tags.
 613        for swta in stored.owner_tags:
 614            new_swta = model.StoredWorkflowTagAssociation()
 615            new_swta.tag = swta.tag
 616            new_swta.user = trans.user
 617            new_swta.user_tname = swta.user_tname
 618            new_swta.user_value = swta.user_value
 619            new_swta.value = swta.value
 620            new_stored.tags.append( new_swta )         
 621        if not owner:
 622            new_stored.name += " shared by '%s'" % stored.user.email
 623        new_stored.user = user
 624        # Persist
 625        session = trans.sa_session
 626        session.add( new_stored )
 627        session.flush()
 628        # Display the management page
 629        trans.set_message( 'Clone created with name "%s"' % new_stored.name )
 630        return self.list( trans )
 631    
 632    @web.expose
 633    @web.require_login( "create workflows" )
 634    def create( self, trans, workflow_name=None, workflow_annotation="" ):
 635        """
 636        Create a new stored workflow with name `workflow_name`.
 637        """
 638        user = trans.get_user()
 639        if workflow_name is not None:
 640            # Create the new stored workflow
 641            stored_workflow = model.StoredWorkflow()
 642            stored_workflow.name = workflow_name
 643            stored_workflow.user = user
 644            # And the first (empty) workflow revision
 645            workflow = model.Workflow()
 646            workflow.name = workflow_name
 647            workflow.stored_workflow = stored_workflow
 648            stored_workflow.latest_workflow = workflow
 649            # Add annotation.
 650            workflow_annotation = sanitize_html( workflow_annotation, 'utf-8', 'text/html' )
 651            self.add_item_annotation( trans.sa_session, trans.get_user(), stored_workflow, workflow_annotation )
 652            # Persist
 653            session = trans.sa_session
 654            session.add( stored_workflow )
 655            session.flush()
 656            # Display the management page
 657            trans.set_message( "Workflow '%s' created" % stored_workflow.name )
 658            return self.list( trans )
 659        else:
 660            return form( url_for(), "Create New Workflow", submit_text="Create", use_panels=True ) \
 661                    .add_text( "workflow_name", "Workflow Name", value="Unnamed workflow" ) \
 662                    .add_text( "workflow_annotation", "Workflow Annotation", value="", help="A description of the workflow; annotation is shown alongside shared or published workflows." )
 663    
 664    @web.expose
 665    def delete( self, trans, id=None ):
 666        """
 667        Mark a workflow as deleted
 668        """        
 669        # Load workflow from database
 670        stored = self.get_stored_workflow( trans, id )
 671        # Marke as deleted and save
 672        stored.deleted = True
 673        trans.sa_session.add( stored )
 674        trans.sa_session.flush()
 675        # Display the management page
 676        trans.set_message( "Workflow '%s' deleted" % stored.name )
 677        return self.list( trans )
 678        
 679    @web.expose
 680    @web.require_login( "edit workflows" )
 681    def editor( self, trans, id=None ):
 682        """
 683        Render the main workflow editor interface. The canvas is embedded as
 684        an iframe (necessary for scrolling to work properly), which is
 685        rendered by `editor_canvas`.
 686        """
 687        if not id:
 688            error( "Invalid workflow id" )
 689        stored = self.get_stored_workflow( trans, id )
 690        return trans.fill_template( "workflow/editor.mako", stored=stored, annotation=self.get_item_annotation_str( trans.sa_session, trans.user, stored ) )
 691        
 692    @web.json
 693    def editor_form_post( self, trans, type='tool', tool_id=None, annotation=None, **incoming ):
 694        """
 695        Accepts a tool state and incoming values, and generates a new tool
 696        form and some additional information, packed into a json dictionary.
 697        This is used for the form shown in the right pane when a node
 698        is selected.
 699        """
 700        
 701        trans.workflow_building_mode = True
 702        module = module_factory.from_dict( trans, {
 703            'type': type,
 704            'tool_id': tool_id,
 705            'tool_state': incoming.pop("tool_state")
 706        } )
 707        module.update_state( incoming )
 708        
 709        if type=='tool':
 710            return {
 711                'tool_state': module.get_state(),
 712                'data_inputs': module.get_data_inputs(),
 713                'data_outputs': module.get_data_outputs(),
 714                'tool_errors': module.get_errors(),
 715                'form_html': module.get_config_form(),
 716                'annotation': annotation,
 717                'post_job_actions': module.get_post_job_actions()
 718            }
 719        else:
 720            return {
 721                'tool_state': module.get_state(),
 722                'data_inputs': module.get_data_inputs(),
 723                'data_outputs': module.get_data_outputs(),
 724                'tool_errors': module.get_errors(),
 725                'form_html': module.get_config_form(),
 726                'annotation': annotation
 727            }
 728        
 729    @web.json
 730    def get_new_module_info( self, trans, type, **kwargs ):
 731        """
 732        Get the info for a new instance of a module initialized with default
 733        parameters (any keyword arguments will be passed along to the module).
 734        Result includes data inputs and outputs, html representation
 735        of the initial form, and the initial tool state (with default values).
 736        This is called asynchronously whenever a new node is added.
 737        """
 738        trans.workflow_building_mode = True
 739        module = module_factory.new( trans, type, **kwargs )
 740        return {
 741            'type': module.type,
 742            'name':  module.get_name(),
 743            'tool_id': module.get_tool_id(),
 744            'tool_state': module.get_state(),
 745            'tooltip': module.get_tooltip(),
 746            'data_inputs': module.get_data_inputs(),
 747            'data_outputs': module.get_data_outputs(),
 748            'form_html': module.get_config_form(),
 749            'annotation': ""
 750        }
 751
 752    @web.json
 753    def load_workflow( self, trans, id ):
 754        """
 755        Get the latest Workflow for the StoredWorkflow identified by `id` and
 756        encode it as a json string that can be read by the workflow editor
 757        web interface.
 758        """
 759        user = trans.get_user()
 760        id = trans.security.decode_id( id )
 761        trans.workflow_building_mode = True
 762        # Load encoded workflow from database
 763        stored = trans.sa_session.query( model.StoredWorkflow ).get( id )
 764        assert stored.user == user
 765        workflow = stored.latest_workflow
 766        # Pack workflow data into a dictionary and return
 767        data = {}
 768        data['name'] = workflow.name
 769        data['steps'] = {}
 770        data['upgrade_messages'] = {}
 771        # For each step, rebuild the form and encode the state
 772        for step in workflow.steps:
 773            # Load from database representation
 774            module = module_factory.from_workflow_step( trans, step )
 775            if not module:
 776                step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
 777                annotation_str = ""
 778                if step_annotation:
 779                    annotation_str = step_annotation.annotation
 780                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">
 781                                            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.
 782                                            </div></div></div>""" % (step.tool_id, step.tool_id)
 783                step_dict = {
 784                    'id': step.order_index,
 785                    'type': 'invalid',
 786                    'tool_id': step.tool_id,
 787                    'name': 'Unrecognized Tool: %s' % step.tool_id,
 788                    'tool_state': None,
 789                    'tooltip': None,
 790                    'tool_errors': ["Unrecognized Tool Id: %s" % step.tool_id],
 791                    'data_inputs': [],
 792                    'data_outputs': [],
 793                    'form_html': invalid_tool_form_html,
 794                    'annotation' : annotation_str,
 795                    'post_job_actions' : {},
 796                    'workflow_outputs' : []
 797                }
 798                step_dict['input_connections'] = input_conn_dict
 799                # Position
 800                step_dict['position'] = step.position
 801                # Add to return value
 802                data['steps'][step.order_index] = step_dict
 803                continue
 804            # Fix any missing parameters
 805            upgrade_message = module.check_and_update_state()
 806            if upgrade_message:
 807                # FIXME: Frontend should be able to handle workflow messages
 808                #        as a dictionary not just the values
 809                data['upgrade_messages'][step.order_index] = upgrade_message.values()
 810            # Get user annotation.
 811            step_annotation = self.get_item_annotation_obj( trans.sa_session, trans.user, step )
 812            annotation_str = ""
 813            if step_annotation:
 814                annotation_str = step_annotation.annotation
 815            # Pack attributes into plain dictionary
 816            step_dict = {
 817                'id': step.order_index,
 818                'type': module.type,
 819                'tool_id': module.get_tool_id(),
 820                'name': module.get_name(),
 821                'tool_state': module.get_state(),
 822                'tooltip': module.get_tooltip(),
 823                'tool_errors': module.get_errors(),
 824                'data_inputs': module.get_data_inputs(),
 825                'data_outputs': module.get_data_outputs(),
 826                'form_html': module.get_config_form(),
 827                'annotation' : annotation_str,
 828                'post_job_actions' : {},
 829                'workflow_outputs' : []
 830            }
 831            # Connections
 832            input_connections = step.input_connections
 833            if step.type is None or step.type == 'tool':
 834                # Determine full (prefixed) names of valid input datasets
 835                data_input_names = {}
 836                def callback( input, value, prefixed_name, prefixed_label ):
 837                    if isinstance( input, DataToolParameter ):
 838                        data_input_names[ prefixed_name ] = True
 839                visit_input_values( module.tool.inputs, module.state.inputs, callback )
 840                # Filter
 841                # FIXME: this removes connection without displaying a message currently!
 842                input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ]
 843                # post_job_actions
 844                pja_dict = {}
 845                for pja in step.post_job_actions:
 846                    pja_dict[pja.action_type+pja.output_name] = dict(action_type = pja.action_type, 
 847                                            output_name = pja.output_name,
 848                                            action_arguments = pja.action_arguments)
 849                step_dict['post_job_actions'] = pja_dict
 850                #workflow outputs
 851                outputs = []
 852                for output in step.workflow_outputs:
 853                    outputs.append(output.output_name)
 854                step_dict['workflow_outputs'] = outputs
 855            # Encode input connections as dictionary
 856            input_conn_dict = {}
 857            for conn in input_connections:
 858                input_conn_dict[ conn.input_name ] = \
 859                    dict( id=conn.output_step.order_index, output_name=conn.output_name )
 860            step_dict['input_connections'] = input_conn_dict
 861            # Position
 862            step_dict['position'] = step.position
 863            # Add to return value
 864            data['steps'][step.order_index] = step_dict
 865        return data
 866
 867    @web.json
 868    def save_workflow( self, trans, id, workflow_data ):
 869        """
 870        Save the workflow described by `workflow_data` with id `id`.
 871        """
 872        # Get the stored workflow
 873        stored = self.get_stored_workflow( trans, id )
 874        # Put parameters in workflow mode
 875        trans.workflow_building_mode = True
 876        # Convert incoming workflow data from json
 877        data = simplejson.loads( workflow_data )
 878        # Create new workflow from incoming data
 879        workflow = model.Workflow()
 880        # Just keep the last name (user can rename later)
 881        workflow.name = stored.name
 882        # Assume no errors until we find a step that has some
 883        workflow.has_errors = False
 884        # Create each step
 885        steps = []
 886        # The editor will provide ids for each step that we don't need to save,
 887        # but do need to use to make connections
 888        steps_by_external_id = {}
 889        errors = []
 890        for key, step_dict in data['steps'].iteritems():
 891            if step_dict['type'] != 'data_input' and step_dict['tool_id'] not in trans.app.toolbox.tools_by_id:
 892                errors.append("Step %s requires tool '%s'." % (step_dict['id'], step_dict['tool_id']))
 893        if errors:
 894            return dict( name=workflow.name,
 895                             message="This workflow includes missing or invalid tools. It cannot be saved until the following steps are removed or the missing tools are enabled.",
 896                             errors=errors)
 897        # First pass to build step objects and populate basic values
 898        for key, step_dict in data['steps'].iteritems():
 899            # Create the model class for the step
 900            step = model.WorkflowStep()
 901            steps.append( step )
 902            steps_by_external_id[ step_dict['id' ] ] = step
 903            # FIXME: Position should be handled inside module
 904            step.position = step_dict['position']
 905            module = module_factory.from_dict( trans, step_dict )
 906            module.save_to_step( step )
 907            if step_dict.has_key('workflow_outputs'):
 908                for output_name in step_dict['workflow_outputs']:
 909                    m = model.WorkflowOutput(workflow_step = step, output_name = output_name)
 910                    trans.sa_session.add(m)
 911            if step.tool_errors:
 912                # DBTODO Check for conditional inputs here.
 913                workflow.has_errors = True
 914            # Stick this in the step temporarily
 915            step.temp_input_connections = step_dict['input_connections']
 916            # Save step annotation.
 917            annotation = step_dict[ 'annotation' ]
 918            if annotation:
 919                annotation = sanitize_html( annotation, 'utf-8', 'text/html' )
 920                self.add_item_annotation( trans.sa_session, trans.get_user(), step, annotation )
 921        # Second pass to deal with connections between steps
 922        for step in steps:
 923            # Input connections
 924            for input_name, conn_dict in step.temp_input_connections.iteritems():
 925                if conn_dict:
 926                    conn = model.WorkflowStepConnection()
 927                    conn.input_step = step
 928                    conn.input_name = input_name
 929                    conn.output_name = conn_dict['output_name']
 930                    conn.output_step = steps_by_external_id[ conn_dict['id'] ]
 931            del step.temp_input_connections
 932        # Order the steps if possible
 933        attach_ordered_steps( workflow, steps )
 934        # Connect up
 935        workflow.stored_workflow = stored
 936        stored.latest_workflow = workflow
 937        # Persist
 938        trans.sa_session.flush()
 939        # Return something informative
 940        errors = []
 941        if workflow.has_errors:
 942            errors.append( "Some steps in this workflow have validation errors" )
 943        if workflow.has_cycles:
 944            errors.append( "This workflow contains cycles" )
 945        if errors:
 946            rval = dict( message="Workflow saved, but will not be runnable due to the following errors",
 947                         errors=errors )
 948        else:
 949            rval = dict( message="Workflow saved" )
 950        rval['name'] = workflow.name
 951        return rval
 952        
 953    @web.expose
 954    @web.require_login( "use workflows" )
 955    def export( self, trans, id=None, **kwd ):
 956        """
 957        Handles download/export workflow command.
 958        """
 959        stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True )
 960        return trans.fill_template( "/workflow/export.mako", item=stored, use_panels=True )
 961        
 962        
 963    @web.expose
 964    @web.require_login( "use workflows" )
 965    def import_from_myexp( self, trans, myexp_id, myexp_username=None, myexp_password=None ):
 966        """
 967        Imports a workflow from the myExperiment website.
 968        """
 969        
 970        #
 971        # Get workflow XML.
 972        #
 973        
 974        # Get workflow content.
 975        conn = httplib.HTTPConnection( self.__myexp_url )
 976        # NOTE: blocks web thread.
 977        headers = {}
 978        if myexp_username and myexp_password:
 979            auth_header = base64.b64encode( '%s:%s' % ( myexp_username, myexp_password ))
 980            headers = { "Authorization" : "Basic %s" % auth_header }
 981        conn.request( "GET", "/workflow.xml?id=%s&elements=content" % myexp_id, headers=headers )
 982        response = conn.getresponse()
 983        workflow_xml = response.read()
 984        conn.close()
 985        parser = SingleTagContentsParser( "content" )
 986        parser.feed( workflow_xml )
 987        workflow_content = base64.b64decode( parser.tag_content )
 988        
 989        #
 990        # Process workflow XML and create workflow.
 991        #
 992        parser = SingleTagContentsParser( "galaxy_json" )
 993        parser.feed( workflow_content )
 994        workflow_dict = from_json_string( parser.tag_content )
 995        
 996        # Create workflow.
 997        workflow = self._workflow_from_dict( trans, workflow_dict, source="myExperiment" ).latest_workflow
 998        
 999        # Provide user feedback.
1000        if workflow.has_errors:
1001            return trans.show_warn_message( "Imported, but some steps in this workflow have validation errors" )
1002        if workflow.has_cycles:
1003            return trans.show_warn_message( "Imported, but this workflow contains cycles" )
1004        else:
1005            return trans.show_message( "Workflow '%s' imported" % workflow.name )
1006         
1007    @web.expose
1008    @web.require_login( "use workflows" )
1009    def export_to_myexp( self, trans, id, myexp_username, myexp_password ):
1010        """
1011        Exports a workflow to myExperiment website.
1012        """
1013        
1014        # Load encoded workflow from database        
1015        user = trans.get_user()
1016        id = trans.security.decode_id( id )
1017        trans.workflow_building_mode = True
1018        stored = trans.sa_session.query( model.StoredWorkflow ).get( id )
1019        self.security_check( trans, stored, False, True )
1020        
1021        # Convert workflow to dict.
1022        workflow_dict = self._workflow_to_dict( trans, stored )
1023        
1024        #
1025        # Create and submit workflow myExperiment request.
1026        #
1027        
1028        # Create workflow content XML.
1029        workflow_dict_packed = simplejson.dumps( workflow_dict, indent=4, sort_keys=True )
1030        workflow_content = trans.fill_template( "workflow/myexp_export_content.mako", \
1031                                                 workflow_dict_packed=workflow_dict_packed, \
1032                                                 workflow_steps=workflow_dict['steps'] )
1033                                                
1034        # Create myExperiment request.
1035        request_raw = trans.fill_template( "workflow/myexp_export.mako", \
1036                                            workflow_name=workflow_dict['name'], \
1037                                            workflow_description=workflow_dict['annotation'], \
1038                                            workflow_content=workflow_content
1039                                            )
1040        # strip() b/c myExperiment XML parser doesn't allow white space before XML; utf-8 handles unicode characters.
1041        request = unicode( request_raw.strip(), 'utf-8' )
1042        
1043        # Do request and get result.
1044        auth_header = base64.b64encode( '%s:%s' % ( myexp_username, myexp_password ))
1045        headers = { "Content-type": "text/xml", "Accept": "text/xml", "Authorization" : "Basic %s" % auth_header }
1046        conn = httplib.HTTPConnection( self.__myexp_url )
1047        # NOTE: blocks web thread.
1048        conn.request("POST", "/workflow.xml", request, headers)
1049        response = conn.getresponse()
1050        response_data = response.read()
1051        conn.close()
1052        
1053        # Do simple parse of response to see if export successful and provide user feedback.
1054        parser = SingleTagContentsParser( 'id' )
1055        parser.feed( response_data )
1056        myexp_workflow_id = pa

Large files files are truncated, but you can click here to view the full file