PageRenderTime 313ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/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

Large files files are truncated, but you can click here to view the full 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 = pa

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