PageRenderTime 58ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

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

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