root/galaxy-central/lib/galaxy/web/controllers/history.py @ 2

リビジョン 2, 74.1 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

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