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