PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/galaxy/web/controllers/history.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 1162 lines | 1133 code | 4 blank | 25 comment | 72 complexity | cec238f621bdf8d484782cfa21a226f9 MD5 | raw file
  1. from galaxy.web.base.controller import *
  2. from galaxy.web.framework.helpers import time_ago, iff, grids
  3. from galaxy.datatypes.data import nice_size
  4. from galaxy import model, util
  5. from galaxy.util.odict import odict
  6. from galaxy.model.mapping import desc
  7. from galaxy.model.orm import *
  8. from galaxy.model.item_attrs import *
  9. from galaxy.util.json import *
  10. from galaxy.util.sanitize_html import sanitize_html
  11. from galaxy.tools.parameters.basic import UnvalidatedValue
  12. from galaxy.tools.actions import upload_common
  13. from galaxy.tags.tag_handler import GalaxyTagHandler
  14. from sqlalchemy.sql.expression import ClauseElement, func
  15. from sqlalchemy.sql import select
  16. import webhelpers, logging, operator, os, tempfile, subprocess, shutil, tarfile
  17. from datetime import datetime
  18. from cgi import escape
  19. log = logging.getLogger( __name__ )
  20. class NameColumn( grids.TextColumn ):
  21. def get_value( self, trans, grid, history ):
  22. return history.get_display_name()
  23. class HistoryListGrid( grids.Grid ):
  24. # Custom column types
  25. class DatasetsByStateColumn( grids.GridColumn ):
  26. def get_value( self, trans, grid, history ):
  27. # Build query to get (state, count) pairs.
  28. cols_to_select = [ trans.app.model.Dataset.table.c.state, func.count( '*' ) ]
  29. from_obj = trans.app.model.HistoryDatasetAssociation.table.join( trans.app.model.Dataset.table )
  30. where_clause = and_( trans.app.model.HistoryDatasetAssociation.table.c.history_id == history.id,
  31. trans.app.model.HistoryDatasetAssociation.table.c.deleted == False,
  32. trans.app.model.HistoryDatasetAssociation.table.c.visible == True,
  33. )
  34. group_by = trans.app.model.Dataset.table.c.state
  35. query = select( columns=cols_to_select,
  36. from_obj=from_obj,
  37. whereclause=where_clause,
  38. group_by=group_by )
  39. # Process results.
  40. state_count_dict = {}
  41. for row in trans.sa_session.execute( query ):
  42. state, count = row
  43. state_count_dict[ state ] = count
  44. rval = []
  45. for state in ( 'ok', 'running', 'queued', 'error' ):
  46. count = state_count_dict.get( state, 0 )
  47. if count:
  48. rval.append( '<div class="count-box state-color-%s">%s</div>' % ( state, count ) )
  49. else:
  50. rval.append( '' )
  51. return rval
  52. class HistoryListNameColumn( NameColumn ):
  53. def get_link( self, trans, grid, history ):
  54. link = None
  55. if not history.deleted:
  56. link = dict( operation="Switch", id=history.id, use_panels=grid.use_panels )
  57. return link
  58. # Grid definition
  59. title = "Saved Histories"
  60. model_class = model.History
  61. template='/history/grid.mako'
  62. default_sort_key = "-update_time"
  63. columns = [
  64. HistoryListNameColumn( "Name", key="name", attach_popup=True, filterable="advanced" ),
  65. DatasetsByStateColumn( "Datasets", key="datasets_by_state", ncells=4, sortable=False ),
  66. grids.IndividualTagsColumn( "Tags", key="tags", model_tag_association_class=model.HistoryTagAssociation, \
  67. filterable="advanced", grid_name="HistoryListGrid" ),
  68. grids.SharingStatusColumn( "Sharing", key="sharing", filterable="advanced", sortable=False ),
  69. grids.GridColumn( "Size on Disk", key="get_disk_size_bytes", format=nice_size, sortable=False ),
  70. grids.GridColumn( "Created", key="create_time", format=time_ago ),
  71. grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
  72. # Columns that are valid for filtering but are not visible.
  73. grids.DeletedColumn( "Status", key="deleted", visible=False, filterable="advanced" )
  74. ]
  75. columns.append(
  76. grids.MulticolFilterColumn(
  77. "search history names and tags",
  78. cols_to_filter=[ columns[0], columns[2] ],
  79. key="free-text-search", visible=False, filterable="standard" )
  80. )
  81. operations = [
  82. grids.GridOperation( "Switch", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ),
  83. grids.GridOperation( "Share or Publish", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ),
  84. grids.GridOperation( "Rename", condition=( lambda item: not item.deleted ), async_compatible=False ),
  85. grids.GridOperation( "Delete", condition=( lambda item: not item.deleted ), async_compatible=True ),
  86. grids.GridOperation( "Delete and remove datasets from disk", condition=( lambda item: not item.deleted ), async_compatible=True ),
  87. grids.GridOperation( "Undelete", condition=( lambda item: item.deleted ), async_compatible=True ),
  88. ]
  89. standard_filters = [
  90. grids.GridColumnFilter( "Active", args=dict( deleted=False ) ),
  91. grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ),
  92. grids.GridColumnFilter( "All", args=dict( deleted='All' ) ),
  93. ]
  94. default_filter = dict( name="All", deleted="False", tags="All", sharing="All" )
  95. num_rows_per_page = 50
  96. preserve_state = False
  97. use_async = True
  98. use_paging = True
  99. def get_current_item( self, trans, **kwargs ):
  100. return trans.get_history()
  101. def apply_query_filter( self, trans, query, **kwargs ):
  102. return query.filter_by( user=trans.user, purged=False, importing=False )
  103. class SharedHistoryListGrid( grids.Grid ):
  104. # Custom column types
  105. class DatasetsByStateColumn( grids.GridColumn ):
  106. def get_value( self, trans, grid, history ):
  107. rval = []
  108. for state in ( 'ok', 'running', 'queued', 'error' ):
  109. total = sum( 1 for d in history.active_datasets if d.state == state )
  110. if total:
  111. rval.append( '<div class="count-box state-color-%s">%s</div>' % ( state, total ) )
  112. else:
  113. rval.append( '' )
  114. return rval
  115. class SharedByColumn( grids.GridColumn ):
  116. def get_value( self, trans, grid, history ):
  117. return history.user.email
  118. # Grid definition
  119. title = "Histories shared with you by others"
  120. model_class = model.History
  121. default_sort_key = "-update_time"
  122. default_filter = {}
  123. columns = [
  124. grids.GridColumn( "Name", key="name", attach_popup=True ), # link=( lambda item: dict( operation="View", id=item.id ) ), attach_popup=True ),
  125. DatasetsByStateColumn( "Datasets", ncells=4, sortable=False ),
  126. grids.GridColumn( "Created", key="create_time", format=time_ago ),
  127. grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
  128. SharedByColumn( "Shared by", key="user_id" )
  129. ]
  130. operations = [
  131. grids.GridOperation( "View", allow_multiple=False, target="_top" ),
  132. grids.GridOperation( "Clone" ),
  133. grids.GridOperation( "Unshare" )
  134. ]
  135. standard_filters = []
  136. def build_initial_query( self, trans, **kwargs ):
  137. return trans.sa_session.query( self.model_class ).join( 'users_shared_with' )
  138. def apply_query_filter( self, trans, query, **kwargs ):
  139. return query.filter( model.HistoryUserShareAssociation.user == trans.user )
  140. class HistoryAllPublishedGrid( grids.Grid ):
  141. class NameURLColumn( grids.PublicURLColumn, NameColumn ):
  142. pass
  143. title = "Published Histories"
  144. model_class = model.History
  145. default_sort_key = "update_time"
  146. default_filter = dict( public_url="All", username="All", tags="All" )
  147. use_async = True
  148. columns = [
  149. NameURLColumn( "Name", key="name", filterable="advanced" ),
  150. grids.OwnerAnnotationColumn( "Annotation", key="annotation", model_annotation_association_class=model.HistoryAnnotationAssociation, filterable="advanced" ),
  151. grids.OwnerColumn( "Owner", key="username", model_class=model.User, filterable="advanced" ),
  152. grids.CommunityRatingColumn( "Community Rating", key="rating" ),
  153. grids.CommunityTagsColumn( "Community Tags", key="tags", model_tag_association_class=model.HistoryTagAssociation, filterable="advanced", grid_name="PublicHistoryListGrid" ),
  154. grids.ReverseSortColumn( "Last Updated", key="update_time", format=time_ago )
  155. ]
  156. columns.append(
  157. grids.MulticolFilterColumn(
  158. "Search name, annotation, owner, and tags",
  159. cols_to_filter=[ columns[0], columns[1], columns[2], columns[4] ],
  160. key="free-text-search", visible=False, filterable="standard" )
  161. )
  162. operations = []
  163. def build_initial_query( self, trans, **kwargs ):
  164. # Join so that searching history.user makes sense.
  165. return trans.sa_session.query( self.model_class ).join( model.User.table )
  166. def apply_query_filter( self, trans, query, **kwargs ):
  167. # A public history is published, has a slug, and is not deleted.
  168. return query.filter( self.model_class.published == True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False )
  169. class HistoryController( BaseUIController, Sharable, UsesAnnotations, UsesItemRatings, UsesHistory ):
  170. @web.expose
  171. def index( self, trans ):
  172. return ""
  173. @web.expose
  174. def list_as_xml( self, trans ):
  175. """XML history list for functional tests"""
  176. trans.response.set_content_type( 'text/xml' )
  177. return trans.fill_template( "/history/list_as_xml.mako" )
  178. stored_list_grid = HistoryListGrid()
  179. shared_list_grid = SharedHistoryListGrid()
  180. published_list_grid = HistoryAllPublishedGrid()
  181. @web.expose
  182. def list_published( self, trans, **kwargs ):
  183. grid = self.published_list_grid( trans, **kwargs )
  184. if 'async' in kwargs:
  185. return grid
  186. else:
  187. # Render grid wrapped in panels
  188. return trans.fill_template( "history/list_published.mako", grid=grid )
  189. @web.expose
  190. @web.require_login( "work with multiple histories" )
  191. def list( self, trans, **kwargs ):
  192. """List all available histories"""
  193. current_history = trans.get_history()
  194. status = message = None
  195. if 'operation' in kwargs:
  196. operation = kwargs['operation'].lower()
  197. if operation == "share or publish":
  198. return self.sharing( trans, **kwargs )
  199. if operation == "rename" and kwargs.get('id', None): # Don't call rename if no ids
  200. if 'name' in kwargs:
  201. del kwargs['name'] # Remove ajax name param that rename method uses
  202. return self.rename( trans, **kwargs )
  203. history_ids = util.listify( kwargs.get( 'id', [] ) )
  204. # Display no message by default
  205. status, message = None, None
  206. refresh_history = False
  207. # Load the histories and ensure they all belong to the current user
  208. histories = []
  209. for history_id in history_ids:
  210. history = self.get_history( trans, history_id )
  211. if history:
  212. # Ensure history is owned by current user
  213. if history.user_id != None and trans.user:
  214. assert trans.user.id == history.user_id, "History does not belong to current user"
  215. histories.append( history )
  216. else:
  217. log.warn( "Invalid history id '%r' passed to list", history_id )
  218. if histories:
  219. if operation == "switch":
  220. status, message = self._list_switch( trans, histories )
  221. # Take action to update UI to reflect history switch. If
  222. # grid is using panels, it is standalone and hence a redirect
  223. # to root is needed; if grid is not using panels, it is nested
  224. # in the main Galaxy UI and refreshing the history frame
  225. # is sufficient.
  226. use_panels = kwargs.get('use_panels', False) == 'True'
  227. if use_panels:
  228. return trans.response.send_redirect( url_for( "/" ) )
  229. else:
  230. trans.template_context['refresh_frames'] = ['history']
  231. elif operation in ( "delete", "delete and remove datasets from disk" ):
  232. if operation == "delete and remove datasets from disk":
  233. status, message = self._list_delete( trans, histories, purge=True )
  234. else:
  235. status, message = self._list_delete( trans, histories )
  236. if current_history in histories:
  237. # Deleted the current history, so a new, empty history was
  238. # created automatically, and we need to refresh the history frame
  239. trans.template_context['refresh_frames'] = ['history']
  240. elif operation == "undelete":
  241. status, message = self._list_undelete( trans, histories )
  242. elif operation == "unshare":
  243. for history in histories:
  244. for husa in trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \
  245. .filter_by( history=history ):
  246. trans.sa_session.delete( husa )
  247. elif operation == "enable import via link":
  248. for history in histories:
  249. if not history.importable:
  250. self._make_item_importable( trans.sa_session, history )
  251. elif operation == "disable import via link":
  252. if history_ids:
  253. histories = [ self.get_history( trans, history_id ) for history_id in history_ids ]
  254. for history in histories:
  255. if history.importable:
  256. history.importable = False
  257. trans.sa_session.flush()
  258. # Render the list view
  259. return self.stored_list_grid( trans, status=status, message=message, **kwargs )
  260. def _list_delete( self, trans, histories, purge=False ):
  261. """Delete histories"""
  262. n_deleted = 0
  263. deleted_current = False
  264. message_parts = []
  265. status = SUCCESS
  266. for history in histories:
  267. if history.users_shared_with:
  268. message_parts.append( "History (%s) has been shared with others, unshare it before deleting it. " % history.name )
  269. status = ERROR
  270. elif not history.deleted:
  271. # We'll not eliminate any DefaultHistoryPermissions in case we undelete the history later
  272. history.deleted = True
  273. # If deleting the current history, make a new current.
  274. if history == trans.get_history():
  275. deleted_current = True
  276. trans.new_history()
  277. trans.log_event( "History (%s) marked as deleted" % history.name )
  278. n_deleted += 1
  279. if purge and trans.app.config.allow_user_dataset_purge:
  280. for hda in history.datasets:
  281. if trans.user:
  282. trans.user.total_disk_usage -= hda.quota_amount( trans.user )
  283. hda.purged = True
  284. trans.sa_session.add( hda )
  285. trans.log_event( "HDA id %s has been purged" % hda.id )
  286. trans.sa_session.flush()
  287. if hda.dataset.user_can_purge:
  288. try:
  289. hda.dataset.full_delete()
  290. trans.log_event( "Dataset id %s has been purged upon the the purge of HDA id %s" % ( hda.dataset.id, hda.id ) )
  291. trans.sa_session.add( hda.dataset )
  292. except:
  293. log.exception( 'Unable to purge dataset (%s) on purge of hda (%s):' % ( hda.dataset.id, hda.id ) )
  294. trans.sa_session.flush()
  295. if n_deleted:
  296. part = "Deleted %d %s" % ( n_deleted, iff( n_deleted != 1, "histories", "history" ) )
  297. if purge and trans.app.config.allow_user_dataset_purge:
  298. part += " and removed %s datasets from disk" % iff( n_deleted != 1, "their", "its" )
  299. elif purge:
  300. part += " but the datasets were not removed from disk because that feature is not enabled in this Galaxy instance"
  301. message_parts.append( "%s. " % part )
  302. if deleted_current:
  303. message_parts.append( "Your active history was deleted, a new empty history is now active. " )
  304. status = INFO
  305. return ( status, " ".join( message_parts ) )
  306. def _list_undelete( self, trans, histories ):
  307. """Undelete histories"""
  308. n_undeleted = 0
  309. n_already_purged = 0
  310. for history in histories:
  311. if history.purged:
  312. n_already_purged += 1
  313. if history.deleted:
  314. history.deleted = False
  315. if not history.default_permissions:
  316. # For backward compatibility - for a while we were deleting all DefaultHistoryPermissions on
  317. # the history when we deleted the history. We are no longer doing this.
  318. # Need to add default DefaultHistoryPermissions in case they were deleted when the history was deleted
  319. default_action = trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS
  320. private_user_role = trans.app.security_agent.get_private_user_role( history.user )
  321. default_permissions = {}
  322. default_permissions[ default_action ] = [ private_user_role ]
  323. trans.app.security_agent.history_set_default_permissions( history, default_permissions )
  324. n_undeleted += 1
  325. trans.log_event( "History (%s) %d marked as undeleted" % ( history.name, history.id ) )
  326. status = SUCCESS
  327. message_parts = []
  328. if n_undeleted:
  329. message_parts.append( "Undeleted %d %s. " % ( n_undeleted, iff( n_undeleted != 1, "histories", "history" ) ) )
  330. if n_already_purged:
  331. message_parts.append( "%d histories have already been purged and cannot be undeleted." % n_already_purged )
  332. status = WARNING
  333. return status, "".join( message_parts )
  334. def _list_switch( self, trans, histories ):
  335. """Switch to a new different history"""
  336. new_history = histories[0]
  337. galaxy_session = trans.get_galaxy_session()
  338. try:
  339. association = trans.sa_session.query( trans.app.model.GalaxySessionToHistoryAssociation ) \
  340. .filter_by( session_id=galaxy_session.id, history_id=trans.security.decode_id( new_history.id ) ) \
  341. .first()
  342. except:
  343. association = None
  344. new_history.add_galaxy_session( galaxy_session, association=association )
  345. trans.sa_session.add( new_history )
  346. trans.sa_session.flush()
  347. trans.set_history( new_history )
  348. # No message
  349. return None, None
  350. @web.expose
  351. @web.require_login( "work with shared histories" )
  352. def list_shared( self, trans, **kwargs ):
  353. """List histories shared with current user by others"""
  354. msg = util.restore_text( kwargs.get( 'msg', '' ) )
  355. status = message = None
  356. if 'operation' in kwargs:
  357. ids = util.listify( kwargs.get( 'id', [] ) )
  358. operation = kwargs['operation'].lower()
  359. if operation == "view":
  360. # Display history.
  361. history = self.get_history( trans, ids[0], False)
  362. return self.display_by_username_and_slug( trans, history.user.username, history.slug )
  363. elif operation == "clone":
  364. if not ids:
  365. message = "Select a history to clone"
  366. return self.shared_list_grid( trans, status='error', message=message, **kwargs )
  367. # When cloning shared histories, only copy active datasets
  368. new_kwargs = { 'clone_choice' : 'active' }
  369. return self.clone( trans, ids, **new_kwargs )
  370. elif operation == 'unshare':
  371. if not ids:
  372. message = "Select a history to unshare"
  373. return self.shared_list_grid( trans, status='error', message=message, **kwargs )
  374. histories = [ self.get_history( trans, history_id ) for history_id in ids ]
  375. for history in histories:
  376. # Current user is the user with which the histories were shared
  377. association = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=trans.user, history=history ).one()
  378. trans.sa_session.delete( association )
  379. trans.sa_session.flush()
  380. message = "Unshared %d shared histories" % len( ids )
  381. status = 'done'
  382. # Render the list view
  383. return self.shared_list_grid( trans, status=status, message=message, **kwargs )
  384. @web.expose
  385. def display_structured( self, trans, id=None ):
  386. """
  387. Display a history as a nested structure showing the jobs and workflow
  388. invocations that created each dataset (if any).
  389. """
  390. # Get history
  391. if id is None:
  392. id = trans.history.id
  393. else:
  394. id = trans.security.decode_id( id )
  395. # Expunge history from the session to allow us to force a reload
  396. # with a bunch of eager loaded joins
  397. trans.sa_session.expunge( trans.history )
  398. history = trans.sa_session.query( model.History ).options(
  399. eagerload_all( 'active_datasets.creating_job_associations.job.workflow_invocation_step.workflow_invocation.workflow' ),
  400. eagerload_all( 'active_datasets.children' )
  401. ).get( id )
  402. assert history
  403. assert history.user and ( history.user.id == trans.user.id ) or ( history.id == trans.history.id )
  404. # Resolve jobs and workflow invocations for the datasets in the history
  405. # items is filled with items (hdas, jobs, or workflows) that go at the
  406. # top level
  407. items = []
  408. # First go through and group hdas by job, if there is no job they get
  409. # added directly to items
  410. jobs = odict()
  411. for hda in history.active_datasets:
  412. if hda.visible == False:
  413. continue
  414. # Follow "copied from ..." association until we get to the original
  415. # instance of the dataset
  416. original_hda = hda
  417. ## while original_hda.copied_from_history_dataset_association:
  418. ## original_hda = original_hda.copied_from_history_dataset_association
  419. # Check if the job has a creating job, most should, datasets from
  420. # before jobs were tracked, or from the upload tool before it
  421. # created a job, may not
  422. if not original_hda.creating_job_associations:
  423. items.append( ( hda, None ) )
  424. # Attach hda to correct job
  425. # -- there should only be one creating_job_association, so this
  426. # loop body should only be hit once
  427. for assoc in original_hda.creating_job_associations:
  428. job = assoc.job
  429. if job in jobs:
  430. jobs[ job ].append( ( hda, None ) )
  431. else:
  432. jobs[ job ] = [ ( hda, None ) ]
  433. # Second, go through the jobs and connect to workflows
  434. wf_invocations = odict()
  435. for job, hdas in jobs.iteritems():
  436. # Job is attached to a workflow step, follow it to the
  437. # workflow_invocation and group
  438. if job.workflow_invocation_step:
  439. wf_invocation = job.workflow_invocation_step.workflow_invocation
  440. if wf_invocation in wf_invocations:
  441. wf_invocations[ wf_invocation ].append( ( job, hdas ) )
  442. else:
  443. wf_invocations[ wf_invocation ] = [ ( job, hdas ) ]
  444. # Not attached to a workflow, add to items
  445. else:
  446. items.append( ( job, hdas ) )
  447. # Finally, add workflow invocations to items, which should now
  448. # contain all hdas with some level of grouping
  449. items.extend( wf_invocations.items() )
  450. # Sort items by age
  451. items.sort( key=( lambda x: x[0].create_time ), reverse=True )
  452. #
  453. return trans.fill_template( "history/display_structured.mako", items=items )
  454. @web.expose
  455. def delete_current( self, trans ):
  456. """Delete just the active history -- this does not require a logged in user."""
  457. history = trans.get_history()
  458. if history.users_shared_with:
  459. return trans.show_error_message( "History (%s) has been shared with others, unshare it before deleting it. " % history.name )
  460. if not history.deleted:
  461. history.deleted = True
  462. trans.sa_session.add( history )
  463. trans.sa_session.flush()
  464. trans.log_event( "History id %d marked as deleted" % history.id )
  465. # Regardless of whether it was previously deleted, we make a new history active
  466. trans.new_history()
  467. return trans.show_ok_message( "History deleted, a new history is active", refresh_frames=['history'] )
  468. @web.expose
  469. @web.require_login( "rate items" )
  470. @web.json
  471. def rate_async( self, trans, id, rating ):
  472. """ Rate a history asynchronously and return updated community data. """
  473. history = self.get_history( trans, id, check_ownership=False, check_accessible=True )
  474. if not history:
  475. return trans.show_error_message( "The specified history does not exist." )
  476. # Rate history.
  477. history_rating = self.rate_item( trans.sa_session, trans.get_user(), history, rating )
  478. return self.get_ave_item_rating_data( trans.sa_session, history )
  479. @web.expose
  480. def rename_async( self, trans, id=None, new_name=None ):
  481. history = self.get_history( trans, id )
  482. # Check that the history exists, and is either owned by the current
  483. # user (if logged in) or the current history
  484. assert history is not None
  485. if history.user is None:
  486. assert history == trans.get_history()
  487. else:
  488. assert history.user == trans.user
  489. # Rename
  490. history.name = sanitize_html( new_name )
  491. trans.sa_session.add( history )
  492. trans.sa_session.flush()
  493. return history.name
  494. @web.expose
  495. @web.require_login( "use Galaxy histories" )
  496. def annotate_async( self, trans, id, new_annotation=None, **kwargs ):
  497. history = self.get_history( trans, id )
  498. if new_annotation:
  499. # Sanitize annotation before adding it.
  500. new_annotation = sanitize_html( new_annotation, 'utf-8', 'text/html' )
  501. self.add_item_annotation( trans.sa_session, trans.get_user(), history, new_annotation )
  502. trans.sa_session.flush()
  503. return new_annotation
  504. @web.expose
  505. # TODO: Remove require_login when users are warned that, if they are not
  506. # logged in, this will remove their current history.
  507. @web.require_login( "use Galaxy histories" )
  508. def import_archive( self, trans, **kwargs ):
  509. """ Import a history from a file archive. """
  510. # Set archive source and type.
  511. archive_file = kwargs.get( 'archive_file', None )
  512. archive_url = kwargs.get( 'archive_url', None )
  513. archive_source = None
  514. if archive_file:
  515. archive_source = archive_file
  516. archive_type = 'file'
  517. elif archive_url:
  518. archive_source = archive_url
  519. archive_type = 'url'
  520. # If no source to create archive from, show form to upload archive or specify URL.
  521. if not archive_source:
  522. return trans.show_form(
  523. web.FormBuilder( web.url_for(), "Import a History from an Archive", submit_text="Submit" ) \
  524. .add_input( "text", "Archived History URL", "archive_url", value="", error=None )
  525. # TODO: add support for importing via a file.
  526. #.add_input( "file", "Archived History File", "archive_file", value=None, error=None )
  527. )
  528. # Run job to do import.
  529. history_imp_tool = trans.app.toolbox.tools_by_id[ '__IMPORT_HISTORY__' ]
  530. incoming = { '__ARCHIVE_SOURCE__' : archive_source, '__ARCHIVE_TYPE__' : archive_type }
  531. history_imp_tool.execute( trans, incoming=incoming )
  532. return trans.show_message( "Importing history from '%s'. \
  533. This history will be visible when the import is complete" % archive_source )
  534. @web.expose
  535. def export_archive( self, trans, id=None, gzip=True, include_hidden=False, include_deleted=False ):
  536. """ Export a history to an archive. """
  537. #
  538. # Convert options to booleans.
  539. #
  540. if isinstance( gzip, basestring ):
  541. gzip = ( gzip in [ 'True', 'true', 'T', 't' ] )
  542. if isinstance( include_hidden, basestring ):
  543. include_hidden = ( include_hidden in [ 'True', 'true', 'T', 't' ] )
  544. if isinstance( include_deleted, basestring ):
  545. include_deleted = ( include_deleted in [ 'True', 'true', 'T', 't' ] )
  546. #
  547. # Get history to export.
  548. #
  549. if id:
  550. history = self.get_history( trans, id, check_ownership=False, check_accessible=True )
  551. else:
  552. # Use current history.
  553. history = trans.history
  554. id = trans.security.encode_id( history.id )
  555. if not history:
  556. return trans.show_error_message( "This history does not exist or you cannot export this history." )
  557. #
  558. # If history has already been exported and it has not changed since export, stream it.
  559. #
  560. jeha = trans.sa_session.query( model.JobExportHistoryArchive ).filter_by( history=history ) \
  561. .order_by( model.JobExportHistoryArchive.id.desc() ).first()
  562. if jeha and ( jeha.job.state not in [ model.Job.states.ERROR, model.Job.states.DELETED ] ) \
  563. and jeha.job.update_time > history.update_time:
  564. if jeha.job.state == model.Job.states.OK:
  565. # Stream archive.
  566. valid_chars = '.,^_-()[]0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  567. hname = history.name
  568. hname = ''.join(c in valid_chars and c or '_' for c in hname)[0:150]
  569. trans.response.headers["Content-Disposition"] = "attachment; filename=Galaxy-History-%s.tar" % ( hname )
  570. if jeha.compressed:
  571. trans.response.headers["Content-Disposition"] += ".gz"
  572. trans.response.set_content_type( 'application/x-gzip' )
  573. else:
  574. trans.response.set_content_type( 'application/x-tar' )
  575. return open( jeha.dataset.file_name )
  576. elif jeha.job.state in [ model.Job.states.RUNNING, model.Job.states.QUEUED, model.Job.states.WAITING ]:
  577. return trans.show_message( "Still exporting history %(n)s; please check back soon. Link: <a href='%(s)s'>%(s)s</a>" \
  578. % ( { 'n' : history.name, 's' : url_for( action="export_archive", id=id, qualified=True ) } ) )
  579. # Run job to do export.
  580. history_exp_tool = trans.app.toolbox.tools_by_id[ '__EXPORT_HISTORY__' ]
  581. params = {
  582. 'history_to_export' : history,
  583. 'compress' : gzip,
  584. 'include_hidden' : include_hidden,
  585. 'include_deleted' : include_deleted }
  586. history_exp_tool.execute( trans, incoming = params, set_output_hid = True )
  587. return trans.show_message( "Exporting History '%(n)s'. Use this link to download \
  588. the archive or import it to another Galaxy server: \
  589. <a href='%(u)s'>%(u)s</a>" \
  590. % ( { 'n' : history.name, 'u' : url_for( action="export_archive", id=id, qualified=True ) } ) )
  591. @web.expose
  592. @web.json
  593. @web.require_login( "get history name and link" )
  594. def get_name_and_link_async( self, trans, id=None ):
  595. """ Returns history's name and link. """
  596. history = self.get_history( trans, id, False )
  597. if self.create_item_slug( trans.sa_session, history ):
  598. trans.sa_session.flush()
  599. return_dict = {
  600. "name" : history.name,
  601. "link" : url_for( action="display_by_username_and_slug", username=history.user.username, slug=history.slug ) }
  602. return return_dict
  603. @web.expose
  604. @web.require_login( "set history's accessible flag" )
  605. def set_accessible_async( self, trans, id=None, accessible=False ):
  606. """ Set history's importable attribute and slug. """
  607. history = self.get_history( trans, id, True )
  608. # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed.
  609. importable = accessible in ['True', 'true', 't', 'T'];
  610. if history and history.importable != importable:
  611. if importable:
  612. self._make_item_accessible( trans.sa_session, history )
  613. else:
  614. history.importable = importable
  615. trans.sa_session.flush()
  616. return
  617. @web.expose
  618. @web.require_login( "modify Galaxy items" )
  619. def set_slug_async( self, trans, id, new_slug ):
  620. history = self.get_history( trans, id )
  621. if history:
  622. history.slug = new_slug
  623. trans.sa_session.flush()
  624. return history.slug
  625. @web.expose
  626. def get_item_content_async( self, trans, id ):
  627. """ Returns item content in HTML format. """
  628. history = self.get_history( trans, id, False, True )
  629. if history is None:
  630. raise web.httpexceptions.HTTPNotFound()
  631. # Get datasets.
  632. datasets = self.get_history_datasets( trans, history )
  633. # Get annotations.
  634. history.annotation = self.get_item_annotation_str( trans.sa_session, history.user, history )
  635. for dataset in datasets:
  636. dataset.annotation = self.get_item_annotation_str( trans.sa_session, history.user, dataset )
  637. return trans.stream_template_mako( "/history/item_content.mako", item = history, item_data = datasets )
  638. @web.expose
  639. def name_autocomplete_data( self, trans, q=None, limit=None, timestamp=None ):
  640. """Return autocomplete data for history names"""
  641. user = trans.get_user()
  642. if not user:
  643. return
  644. ac_data = ""
  645. for history in trans.sa_session.query( model.History ).filter_by( user=user ).filter( func.lower( model.History.name ) .like(q.lower() + "%") ):
  646. ac_data = ac_data + history.name + "\n"
  647. return ac_data
  648. @web.expose
  649. def imp( self, trans, id=None, confirm=False, **kwd ):
  650. """Import another user's history via a shared URL"""
  651. msg = ""
  652. user = trans.get_user()
  653. user_history = trans.get_history()
  654. # Set referer message
  655. if 'referer' in kwd:
  656. referer = kwd['referer']
  657. else:
  658. referer = trans.request.referer
  659. if referer is not "":
  660. referer_message = "<a href='%s'>return to the previous page</a>" % referer
  661. else:
  662. referer_message = "<a href='%s'>go to Galaxy's start page</a>" % url_for( '/' )
  663. # Do import.
  664. if not id:
  665. return trans.show_error_message( "You must specify a history you want to import.<br>You can %s." % referer_message, use_panels=True )
  666. import_history = self.get_history( trans, id, check_ownership=False, check_accessible=False )
  667. if not import_history:
  668. return trans.show_error_message( "The specified history does not exist.<br>You can %s." % referer_message, use_panels=True )
  669. # History is importable if user is admin or it's accessible. TODO: probably want to have app setting to enable admin access to histories.
  670. if not trans.user_is_admin() and not self.security_check( trans, import_history, check_ownership=False, check_accessible=True ):
  671. return trans.show_error_message( "You cannot access this history.<br>You can %s." % referer_message, use_panels=True )
  672. if user:
  673. #dan: I can import my own history.
  674. #if import_history.user_id == user.id:
  675. # return trans.show_error_message( "You cannot import your own history.<br>You can %s." % referer_message, use_panels=True )
  676. new_history = import_history.copy( target_user=user )
  677. new_history.name = "imported: " + new_history.name
  678. new_history.user_id = user.id
  679. galaxy_session = trans.get_galaxy_session()
  680. try:
  681. association = trans.sa_session.query( trans.app.model.GalaxySessionToHistoryAssociation ) \
  682. .filter_by( session_id=galaxy_session.id, history_id=new_history.id ) \
  683. .first()
  684. except:
  685. association = None
  686. new_history.add_galaxy_session( galaxy_session, association=association )
  687. trans.sa_session.add( new_history )
  688. trans.sa_session.flush()
  689. # Set imported history to be user's current history.
  690. trans.set_history( new_history )
  691. return trans.show_ok_message(
  692. message="""History "%s" has been imported. <br>You can <a href="%s">start using this history</a> or %s."""
  693. % ( new_history.name, web.url_for( '/' ), referer_message ), use_panels=True )
  694. elif not user_history or not user_history.datasets or confirm:
  695. new_history = import_history.copy()
  696. new_history.name = "imported: " + new_history.name
  697. new_history.user_id = None
  698. galaxy_session = trans.get_galaxy_session()
  699. try:
  700. association = trans.sa_session.query( trans.app.model.GalaxySessionToHistoryAssociation ) \
  701. .filter_by( session_id=galaxy_session.id, history_id=new_history.id ) \
  702. .first()
  703. except:
  704. association = None
  705. new_history.add_galaxy_session( galaxy_session, association=association )
  706. trans.sa_session.add( new_history )
  707. trans.sa_session.flush()
  708. trans.set_history( new_history )
  709. return trans.show_ok_message(
  710. message="""History "%s" has been imported. <br>You can <a href="%s">start using this history</a> or %s."""
  711. % ( new_history.name, web.url_for( '/' ), referer_message ), use_panels=True )
  712. return trans.show_warn_message( """
  713. Warning! If you import this history, you will lose your current
  714. history. <br>You can <a href="%s">continue and import this history</a> or %s.
  715. """ % ( web.url_for( id=id, confirm=True, referer=trans.request.referer ), referer_message ), use_panels=True )
  716. @web.expose
  717. def view( self, trans, id=None, show_deleted=False ):
  718. """View a history. If a history is importable, then it is viewable by any user."""
  719. # Get history to view.
  720. if not id:
  721. return trans.show_error_message( "You must specify a history you want to view." )
  722. history_to_view = self.get_history( trans, id, False)
  723. # Integrity checks.
  724. if not history_to_view:
  725. return trans.show_error_message( "The specified history does not exist." )
  726. # Admin users can view any history
  727. if not trans.user_is_admin() and not history_to_view.importable:
  728. error( "Either you are not allowed to view this history or the owner of this history has not made it accessible." )
  729. # View history.
  730. show_deleted = util.string_as_bool( show_deleted )
  731. datasets = self.get_history_datasets( trans, history_to_view, show_deleted=show_deleted )
  732. return trans.stream_template_mako( "history/view.mako",
  733. history = history_to_view,
  734. datasets = datasets,
  735. show_deleted = show_deleted )
  736. @web.expose
  737. def display_by_username_and_slug( self, trans, username, slug ):
  738. """ Display history based on a username and slug. """
  739. # Get history.
  740. session = trans.sa_session
  741. user = session.query( model.User ).filter_by( username=username ).first()
  742. history = trans.sa_session.query( model.History ).filter_by( user=user, slug=slug, deleted=False ).first()
  743. if history is None:
  744. raise web.httpexceptions.HTTPNotFound()
  745. # Security check raises error if user cannot access history.
  746. self.security_check( trans, history, False, True)
  747. # Get datasets.
  748. datasets = self.get_history_datasets( trans, history )
  749. # Get annotations.
  750. history.annotation = self.get_item_annotation_str( trans.sa_session, history.user, history )
  751. for dataset in datasets:
  752. dataset.annotation = self.get_item_annotation_str( trans.sa_session, history.user, dataset )
  753. # Get rating data.
  754. user_item_rating = 0
  755. if trans.get_user():
  756. user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), history )
  757. if user_item_rating:
  758. user_item_rating = user_item_rating.rating
  759. else:
  760. user_item_rating = 0
  761. ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, history )
  762. return trans.stream_template_mako( "history/display.mako", item = history, item_data = datasets,
  763. user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings )
  764. @web.expose
  765. @web.require_login( "share Galaxy histories" )
  766. def sharing( self, trans, id=None, histories=[], **kwargs ):
  767. """ Handle history sharing. """
  768. # Get session and histories.
  769. session = trans.sa_session
  770. # Id values take precedence over histories passed in; last resort is current history.
  771. if id:
  772. ids = util.listify( id )
  773. if ids:
  774. histories = [ self.get_history( trans, history_id ) for history_id in ids ]
  775. elif not histories:
  776. histories = [ trans.history ]
  777. # Do operation on histories.
  778. for history in histories:
  779. if 'make_accessible_via_link' in kwargs:
  780. self._make_item_accessible( trans.sa_session, history )
  781. elif 'make_accessible_and_publish' in kwargs:
  782. self._make_item_accessible( trans.sa_session, history )
  783. history.published = True
  784. elif 'publish' in kwargs:
  785. if history.importable:
  786. history.published = True
  787. else:
  788. # TODO: report error here.
  789. pass
  790. elif 'disable_link_access' in kwargs:
  791. history.importable = False
  792. elif 'unpublish' in kwargs:
  793. history.published = False
  794. elif 'disable_link_access_and_unpublish' in kwargs:
  795. history.importable = history.published = False
  796. elif 'unshare_user' in kwargs:
  797. user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( kwargs[ 'unshare_user' ] ) )
  798. # Look for and delete sharing relation for history-user.
  799. deleted_sharing_relation = False
  800. husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all()
  801. if husas:
  802. deleted_sharing_relation = True
  803. for husa in husas:
  804. trans.sa_session.delete( husa )
  805. if not deleted_sharing_relation:
  806. message = "History '%s' does not seem to be shared with user '%s'" % ( history.name, user.email )
  807. return trans.fill_template( '/sharing_base.mako', item=history,
  808. message=message, status='error' )
  809. # Legacy issue: histories made accessible before recent updates may not have a slug. Create slug for any histories that need them.
  810. for history in histories:
  811. if history.importable and not history.slug:
  812. self._make_item_accessible( trans.sa_session, history )
  813. session.flush()
  814. return trans.fill_template( "/sharing_base.mako", item=history )
  815. @web.expose
  816. @web.require_login( "share histories with other users" )
  817. def share( self, trans, id=None, email="", **kwd ):
  818. # If a history contains both datasets that can be shared and others that cannot be shared with the desired user,
  819. # then the entire history is shared, and the protected datasets will be visible, but inaccessible ( greyed out )
  820. # in the cloned history
  821. params = util.Params( kwd )
  822. user = trans.get_user()
  823. # TODO: we have too many error messages floating around in here - we need
  824. # to incorporate the messaging system used by the libraries that will display
  825. # a message on any page.
  826. err_msg = util.restore_text( params.get( 'err_msg', '' ) )
  827. if not email:
  828. if not id:
  829. # Default to the current history
  830. id = trans.security.encode_id( trans.history.id )
  831. id = util.listify( id )
  832. send_to_err = err_msg
  833. histories = []
  834. for history_id in id:
  835. histories.append( self.get_history( trans, history_id ) )
  836. return trans.fill_template( "/history/share.mako",
  837. histories=histories,
  838. email=email,
  839. send_to_err=send_to_err )
  840. histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email )
  841. if not send_to_users:
  842. if not send_to_err:
  843. send_to_err += "%s is not a valid Galaxy user. %s" % ( email, err_msg )
  844. return trans.fill_template( "/history/share.mako",
  845. histories=histories,
  846. email=email,
  847. send_to_err=send_to_err )
  848. if params.get( 'share_button', False ):
  849. # The user has not yet made a choice about how to share, so dictionaries will be built for display
  850. can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \
  851. self._populate_restricted( trans, user, histories, send_to_users, None, send_to_err, unique=True )
  852. send_to_err += err_msg
  853. if cannot_change and not no_change_needed and not can_change:
  854. send_to_err = "The histories you are sharing do not contain any datasets that can be accessed by the users with which you are sharing."
  855. return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err )
  856. if can_change or cannot_change:
  857. return trans.fill_template( "/history/share.mako",
  858. histories=histories,
  859. email=email,
  860. send_to_err=send_to_err,
  861. can_change=can_change,
  862. cannot_change=cannot_change,
  863. no_change_needed=unique_no_change_needed )
  864. if no_change_needed:
  865. return self._share_histories( trans, user, send_to_err, histories=no_change_needed )
  866. elif not send_to_err:
  867. # User seems to be sharing an empty history
  868. send_to_err = "You cannot share an empty history. "
  869. return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err )
  870. @web.expose
  871. @web.require_login( "share restricted histories with other users" )
  872. def share_restricted( self, trans, id=None, email="", **kwd ):
  873. if 'action' in kwd:
  874. action = kwd[ 'action' ]
  875. else:
  876. err_msg = "Select an action. "
  877. return trans.response.send_redirect( url_for( controller='history',
  878. action='share',
  879. id=id,
  880. email=email,
  881. err_msg=err_msg,
  882. share_button=True ) )
  883. user = trans.get_user()
  884. user_roles = user.all_roles()
  885. histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email )
  886. send_to_err = ''
  887. # The user has made a choice, so dictionaries will be built for sharing
  888. can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \
  889. self._populate_restricted( trans, user, histories, send_to_users, action, send_to_err )
  890. # Now that we've populated the can_change, cannot_change, and no_change_needed dictionaries,
  891. # we'll populate the histories_for_sharing dictionary from each of them.
  892. histories_for_sharing = {}
  893. if no_change_needed:
  894. # Don't need to change anything in cannot_change, so populate as is
  895. histories_for_sharing, send_to_err = \
  896. self._populate( trans, histories_for_sharing, no_change_needed, send_to_err )
  897. if cannot_change:
  898. # Can't change anything in cannot_change, so populate as is
  899. histories_for_sharing, send_to_err = \
  900. self._populate( trans, histories_for_sharing, cannot_change, send_to_err )
  901. # The action here is either 'public' or 'private', so we'll continue to populate the
  902. # histories_for_sharing dictionary from the can_change dictionary.
  903. for send_to_user, history_dict in can_change.items():
  904. for history in history_dict:
  905. # Make sure the current history has not already been shared with the current send_to_user
  906. if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \
  907. .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id,
  908. trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \
  909. .count() > 0:
  910. send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email )
  911. else:
  912. # Only deal with datasets that have not been purged
  913. for hda in history.activatable_datasets:
  914. # If the current dataset is not public, we may need to perform an action on it to
  915. # make it accessible by the other user.
  916. if not trans.app.security_agent.can_access_dataset( send_to_user.all_roles(), hda.dataset ):
  917. # The user with which we are sharing the history does not have access permission on the current dataset
  918. if trans.app.security_agent.can_manage_dataset( user_roles, hda.dataset ) and not hda.dataset.library_associations:
  919. # The current user has authority to change permissions on the current dataset because
  920. # they have permission to manage permissions on the dataset and the dataset is not associated
  921. # with a library.
  922. if action == "private":
  923. trans.app.security_agent.privately_share_dataset( hda.dataset, users=[ user, send_to_user ] )
  924. elif action == "public":
  925. trans.app.security_agent.make_dataset_public( hda.dataset )
  926. # Populate histories_for_sharing with the history after performing any requested actions on
  927. # it's datasets to make them accessible by the other user.
  928. if send_to_user not in histories_for_sharing:
  929. histories_for_sharing[ send_to_user ] = [ history ]
  930. elif history not in histories_for_sharing[ send_to_user ]:
  931. histories_for_sharing[ send_to_user ].append( history )
  932. return self._share_histories( trans, user, send_to_err, histories=histories_for_sharing )
  933. def _get_histories_and_users( self, trans, user, id, email ):
  934. if not id:
  935. # Default to the current history
  936. id = trans.security.encode_id( trans.history.id )
  937. id = util.listify( id )
  938. send_to_err = ""
  939. histories = []
  940. for history_id in id:
  941. histories.append( self.get_history( trans, history_id ) )
  942. send_to_users = []
  943. for email_address in util.listify( email ):
  944. email_address = email_address.strip()
  945. if email_address:
  946. if email_address == user.email:
  947. send_to_err += "You cannot send histories to yourself. "
  948. else:
  949. send_to_user = trans.sa_session.query( trans.app.model.User ) \
  950. .filter( and_( trans.app.model.User.table.c.email==email_address,
  951. trans.app.model.User.table.c.deleted==False ) ) \
  952. .first()
  953. if send_to_user:
  954. send_to_users.append( send_to_user )
  955. else:
  956. send_to_err += "%s is not a valid Galaxy user. " % email_address
  957. return histories, send_to_users, send_to_err
  958. def _populate( self, trans, histories_for_sharing, other, send_to_err ):
  959. # This method will populate the histories_for_sharing dictionary with the users and
  960. # histories in other, eliminating histories that have already been shared with the
  961. # associated user. No security checking on datasets is performed.
  962. # If not empty, the histories_for_sharing dictionary looks like:
  963. # { userA: [ historyX, historyY ], userB: [ historyY ] }
  964. # other looks like:
  965. # { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} }
  966. for send_to_user, history_dict in other.items():
  967. for history in history_dict:
  968. # Make sure the current history has not already been shared with the current send_to_user
  969. if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \
  970. .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id,
  971. trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \
  972. .count() > 0:
  973. send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email )
  974. else:
  975. # Build the dict that will be used for sharing
  976. if send_to_user not in histories_for_sharing:
  977. histories_for_sharing[ send_to_user ] = [ history ]
  978. elif history not in histories_for_sharing[ send_to_user ]:
  979. histories_for_sharing[ send_to_user ].append( history )
  980. return histories_for_sharing, send_to_err
  981. def _populate_restricted( self, trans, user, histories, send_to_users, action, send_to_err, unique=False ):
  982. # The user may be attempting to share histories whose datasets cannot all be accessed by other users.
  983. # If this is the case, the user sharing the histories can:
  984. # 1) action=='public': choose to make the datasets public if he is permitted to do so
  985. # 2) action=='private': automatically create a new "sharing role" allowing protected
  986. # datasets to be accessed only by the desired users
  987. # This method will populate the can_change, cannot_change and no_change_needed dictionaries, which
  988. # are used for either displaying to the user, letting them make 1 of the choices above, or sharing
  989. # after the user has made a choice. They will be used for display if 'unique' is True, and will look
  990. # like: {historyX : [hda, hda], historyY : [hda] }
  991. # For sharing, they will look like:
  992. # { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} }
  993. can_change = {}
  994. cannot_change = {}
  995. no_change_needed = {}
  996. unique_no_change_needed = {}
  997. user_roles = user.all_roles()
  998. for history in histories:
  999. for send_to_user in send_to_users:
  1000. # Make sure the current history has not already been shared with the current send_to_user
  1001. if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \
  1002. .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id,
  1003. trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \
  1004. .count() > 0:
  1005. send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email )
  1006. else:
  1007. # Only deal with datasets that have not been purged
  1008. for hda in history.activatable_datasets:
  1009. if trans.app.security_agent.can_access_dataset( send_to_user.all_roles(), hda.dataset ):
  1010. # The no_change_needed dictionary is a special case. If both of can_change
  1011. # and cannot_change are empty, no_change_needed will used for sharing. Otherwise
  1012. # unique_no_change_needed will be used for displaying, so we need to populate both.
  1013. # Build the dictionaries for display, containing unique histories only
  1014. if history not in unique_no_change_needed:
  1015. unique_no_change_needed[ history ] = [ hda ]
  1016. else:
  1017. unique_no_change_needed[ history ].append( hda )
  1018. # Build the dictionaries for sharing
  1019. if send_to_user not in no_change_needed:
  1020. no_change_needed[ send_to_user ] = {}
  1021. if history not in no_change_needed[ send_to_user ]:
  1022. no_change_needed[ send_to_user ][ history ] = [ hda ]
  1023. else:
  1024. no_change_needed[ send_to_user ][ history ].append( hda )
  1025. else:
  1026. # The user with which we are sharing the history does not have access permission on the current dataset
  1027. if trans.app.security_agent.can_manage_dataset( user_roles, hda.dataset ):
  1028. # The current user has authority to change permissions on the current dataset because
  1029. # they have permission to manage permissions on the dataset.
  1030. # NOTE: ( gvk )There may be problems if the dataset also has an ldda, but I don't think so
  1031. # because the user with which we are sharing will not have the "manage permission" permission
  1032. # on the dataset in their history. Keep an eye on this though...
  1033. if unique:
  1034. # Build the dictionaries for display, containing unique histories only
  1035. if history not in can_change:
  1036. can_change[ history ] = [ hda ]
  1037. else:
  1038. can_change[ history ].append( hda )
  1039. else:
  1040. # Build the dictionaries for sharing
  1041. if send_to_user not in can_change:
  1042. can_change[ send_to_user ] = {}
  1043. if history not in can_change[ send_to_user ]:
  1044. can_change[ send_to_user ][ history ] = [ hda ]
  1045. else:
  1046. can_change[ send_to_user ][ history ].append( hda )
  1047. else:
  1048. if action in [ "private", "public" ]:
  1049. # The user has made a choice, so 'unique' doesn't apply. Don't change stuff
  1050. # that the user doesn't have permission to change
  1051. continue
  1052. if unique:
  1053. # Build the dictionaries for display, containing unique histories only
  1054. if history not in cannot_change:
  1055. cannot_change[ history ] = [ hda ]
  1056. else:
  1057. cannot_change[ history ].append( hda )
  1058. else:
  1059. # Build the dictionaries for sharing
  1060. if send_to_user not in cannot_change:
  1061. cannot_change[ send_to_user ] = {}
  1062. if history not in cannot_change[ send_to_user ]:
  1063. cannot_change[ send_to_user ][ history ] = [ hda ]
  1064. else:
  1065. cannot_change[ send_to_user ][ history ].append( hda )
  1066. return can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err
  1067. def _share_histories( self, trans, user, send_to_err, histories={} ):
  1068. # histories looks like: { userA: [ historyX, historyY ], userB: [ historyY ] }
  1069. msg = ""
  1070. sent_to_emails = []
  1071. for send_to_user in histories.keys():
  1072. sent_to_emails.append( send_to_user.email )
  1073. emails = ",".join( e for e in sent_to_emails )
  1074. if not histories:
  1075. send_to_err += "No users have been specified or no histories can be sent without changing permissions or associating a sharing role. "
  1076. else:
  1077. for send_to_user, send_to_user_histories in histories.items():
  1078. shared_histories = []
  1079. for history in send_to_user_histories:
  1080. share = trans.app.model.HistoryUserShareAssociation()
  1081. share.history = history
  1082. share.user = send_to_user
  1083. trans.sa_session.add( share )
  1084. self.create_item_slug( trans.sa_session, history )
  1085. trans.sa_session.flush()
  1086. if history not in shared_histories:
  1087. shared_histories.append( history )
  1088. if send_to_err:
  1089. msg += send_to_err
  1090. return self.sharing( trans, histories=shared_histories, msg=msg )
  1091. @web.expose
  1092. @web.require_login( "rename histories" )
  1093. def rename( self, trans, id=None, name=None, **kwd ):
  1094. user = trans.get_user()
  1095. if not id:
  1096. # Default to the current history
  1097. history = trans.get_history()
  1098. if not history.user:
  1099. return trans.show_error_message( "You must save your history before renaming it." )
  1100. id = trans.security.encode_id( history.id )
  1101. id = util.listify( id )
  1102. name = util.listify( name )
  1103. histories = []
  1104. cur_names = []
  1105. for history_id in id:
  1106. history = self.get_history( trans, history_id )
  1107. if history and history.user_id == user.id:
  1108. histories.append( history )
  1109. cur_names.append( history.get_display_name() )
  1110. if not name or len( histories ) != len( name ):
  1111. return trans.fill_template( "/history/rename.mako", histories=histories )
  1112. change_msg = ""
  1113. for i in ra