/lib/galaxy/web/controllers/workflow.py
Python | 1321 lines | 1309 code | 9 blank | 3 comment | 24 complexity | 9c8918f056f2affdf444db78e0988e28 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" ) 5import simplejson 6 7from galaxy.web.framework.helpers import time_ago, iff, grids 8from galaxy.tools.parameters import * 9from galaxy.tools import DefaultToolState 10from galaxy.tools.parameters.grouping import Repeat, Conditional 11from galaxy.datatypes.data import Data 12from galaxy.util.odict import odict 13from galaxy.util.bunch import Bunch 14from galaxy.util.sanitize_html import sanitize_html 15from galaxy.util.topsort import topsort, topsort_levels, CycleError 16from galaxy.workflow.modules import * 17from galaxy import model 18from galaxy.model.mapping import desc 19from galaxy.model.orm import * 20from galaxy.jobs.actions.post import * 21from galaxy.item_attrs.ratings import UsesItemRatings 22 23import urllib2 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", model_class=model.StoredWorkflow, attach_popup=True, filterable="advanced" ), 38 grids.IndividualTagsColumn( "Tags", "tags", model.StoredWorkflow, 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", model_class=model.StoredWorkflow, filterable="advanced" ), 68 grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_class=model.StoredWorkflow, model_annotation_association_class=model.StoredWorkflowAnnotationAssociation, filterable="advanced" ), 69 grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced", sortable=False ), 70 grids.CommunityTagsColumn( "Community Tags", "tags", model.StoredWorkflow, model.StoredWorkflowTagAssociation, filterable="advanced", grid_name="PublicWorkflowListGrid" ), 71 grids.GridColumn( "Last Updated", key="update_time", format=time_ago ) 72 ] 73 columns.append( 74 grids.MulticolFilterColumn( 75 "Search", 76 cols_to_filter=[ columns[0], columns[1], columns[2] ], 77 key="free-text-search", visible=False, filterable="standard" ) 78 ) 79 operations = [] 80 def build_initial_query( self, trans, **kwargs ): 81 # Join so that searching stored_workflow.user makes sense. 82 return trans.sa_session.query( self.model_class ).join( model.User.table ) 83 def apply_query_filter( self, trans, query, **kwargs ): 84 # A public workflow is published, has a slug, and is not deleted. 85 return query.filter( self.model_class.published==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) 86 87class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnnotations, UsesItemRatings ): 88 stored_list_grid = StoredWorkflowListGrid() 89 published_list_grid = StoredWorkflowAllPublishedGrid() 90 91 @web.expose 92 def index( self, trans ): 93 return self.list( trans ) 94 95 @web.expose 96 @web.require_login( "use Galaxy workflows" ) 97 def list_grid( self, trans, **kwargs ): 98 """ List user's stored workflows. """ 99 status = message = None 100 if 'operation' in kwargs: 101 operation = kwargs['operation'].lower() 102 if operation == "rename": 103 return self.rename( trans, **kwargs ) 104 history_ids = util.listify( kwargs.get( 'id', [] ) ) 105 if operation == "sharing": 106 return self.sharing( trans, id=history_ids ) 107 return self.stored_list_grid( trans, **kwargs ) 108 109 @web.expose 110 @web.require_login( "use Galaxy workflows", use_panels=True ) 111 def list( self, trans ): 112 """ 113 Render workflow main page (management of existing workflows) 114 """ 115 user = trans.get_user() 116 workflows = trans.sa_session.query( model.StoredWorkflow ) \ 117 .filter_by( user=user, deleted=False ) \ 118 .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \ 119 .all() 120 shared_by_others = trans.sa_session \ 121 .query( model.StoredWorkflowUserShareAssociation ) \ 122 .filter_by( user=user ) \ 123 .join( 'stored_workflow' ) \ 124 .filter( model.StoredWorkflow.deleted == False ) \ 125 .order_by( desc( model.StoredWorkflow.update_time ) ) \ 126 .all() 127 128 # Legacy issue: all shared workflows must have slugs. 129 slug_set = False 130 for workflow_assoc in shared_by_others: 131 slug_set = self.create_item_slug( trans.sa_session, workflow_assoc.stored_workflow ) 132 if slug_set: 133 trans.sa_session.flush() 134 135 return trans.fill_template( "workflow/list.mako", 136 workflows = workflows, 137 shared_by_others = shared_by_others ) 138 139 @web.expose 140 @web.require_login( "use Galaxy workflows" ) 141 def list_for_run( self, trans ): 142 """ 143 Render workflow list for analysis view (just allows running workflow 144 or switching to management view) 145 """ 146 user = trans.get_user() 147 workflows = trans.sa_session.query( model.StoredWorkflow ) \ 148 .filter_by( user=user, deleted=False ) \ 149 .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \ 150 .all() 151 shared_by_others = trans.sa_session \ 152 .query( model.StoredWorkflowUserShareAssociation ) \ 153 .filter_by( user=user ) \ 154 .filter( model.StoredWorkflow.deleted == False ) \ 155 .order_by( desc( model.StoredWorkflow.table.c.update_time ) ) \ 156 .all() 157 return trans.fill_template( "workflow/list_for_run.mako", 158 workflows = workflows, 159 shared_by_others = shared_by_others ) 160 161 @web.expose 162 def list_published( self, trans, **kwargs ): 163 grid = self.published_list_grid( trans, **kwargs ) 164 if 'async' in kwargs: 165 return grid 166 else: 167 # Render grid wrapped in panels 168 return trans.fill_template( "workflow/list_published.mako", grid=grid ) 169 170 @web.expose 171 def display_by_username_and_slug( self, trans, username, slug ): 172 """ Display workflow based on a username and slug. """ 173 174 # Get workflow. 175 session = trans.sa_session 176 user = session.query( model.User ).filter_by( username=username ).first() 177 stored_workflow = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, slug=slug, deleted=False ).first() 178 if stored_workflow is None: 179 raise web.httpexceptions.HTTPNotFound() 180 # Security check raises error if user cannot access workflow. 181 self.security_check( trans.get_user(), stored_workflow, False, True) 182 183 # Get data for workflow's steps. 184 self.get_stored_workflow_steps( trans, stored_workflow ) 185 # Get annotations. 186 stored_workflow.annotation = self.get_item_annotation_str( trans, stored_workflow.user, stored_workflow ) 187 for step in stored_workflow.latest_workflow.steps: 188 step.annotation = self.get_item_annotation_str( trans, stored_workflow.user, step ) 189 190 # Get rating data. 191 user_item_rating = 0 192 if trans.get_user(): 193 user_item_rating = self.get_user_item_rating( trans, trans.get_user(), stored_workflow ) 194 if user_item_rating: 195 user_item_rating = user_item_rating.rating 196 else: 197 user_item_rating = 0 198 ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans, stored_workflow ) 199 return trans.fill_template_mako( "workflow/display.mako", item=stored_workflow, item_data=stored_workflow.latest_workflow.steps, 200 user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings ) 201 202 @web.expose 203 def get_item_content_async( self, trans, id ): 204 """ Returns item content in HTML format. """ 205 206 stored = self.get_stored_workflow( trans, id, False, True ) 207 if stored is None: 208 raise web.httpexceptions.HTTPNotFound() 209 210 # Get data for workflow's steps. 211 self.get_stored_workflow_steps( trans, stored ) 212 # Get annotations. 213 stored.annotation = self.get_item_annotation_str( trans, stored.user, stored ) 214 for step in stored.latest_workflow.steps: 215 step.annotation = self.get_item_annotation_str( trans, stored.user, step ) 216 return trans.stream_template_mako( "/workflow/item_content.mako", item = stored, item_data = stored.latest_workflow.steps ) 217 218 @web.expose 219 @web.require_login( "use Galaxy workflows" ) 220 def share( self, trans, id, email="" ): 221 msg = mtype = None 222 # Load workflow from database 223 stored = self.get_stored_workflow( trans, id ) 224 if email: 225 other = trans.sa_session.query( model.User ) \ 226 .filter( and_( model.User.table.c.email==email, 227 model.User.table.c.deleted==False ) ) \ 228 .first() 229 if not other: 230 mtype = "error" 231 msg = ( "User '%s' does not exist" % email ) 232 elif other == trans.get_user(): 233 mtype = "error" 234 msg = ( "You cannot share a workflow with yourself" ) 235 elif trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ 236 .filter_by( user=other, stored_workflow=stored ).count() > 0: 237 mtype = "error" 238 msg = ( "Workflow already shared with '%s'" % email ) 239 else: 240 share = model.StoredWorkflowUserShareAssociation() 241 share.stored_workflow = stored 242 share.user = other 243 session = trans.sa_session 244 session.add( share ) 245 self.create_item_slug( session, stored ) 246 session.flush() 247 trans.set_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) ) 248 return trans.response.send_redirect( url_for( controller='workflow', action='sharing', id=id ) ) 249 return trans.fill_template( "workflow/share.mako", 250 message = msg, 251 messagetype = mtype, 252 stored=stored, 253 email=email ) 254 255 @web.expose 256 @web.require_login( "use Galaxy workflows" ) 257 def sharing( self, trans, id, **kwargs ): 258 """ Handle workflow sharing. """ 259 260 # Get session and workflow. 261 session = trans.sa_session 262 stored = self.get_stored_workflow( trans, id ) 263 session.add( stored ) 264 265 # Do operation on workflow. 266 if 'make_accessible_via_link' in kwargs: 267 self._make_item_accessible( trans.sa_session, stored ) 268 elif 'make_accessible_and_publish' in kwargs: 269 self._make_item_accessible( trans.sa_session, stored ) 270 stored.published = True 271 elif 'publish' in kwargs: 272 stored.published = True 273 elif 'disable_link_access' in kwargs: 274 stored.importable = False 275 elif 'unpublish' in kwargs: 276 stored.published = False 277 elif 'disable_link_access_and_unpublish' in kwargs: 278 stored.importable = stored.published = False 279 elif 'unshare_user' in kwargs: 280 user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) ) 281 if not user: 282 error( "User not found for provided id" ) 283 association = session.query( model.StoredWorkflowUserShareAssociation ) \ 284 .filter_by( user=user, stored_workflow=stored ).one() 285 session.delete( association ) 286 287 # Legacy issue: workflows made accessible before recent updates may not have a slug. Create slug for any workflows that need them. 288 if stored.importable and not stored.slug: 289 self._make_item_accessible( trans.sa_session, stored ) 290 291 session.flush() 292 293 return trans.fill_template( "/sharing_base.mako", 294 item=stored ) 295 296 @web.expose 297 @web.require_login( "to import a workflow", use_panels=True ) 298 def imp( self, trans, id, **kwargs ): 299 # Set referer message. 300 referer = trans.request.referer 301 if referer is not "": 302 referer_message = "<a href='%s'>return to the previous page</a>" % referer 303 else: 304 referer_message = "<a href='%s'>go to Galaxy's start page</a>" % url_for( '/' ) 305 306 # Do import. 307 session = trans.sa_session 308 stored = self.get_stored_workflow( trans, id, check_ownership=False ) 309 if stored.importable == False: 310 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 ) 311 elif stored.user == trans.user: 312 return trans.show_error_message( "You can't import this workflow because you own it.<br>You can %s" % referer_message, use_panels=True ) 313 elif stored.deleted: 314 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 ) 315 else: 316 # Create imported workflow via copy. 317 imported_stored = model.StoredWorkflow() 318 imported_stored.name = "imported: " + stored.name 319 imported_stored.latest_workflow = stored.latest_workflow 320 imported_stored.user = trans.user 321 # Save new workflow. 322 session = trans.sa_session 323 session.add( imported_stored ) 324 session.flush() 325 326 # Redirect to load galaxy frames. 327 return trans.show_ok_message( 328 message="""Workflow "%s" has been imported. <br>You can <a href="%s">start using this workflow</a> or %s.""" 329 % ( stored.name, web.url_for( controller='workflow' ), referer_message ), use_panels=True ) 330 331 @web.expose 332 @web.require_login( "use Galaxy workflows" ) 333 def edit_attributes( self, trans, id, **kwargs ): 334 # Get workflow and do error checking. 335 stored = self.get_stored_workflow( trans, id ) 336 if not stored: 337 error( "You do not own this workflow or workflow ID is invalid." ) 338 339 # Update workflow attributes if new values submitted. 340 if 'name' in kwargs: 341 # Rename workflow. 342 stored.name = kwargs[ 'name' ] 343 if 'annotation' in kwargs: 344 # Set workflow annotation; sanitize annotation before adding it. 345 annotation = sanitize_html( kwargs[ 'annotation' ], 'utf-8', 'text/html' ) 346 self.add_item_annotation( trans, stored, annotation ) 347 trans.sa_session.flush() 348 349 return trans.fill_template( 'workflow/edit_attributes.mako', 350 stored=stored, 351 annotation=self.get_item_annotation_str( trans, trans.user, stored ) 352 ) 353 354 @web.expose 355 @web.require_login( "use Galaxy workflows" ) 356 def rename( self, trans, id, new_name=None, **kwargs ): 357 stored = self.get_stored_workflow( trans, id ) 358 if new_name is not None: 359 stored.name = new_name 360 trans.sa_session.flush() 361 # For current workflows grid: 362 trans.set_message ( "Workflow renamed to '%s'." % new_name ) 363 return self.list( trans ) 364 # For new workflows grid: 365 #message = "Workflow renamed to '%s'." % new_name 366 #return self.list_grid( trans, message=message, status='done' ) 367 else: 368 return form( url_for( action='rename', id=trans.security.encode_id(stored.id) ), "Rename workflow", submit_text="Rename" ) \ 369 .add_text( "new_name", "Workflow Name", value=stored.name ) 370 371 @web.expose 372 @web.require_login( "use Galaxy workflows" ) 373 def rename_async( self, trans, id, new_name=None, **kwargs ): 374 stored = self.get_stored_workflow( trans, id ) 375 if new_name: 376 stored.name = new_name 377 trans.sa_session.flush() 378 return stored.name 379 380 @web.expose 381 @web.require_login( "use Galaxy workflows" ) 382 def annotate_async( self, trans, id, new_annotation=None, **kwargs ): 383 stored = self.get_stored_workflow( trans, id ) 384 if new_annotation: 385 # Sanitize annotation before adding it. 386 new_annotation = sanitize_html( new_annotation, 'utf-8', 'text/html' ) 387 self.add_item_annotation( trans, stored, new_annotation ) 388 trans.sa_session.flush() 389 return new_annotation 390 391 @web.expose 392 @web.require_login( "rate items" ) 393 @web.json 394 def rate_async( self, trans, id, rating ): 395 """ Rate a workflow asynchronously and return updated community data. """ 396 397 stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True ) 398 if not stored: 399 return trans.show_error_message( "The specified workflow does not exist." ) 400 401 # Rate workflow. 402 stored_rating = self.rate_item( trans, trans.get_user(), stored, rating ) 403 404 return self.get_ave_item_rating_data( trans, stored ) 405 406 @web.expose 407 @web.require_login( "use Galaxy workflows" ) 408 def set_accessible_async( self, trans, id=None, accessible=False ): 409 """ Set workflow's importable attribute and slug. """ 410 stored = self.get_stored_workflow( trans, id ) 411 412 # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed. 413 importable = accessible in ['True', 'true', 't', 'T']; 414 if stored and stored.importable != importable: 415 if importable: 416 self._make_item_accessible( trans.sa_session, stored ) 417 else: 418 stored.importable = importable 419 trans.sa_session.flush() 420 return 421 422 @web.expose 423 @web.require_login( "modify Galaxy items" ) 424 def set_slug_async( self, trans, id, new_slug ): 425 stored = self.get_stored_workflow( trans, id ) 426 if stored: 427 stored.slug = new_slug 428 trans.sa_session.flush() 429 return stored.slug 430 431 @web.expose 432 def get_embed_html_async( self, trans, id ): 433 """ Returns HTML for embedding a workflow in a page. """ 434 435 # TODO: user should be able to embed any item he has access to. see display_by_username_and_slug for security code. 436 stored = self.get_stored_workflow( trans, id ) 437 if stored: 438 return "Embedded Workflow '%s'" % stored.name 439 440 @web.expose 441 @web.json 442 @web.require_login( "use Galaxy workflows" ) 443 def get_name_and_link_async( self, trans, id=None ): 444 """ Returns workflow's name and link. """ 445 stored = self.get_stored_workflow( trans, id ) 446 447 if self.create_item_slug( trans.sa_session, stored ): 448 trans.sa_session.flush() 449 return_dict = { "name" : stored.name, "link" : url_for( action="display_by_username_and_slug", username=stored.user.username, slug=stored.slug ) } 450 return return_dict 451 452 @web.expose 453 @web.require_login( "use Galaxy workflows" ) 454 def clone( self, trans, id ): 455 stored = self.get_stored_workflow( trans, id, check_ownership=False ) 456 user = trans.get_user() 457 if stored.user == user: 458 owner = True 459 else: 460 if trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ 461 .filter_by( user=user, stored_workflow=stored ).count() == 0: 462 error( "Workflow is not owned by or shared with current user" ) 463 owner = False 464 new_stored = model.StoredWorkflow() 465 new_stored.name = "Clone of '%s'" % stored.name 466 new_stored.latest_workflow = stored.latest_workflow 467 if not owner: 468 new_stored.name += " shared by '%s'" % stored.user.email 469 new_stored.user = user 470 # Persist 471 session = trans.sa_session 472 session.add( new_stored ) 473 session.flush() 474 # Display the management page 475 trans.set_message( 'Clone created with name "%s"' % new_stored.name ) 476 return self.list( trans ) 477 478 @web.expose 479 @web.require_login( "create workflows" ) 480 def create( self, trans, workflow_name=None, workflow_annotation="" ): 481 """ 482 Create a new stored workflow with name `workflow_name`. 483 """ 484 user = trans.get_user() 485 if workflow_name is not None: 486 # Create the new stored workflow 487 stored_workflow = model.StoredWorkflow() 488 stored_workflow.name = workflow_name 489 stored_workflow.user = user 490 # And the first (empty) workflow revision 491 workflow = model.Workflow() 492 workflow.name = workflow_name 493 workflow.stored_workflow = stored_workflow 494 stored_workflow.latest_workflow = workflow 495 # Add annotation. 496 workflow_annotation = sanitize_html( workflow_annotation, 'utf-8', 'text/html' ) 497 self.add_item_annotation( trans, stored_workflow, workflow_annotation ) 498 # Persist 499 session = trans.sa_session 500 session.add( stored_workflow ) 501 session.flush() 502 # Display the management page 503 trans.set_message( "Workflow '%s' created" % stored_workflow.name ) 504 return self.list( trans ) 505 else: 506 return form( url_for(), "Create New Workflow", submit_text="Create" ) \ 507 .add_text( "workflow_name", "Workflow Name", value="Unnamed workflow" ) \ 508 .add_text( "workflow_annotation", "Workflow Annotation", value="", help="A description of the workflow; annotation is shown alongside shared or published workflows." ) 509 510 @web.expose 511 def delete( self, trans, id=None ): 512 """ 513 Mark a workflow as deleted 514 """ 515 # Load workflow from database 516 stored = self.get_stored_workflow( trans, id ) 517 # Marke as deleted and save 518 stored.deleted = True 519 trans.sa_session.add( stored ) 520 trans.sa_session.flush() 521 # Display the management page 522 trans.set_message( "Workflow '%s' deleted" % stored.name ) 523 return self.list( trans ) 524 525 @web.expose 526 @web.require_login( "edit workflows" ) 527 def editor( self, trans, id=None ): 528 """ 529 Render the main workflow editor interface. The canvas is embedded as 530 an iframe (necessary for scrolling to work properly), which is 531 rendered by `editor_canvas`. 532 """ 533 if not id: 534 error( "Invalid workflow id" ) 535 stored = self.get_stored_workflow( trans, id ) 536 return trans.fill_template( "workflow/editor.mako", stored=stored, annotation=self.get_item_annotation_str( trans, trans.user, stored ) ) 537 538 @web.json 539 def editor_form_post( self, trans, type='tool', tool_id=None, annotation=None, **incoming ): 540 """ 541 Accepts a tool state and incoming values, and generates a new tool 542 form and some additional information, packed into a json dictionary. 543 This is used for the form shown in the right pane when a node 544 is selected. 545 """ 546 547 trans.workflow_building_mode = True 548 module = module_factory.from_dict( trans, { 549 'type': type, 550 'tool_id': tool_id, 551 'tool_state': incoming.pop("tool_state") 552 } ) 553 module.update_state( incoming ) 554 555 if type=='tool': 556 return { 557 'tool_state': module.get_state(), 558 'data_inputs': module.get_data_inputs(), 559 'data_outputs': module.get_data_outputs(), 560 'tool_errors': module.get_errors(), 561 'form_html': module.get_config_form(), 562 'annotation': annotation, 563 'post_job_actions': module.get_post_job_actions() 564 } 565 else: 566 return { 567 'tool_state': module.get_state(), 568 'data_inputs': module.get_data_inputs(), 569 'data_outputs': module.get_data_outputs(), 570 'tool_errors': module.get_errors(), 571 'form_html': module.get_config_form(), 572 'annotation': annotation 573 } 574 575 @web.json 576 def get_new_module_info( self, trans, type, **kwargs ): 577 """ 578 Get the info for a new instance of a module initialized with default 579 parameters (any keyword arguments will be passed along to the module). 580 Result includes data inputs and outputs, html representation 581 of the initial form, and the initial tool state (with default values). 582 This is called asynchronously whenever a new node is added. 583 """ 584 trans.workflow_building_mode = True 585 module = module_factory.new( trans, type, **kwargs ) 586 return { 587 'type': module.type, 588 'name': module.get_name(), 589 'tool_id': module.get_tool_id(), 590 'tool_state': module.get_state(), 591 'tooltip': module.get_tooltip(), 592 'data_inputs': module.get_data_inputs(), 593 'data_outputs': module.get_data_outputs(), 594 'form_html': module.get_config_form(), 595 'annotation': "" 596 } 597 598 @web.json 599 def load_workflow( self, trans, id ): 600 """ 601 Get the latest Workflow for the StoredWorkflow identified by `id` and 602 encode it as a json string that can be read by the workflow editor 603 web interface. 604 """ 605 user = trans.get_user() 606 id = trans.security.decode_id( id ) 607 trans.workflow_building_mode = True 608 # Load encoded workflow from database 609 stored = trans.sa_session.query( model.StoredWorkflow ).get( id ) 610 assert stored.user == user 611 workflow = stored.latest_workflow 612 # Pack workflow data into a dictionary and return 613 data = {} 614 data['name'] = workflow.name 615 data['steps'] = {} 616 data['upgrade_messages'] = {} 617 # For each step, rebuild the form and encode the state 618 for step in workflow.steps: 619 # Load from database representation 620 module = module_factory.from_workflow_step( trans, step ) 621 # Fix any missing parameters 622 upgrade_message = module.check_and_update_state() 623 if upgrade_message: 624 # FIXME: Frontend should be able to handle workflow messages 625 # as a dictionary not just the values 626 data['upgrade_messages'][step.order_index] = upgrade_message.values() 627 # Get user annotation. 628 step_annotation = self.get_item_annotation_obj ( trans, trans.user, step ) 629 annotation_str = "" 630 if step_annotation: 631 annotation_str = step_annotation.annotation 632 # Pack attributes into plain dictionary 633 step_dict = { 634 'id': step.order_index, 635 'type': module.type, 636 'tool_id': module.get_tool_id(), 637 'name': module.get_name(), 638 'tool_state': module.get_state(), 639 'tooltip': module.get_tooltip(), 640 'tool_errors': module.get_errors(), 641 'data_inputs': module.get_data_inputs(), 642 'data_outputs': module.get_data_outputs(), 643 'form_html': module.get_config_form(), 644 'annotation' : annotation_str 645 } 646 # Connections 647 input_connections = step.input_connections 648 if step.type is None or step.type == 'tool': 649 # Determine full (prefixed) names of valid input datasets 650 data_input_names = {} 651 def callback( input, value, prefixed_name, prefixed_label ): 652 if isinstance( input, DataToolParameter ): 653 data_input_names[ prefixed_name ] = True 654 visit_input_values( module.tool.inputs, module.state.inputs, callback ) 655 # Filter 656 # FIXME: this removes connection without displaying a message currently! 657 input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ] 658 # post_job_actions 659 pja_dict = {} 660 for pja in step.post_job_actions: 661 pja_dict[pja.action_type+pja.output_name] = dict(action_type = pja.action_type, 662 output_name = pja.output_name, 663 action_arguments = pja.action_arguments) 664 step_dict['post_job_actions'] = pja_dict 665 # Encode input connections as dictionary 666 input_conn_dict = {} 667 for conn in input_connections: 668 input_conn_dict[ conn.input_name ] = \ 669 dict( id=conn.output_step.order_index, output_name=conn.output_name ) 670 step_dict['input_connections'] = input_conn_dict 671 # Position 672 step_dict['position'] = step.position 673 # Add to return value 674 data['steps'][step.order_index] = step_dict 675 return data 676 677 @web.json 678 def save_workflow( self, trans, id, workflow_data ): 679 """ 680 Save the workflow described by `workflow_data` with id `id`. 681 """ 682 # Get the stored workflow 683 stored = self.get_stored_workflow( trans, id ) 684 # Put parameters in workflow mode 685 trans.workflow_building_mode = True 686 # Convert incoming workflow data from json 687 data = simplejson.loads( workflow_data ) 688 # Create new workflow from incoming data 689 workflow = model.Workflow() 690 # Just keep the last name (user can rename later) 691 workflow.name = stored.name 692 # Assume no errors until we find a step that has some 693 workflow.has_errors = False 694 # Create each step 695 steps = [] 696 # The editor will provide ids for each step that we don't need to save, 697 # but do need to use to make connections 698 steps_by_external_id = {} 699 # First pass to build step objects and populate basic values 700 for key, step_dict in data['steps'].iteritems(): 701 # Create the model class for the step 702 step = model.WorkflowStep() 703 steps.append( step ) 704 steps_by_external_id[ step_dict['id' ] ] = step 705 # FIXME: Position should be handled inside module 706 step.position = step_dict['position'] 707 module = module_factory.from_dict( trans, step_dict ) 708 module.save_to_step( step ) 709 if step.tool_errors: 710 workflow.has_errors = True 711 # Stick this in the step temporarily 712 step.temp_input_connections = step_dict['input_connections'] 713 # Save step annotation. 714 annotation = step_dict[ 'annotation' ] 715 if annotation: 716 annotation = sanitize_html( annotation, 'utf-8', 'text/html' ) 717 self.add_item_annotation( trans, step, annotation ) 718 # Second pass to deal with connections between steps 719 for step in steps: 720 # Input connections 721 for input_name, conn_dict in step.temp_input_connections.iteritems(): 722 if conn_dict: 723 conn = model.WorkflowStepConnection() 724 conn.input_step = step 725 conn.input_name = input_name 726 conn.output_name = conn_dict['output_name'] 727 conn.output_step = steps_by_external_id[ conn_dict['id'] ] 728 del step.temp_input_connections 729 # Order the steps if possible 730 attach_ordered_steps( workflow, steps ) 731 # Connect up 732 workflow.stored_workflow = stored 733 stored.latest_workflow = workflow 734 # Persist 735 trans.sa_session.flush() 736 # Return something informative 737 errors = [] 738 if workflow.has_errors: 739 errors.append( "Some steps in this workflow have validation errors" ) 740 if workflow.has_cycles: 741 errors.append( "This workflow contains cycles" ) 742 if errors: 743 rval = dict( message="Workflow saved, but will not be runnable due to the following errors", 744 errors=errors ) 745 else: 746 rval = dict( message="Workflow saved" ) 747 rval['name'] = workflow.name 748 return rval 749 750 @web.json_pretty 751 def export_workflow( self, trans, id ): 752 """ 753 Get the latest Workflow for the StoredWorkflow identified by `id` and 754 encode it as a json string that can be imported back into Galaxy 755 756 This has slightly different information than the above. In particular, 757 it does not attempt to decode forms and build UIs, it just stores 758 the raw state. 759 """ 760 user = trans.get_user() 761 id = trans.security.decode_id( id ) 762 trans.workflow_building_mode = True 763 # Load encoded workflow from database 764 stored = trans.sa_session.query( model.StoredWorkflow ).get( id ) 765 self.security_check( trans.get_user(), stored, False, True ) 766 workflow = stored.latest_workflow 767 # Pack workflow data into a dictionary and return 768 data = {} 769 data['name'] = workflow.name 770 data['steps'] = {} 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 # Get user annotation. 776 step_annotation = self.get_item_annotation_obj( trans, trans.user, step ) 777 annotation_str = "" 778 if step_annotation: 779 annotation_str = step_annotation.annotation 780 # Pack attributes into plain dictionary 781 step_dict = { 782 'id': step.order_index, 783 'type': module.type, 784 'tool_id': module.get_tool_id(), 785 'name': module.get_name(), 786 'tool_state': module.get_state( secure=False ), 787 'tool_errors': module.get_errors(), 788 ## 'data_inputs': module.get_data_inputs(), 789 ## 'data_outputs': module.get_data_outputs(), 790 'annotation' : annotation_str 791 } 792 # Connections 793 input_connections = step.input_connections 794 if step.type is None or step.type == 'tool': 795 # Determine full (prefixed) names of valid input datasets 796 data_input_names = {} 797 def callback( input, value, prefixed_name, prefixed_label ): 798 if isinstance( input, DataToolParameter ): 799 data_input_names[ prefixed_name ] = True 800 visit_input_values( module.tool.inputs, module.state.inputs, callback ) 801 # Filter 802 # FIXME: this removes connection without displaying a message currently! 803 input_connections = [ conn for conn in input_connections if conn.input_name in data_input_names ] 804 # Encode input connections as dictionary 805 input_conn_dict = {} 806 for conn in input_connections: 807 input_conn_dict[ conn.input_name ] = \ 808 dict( id=conn.output_step.order_index, output_name=conn.output_name ) 809 step_dict['input_connections'] = input_conn_dict 810 # Position 811 step_dict['position'] = step.position 812 # Add to return value 813 data['steps'][step.order_index] = step_dict 814 return data 815 816 @web.expose 817 def import_workflow( self, trans, workflow_text=None, url=None ): 818 if workflow_text is None and url is None: 819 return form( url_for(), "Import Workflow", submit_text="Import" ) \ 820 .add_text( "url", "URL to load workflow from", "" ) \ 821 .add_input( "textarea", "Encoded workflow (as generated by export workflow)", "workflow_text", "" ) 822 if url: 823 # Load workflow from external URL 824 # NOTE: blocks the web thread. 825 try: 826 workflow_data = urllib2.urlopen( url ).read() 827 except Exception, e: 828 return trans.show_error_message( "Failed to open URL %s<br><br>Message: %s" % ( url, str( e ) ) ) 829 else: 830 workflow_data = workflow_text 831 # Convert incoming workflow data from json 832 try: 833 data = simplejson.loads( workflow_data ) 834 except Exception, e: 835 return trans.show_error_message( "Data at '%s' does not appear to be a Galaxy workflow<br><br>Message: %s" % ( url, str( e ) ) ) 836 # Put parameters in workflow mode 837 trans.workflow_building_mode = True 838 # Create new workflow from incoming data 839 workflow = model.Workflow() 840 # Just keep the last name (user can rename later) 841 workflow.name = data['name'] 842 # Assume no errors until we find a step that has some 843 workflow.has_errors = False 844 # Create each step 845 steps = [] 846 # The editor will provide ids for each step that we don't need to save, 847 # but do need to use to make connections 848 steps_by_external_id = {} 849 # First pass to build step objects and populate basic values 850 for key, step_dict in data['steps'].iteritems(): 851 # Create the model class for the step 852 step = model.WorkflowStep() 853 steps.append( step ) 854 steps_by_external_id[ step_dict['id' ] ] = step 855 # FIXME: Position should be handled inside module 856 step.position = step_dict['position'] 857 module = module_factory.from_dict( trans, step_dict, secure=False ) 858 module.save_to_step( step ) 859 if step.tool_errors: 860 workflow.has_errors = True 861 # Stick this in the step temporarily 862 step.temp_input_connections = step_dict['input_connections'] 863 # Save step annotation. 864 annotation = step_dict[ 'annotation' ] 865 if annotation: 866 annotation = sanitize_html( annotation, 'utf-8', 'text/html' ) 867 self.add_item_annotation( trans, step, annotation ) 868 # Second pass to deal with connections between steps 869 for step in steps: 870 # Input connections 871 for input_name, conn_dict in step.temp_input_connections.iteritems(): 872 if conn_dict: 873 conn = model.WorkflowStepConnection() 874 conn.input_step = step 875 conn.input_name = input_name 876 conn.output_name = conn_dict['output_name'] 877 conn.output_step = steps_by_external_id[ conn_dict['id'] ] 878 del step.temp_input_connections 879 # Order the steps if possible 880 attach_ordered_steps( workflow, steps ) 881 # Connect up 882 stored = model.StoredWorkflow() 883 stored.name = workflow.name 884 workflow.stored_workflow = stored 885 stored.latest_workflow = workflow 886 stored.user = trans.user 887 # Persist 888 trans.sa_session.add( stored ) 889 trans.sa_session.flush() 890 # Return something informative 891 errors = [] 892 if workflow.has_errors: 893 return trans.show_warn_message( "Imported, but some steps in this workflow have validation errors" ) 894 if workflow.has_cycles: 895 return trans.show_warn_message( "Imported, but this workflow contains cycles" ) 896 else: 897 return trans.show_message( "Workflow '%s' imported" % workflow.name ) 898 899 @web.json 900 def get_datatypes( self, trans ): 901 ext_to_class_name = dict() 902 classes = [] 903 for k, v in trans.app.datatypes_registry.datatypes_by_extension.iteritems(): 904 c = v.__class__ 905 ext_to_class_name[k] = c.__module__ + "." + c.__name__ 906 classes.append( c ) 907 class_to_classes = dict() 908 def visit_bases( types, cls ): 909 for base in cls.__bases__: 910 if issubclass( base, Data ): 911 types.add( base.__module__ + "." + base.__name__ ) 912 visit_bases( types, base ) 913 for c in classes: 914 n = c.__module__ + "." + c.__name__ 915 types = set( [ n ] ) 916 visit_bases( types, c ) 917 class_to_classes[ n ] = dict( ( t, True ) for t in types ) 918 return dict( ext_to_class_name=ext_to_class_name, class_to_classes=class_to_classes ) 919 920 @web.expose 921 def build_from_current_history( self, trans, job_ids=None, dataset_ids=None, workflow_name=None ): 922 user = trans.get_user() 923 history = trans.get_history() 924 if not user: 925 return trans.show_error_message( "Must be logged in to create workflows" ) 926 if ( job_ids is None and dataset_ids is None ) or workflow_name is None: 927 jobs, warnings = get_job_dict( trans ) 928 # Render 929 return trans.fill_template( 930 "workflow/build_from_current_history.mako", 931 jobs=jobs, 932 warnings=warnings, 933 history=history ) 934 else: 935 # Ensure job_ids and dataset_ids are lists (possibly empty) 936 if job_ids is None: 937 job_ids = [] 938 elif type( job_ids ) is not list: 939 job_ids = [ job_ids ] 940 if dataset_ids is None: 941 dataset_ids = [] 942 elif type( dataset_ids ) is not list: 943 dataset_ids = [ dataset_ids ] 944 # Convert both sets of ids to integers 945 job_ids = [ int( id ) for id in job_ids ] 946 dataset_ids = [ int( id ) for id in dataset_ids ] 947 # Find each job, for security we (implicately) check that they are 948 # associated witha job in the current history. 949 jobs, warnings = get_job_dict( trans ) 950 jobs_by_id = dict( ( job.id, job ) for job in jobs.keys() ) 951 steps = [] 952 steps_by_job_id = {} 953 hid_to_output_pair = {} 954 # Input dataset steps 955 for hid in dataset_ids: 956 step = model.WorkflowStep() 957 step.type = 'data_input' 958 hid_to_output_pair[ hid ] = ( step, 'output' ) 959 steps.append( step ) 960 # Tool steps 961 for job_id in job_ids: 962 assert job_id in jobs_by_id, "Attempt to create workflow with job not connected to current history" 963 job = jobs_by_id[ job_id ] 964 tool = trans.app.toolbox.tools_by_id[ job.tool_id ] 965 param_values = job.get_param_values( trans.app ) 966 associations = cleanup_param_values( tool.inputs, param_values ) 967 step = model.WorkflowStep() 968 step.type = 'tool' 969 step.tool_id = job.tool_id 970 step.tool_inputs = tool.params_to_strings( param_values, trans.app ) 971 # NOTE: We shouldn't need to do two passes here since only 972 # an earlier job can be used as an input to a later 973 # job. 974 for other_hid, input_name in associations: 975 if other_hid in hid_to_output_pair: 976 other_step, other_name = hid_to_output_pair[ other_hid ] 977 conn = model.WorkflowStepConnection() 978 conn.input_step = step 979 conn.input_name = input_name 980 # Should always be connected to an earlier step 981 conn.output_step = other_step 982 conn.output_name = other_name 983 steps.append( step ) 984 steps_by_job_id[ job_id ] = step 985 # Store created dataset hids 986 for assoc in job.output_datasets: 987 hid_to_output_pair[ assoc.dataset.hid ] = ( step, assoc.name ) 988 # Workflow to populate 989 workflow = model.Workflow() 990 workflow.name = workflow_name 991 # Order the steps if possible 992 attach_ordered_steps( workflow, steps ) 993 # And let's try to set up some reasonable locations on the canvas 994 # (these are pretty arbitrary values) 995 levorder = order_workflow_steps_with_levels( steps ) 996 base_pos = 10 997 for i, steps_at_level in enumerate( levorder ): 998 for j, index in enumerate( steps_at_level ): 999 step = steps[ index ] 1000 step.position = dict( top = ( base_pos + 120 * j ), 1001 left = ( base_pos + 220 * i ) ) 1002 # Store it 1003 stored = model.StoredWorkflow() 1004 stored.user = user 1005 stored.name = workflow_name 1006 workflow.stored_workflow = stored 1007 stored.latest_workflow = workflow 1008 trans.sa_session.add( stored ) 1009 trans.sa_session.flush() 1010 # Index page with message 1011 return trans.show_message( "Workflow '%s' created from current history." % workflow_name ) 1012 ## return trans.show_ok_message( "<p>Workflow '%s' created.</p><p><a target='_top' href='%s'>Click to load in workflow editor</a></p>" 1013 ## % ( workflow_name, web.url_for( action='editor', id=trans.security.encode_id(stored.id) ) ) ) 1014 1015 @web.expose 1016 def run( self, trans, id, check_user=True, **kwargs ): 1017 stored = self.get_stored_workflow( trans, id, check_ownership=False ) 1018 if check_user: 1019 user = trans.get_user() 1020 if stored.user != user: 1021 if trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ 1022 .filter_by( user=user, stored_workflow=stored ).count() == 0: 1023 error( "Workflow is not owned by or shared with current user" ) 1024 # Get the latest revision 1025 workflow = stored.latest_workflow 1026 # It is possible for a workflow to have 0 steps 1027 if len( workflow.steps ) == 0: 1028 error( "Workflow cannot be run because it does not have any steps" ) 1029 #workflow = Workflow.from_simple( simplejson.loads( stored.encoded_value ), trans.app ) 1030 if workflow.has_cycles: 1031 error( "Workflow cannot be run because it contains cycles" ) 1032 if workflow.has_errors: 1033 error( "Workflow cannot be run because of validation errors in some steps" ) 1034 # Build the state for each step 1035 errors = {} 1036 has_upgrade_messages = False 1037 has_errors = False 1038 if kwargs: 1039 # If kwargs were provided, the states for each step should have 1040 # been POSTed 1041 for step in workflow.steps: 1042 step.upgrade_messages = {} 1043 # Connections by input name 1044 step.input_connections_by_name = \ 1045 dict( ( conn.input_name, conn ) for conn in step.input_connections ) 1046 # Extract just the arguments for this step by prefix 1047 p = "%s|" % step.id 1048 l = len(p) 1049 …
Large files files are truncated, but you can click here to view the full file