PageRenderTime 115ms CodeModel.GetById 11ms app.highlight 90ms RepoModel.GetById 1ms app.codeStats 1ms

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

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