[2] | 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 ) |
---|