PageRenderTime 42ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

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

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