PageRenderTime 93ms CodeModel.GetById 15ms app.highlight 65ms RepoModel.GetById 3ms app.codeStats 0ms

/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

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

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