1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/65ec063664eb/ Changeset: 65ec063664eb User: carlfeberhard Date: 2014-07-10 15:56:17 Summary: HDA & History Managers: move serialization logic for bootstrapping into view, display_by_username_and_slug, and root/history into Managers in order to display dataset collections Affected #: 7 files diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/managers/__init__.py --- a/lib/galaxy/managers/__init__.py +++ b/lib/galaxy/managers/__init__.py @@ -1,4 +1,33 @@ -""" 'Business logic' independent of web transactions/user context (trans) -should be pushed into models - but logic that requires the context trans -should be placed under this module. """ +Classes that manage resources (models, tools, etc.) by using the current +Transaction. + +Encapsulates the intersection of trans (or trans.sa_session), models, +and Controllers. + +Responsibilities: + model operations that involve the trans/sa_session (CRUD) + security: + ownership, accessibility + common aspect-oriented operations via new mixins: + sharable, annotatable, tagable, ratable + +Not responsible for: + encoding/decoding ids + any http gobblygook + formatting of returned data (always python structures) + formatting of raised errors + +The goal is to have Controllers only handle: + query-string/payload parsing and encoding/decoding ids + http + return formatting + +and: + control, improve namespacing in Controllers + DRY for Controller ops (define here - use in both UI/API Controllers) + +In other words, 'Business logic' independent of web transactions/user context +(trans) should be pushed into models - but logic that requires the context +trans should be placed under this module. +""" diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/managers/base.py --- /dev/null +++ b/lib/galaxy/managers/base.py @@ -0,0 +1,7 @@ + + +class ModelManager( object ): + pass + +class ModelSerializer( object ): + pass diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/managers/hdas.py --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -1,16 +1,35 @@ +""" +Manager and Serializer for HDAs. + +HistoryDatasetAssociations (HDAs) are datasets contained or created in a +history. +""" + from galaxy import exceptions -from ..managers import histories +from galaxy.managers import base as manager_base +from galaxy.managers import histories as history_manager -class HDAManager( object ): +import galaxy.web +import galaxy.datatypes.metadata +from galaxy import objectstore + +class HDAManager( manager_base.ModelManager ): + """ + Interface/service object for interacting with HDAs. + """ def __init__( self ): - self.histories_mgr = histories.HistoryManager() + """ + Set up and initialize other managers needed by hdas. + """ + self.histories_mgr = history_manager.HistoryManager() def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True ): """ + Get an HDA by its unencoded db id, checking ownership (via its history) + or accessibility (via dataset shares/permissions). """ - # this is a replacement for UsesHistoryDatasetAssociationMixin because mixins are a bad soln/structure hda = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( unencoded_id ) if hda is None: raise exceptions.ObjectNotFound() @@ -19,7 +38,8 @@ def secure( self, trans, hda, check_ownership=True, check_accessible=True ): """ - checks if (a) user owns item or (b) item is accessible to user. + check ownership (via its history) or accessibility (via dataset + shares/permissions). """ # all items are accessible to an admin if trans.user and trans.user_is_admin(): @@ -31,12 +51,18 @@ return hda def can_access_dataset( self, trans, hda ): + """ + Use security agent to see if current user has access to dataset. + """ current_user_roles = trans.get_current_user_roles() return trans.app.security_agent.can_access_dataset( current_user_roles, hda.dataset ) #TODO: is_owner, is_accessible def check_ownership( self, trans, hda ): + """ + Use history to see if current user owns HDA. + """ if not trans.user: #if hda.history == trans.history: # return hda @@ -51,6 +77,9 @@ "HistoryDatasetAssociation is not owned by the current user", type='error' ) def check_accessible( self, trans, hda ): + """ + Raise error if HDA is not accessible. + """ if trans.user and trans.user_is_admin(): return hda # check for access of the containing history... @@ -62,6 +91,151 @@ "HistoryDatasetAssociation is not accessible to the current user", type='error' ) def err_if_uploading( self, trans, hda ): + """ + Raise error if HDA is still uploading. + """ if hda.state == trans.model.Dataset.states.UPLOAD: raise exceptions.Conflict( "Please wait until this dataset finishes uploading" ) return hda + + def get_hda_dict( self, trans, hda ): + """ + Return full details of this HDA in dictionary form. + """ + #precondition: the user's access to this hda has already been checked + #TODO:?? postcondition: all ids are encoded (is this really what we want at this level?) + expose_dataset_path = trans.user_is_admin() or trans.app.config.expose_dataset_path + hda_dict = hda.to_dict( view='element', expose_dataset_path=expose_dataset_path ) + hda_dict[ 'api_type' ] = "file" + + # Add additional attributes that depend on trans must be added here rather than at the model level. + can_access_hda = trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), hda.dataset ) + can_access_hda = ( trans.user_is_admin() or can_access_hda ) + if not can_access_hda: + return self.get_inaccessible_hda_dict( trans, hda ) + hda_dict[ 'accessible' ] = True + + #TODO: I'm unclear as to which access pattern is right + hda_dict[ 'annotation' ] = hda.get_item_annotation_str( trans.sa_session, hda.history.user, hda ) + #annotation = getattr( hda, 'annotation', hda.get_item_annotation_str( trans.sa_session, trans.user, hda ) ) + + # ---- return here if deleted AND purged OR can't access + purged = ( hda.purged or hda.dataset.purged ) + if ( hda.deleted and purged ): + #TODO: to_dict should really go AFTER this - only summary data + return trans.security.encode_dict_ids( hda_dict ) + + if expose_dataset_path: + try: + hda_dict[ 'file_name' ] = hda.file_name + except objectstore.ObjectNotFound: + log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) + + hda_dict[ 'download_url' ] = galaxy.web.url_for( 'history_contents_display', + history_id = trans.security.encode_id( hda.history.id ), + history_content_id = trans.security.encode_id( hda.id ) ) + + # indeces, assoc. metadata files, etc. + meta_files = [] + for meta_type in hda.metadata.spec.keys(): + if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): + meta_files.append( dict( file_type=meta_type ) ) + if meta_files: + hda_dict[ 'meta_files' ] = meta_files + + # currently, the viz reg is optional - handle on/off + if trans.app.visualizations_registry: + hda_dict[ 'visualizations' ] = trans.app.visualizations_registry.get_visualizations( trans, hda ) + else: + hda_dict[ 'visualizations' ] = hda.get_visualizations() + #TODO: it may also be wiser to remove from here and add as API call that loads the visualizations + # when the visualizations button is clicked (instead of preloading/pre-checking) + + # ---- return here if deleted + if hda.deleted and not purged: + return trans.security.encode_dict_ids( hda_dict ) + + return trans.security.encode_dict_ids( hda_dict ) + + def get_inaccessible_hda_dict( self, trans, hda ): + """ + Return truncated serialization of HDA when inaccessible to user. + """ + return trans.security.encode_dict_ids({ + 'id' : hda.id, + 'history_id': hda.history.id, + 'hid' : hda.hid, + 'name' : hda.name, + 'state' : hda.state, + 'deleted' : hda.deleted, + 'visible' : hda.visible, + 'accessible': False + }) + + def get_hda_dict_with_error( self, trans, hda=None, history_id=None, id=None, error_msg='Error' ): + """ + Return truncated serialization of HDA when error raised getting + details. + """ + return trans.security.encode_dict_ids({ + 'id' : hda.id if hda else id, + 'history_id': hda.history.id if hda else history_id, + 'hid' : hda.hid if hda else '(unknown)', + 'name' : hda.name if hda else '(unknown)', + 'error' : error_msg, + 'state' : trans.model.Dataset.states.NEW + }) + + def get_display_apps( self, trans, hda ): + """ + Return dictionary containing new-style display app urls. + """ + display_apps = [] + for display_app in hda.get_display_applications( trans ).itervalues(): + + app_links = [] + for link_app in display_app.links.itervalues(): + app_links.append({ + 'target': link_app.url.get( 'target_frame', '_blank' ), + 'href' : link_app.get_display_url( hda, trans ), + 'text' : gettext( link_app.name ) + }) + if app_links: + display_apps.append( dict( label=display_app.name, links=app_links ) ) + + return display_apps + + def get_old_display_applications( self, trans, hda ): + """ + Return dictionary containing old-style display app urls. + """ + display_apps = [] + if not trans.app.config.enable_old_display_applications: + return display_apps + + for display_app in hda.datatype.get_display_types(): + target_frame, display_links = hda.datatype.get_display_links( hda, + display_app, trans.app, trans.request.base ) + + if len( display_links ) > 0: + display_label = hda.datatype.get_display_label( display_app ) + + app_links = [] + for display_name, display_link in display_links: + app_links.append({ + 'target': target_frame, + 'href' : display_link, + 'text' : gettext( display_name ) + }) + if app_links: + display_apps.append( dict( label=display_label, links=app_links ) ) + + return display_apps + + +# ============================================================================= +class HistorySerializer( manager_base.ModelSerializer ): + """ + Interface/service object for serializing HDAs into dictionaries. + """ + pass diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/managers/histories.py --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -1,9 +1,32 @@ +""" +Manager and Serializer for histories. + +Histories are containers for datasets or dataset collections +created (or copied) by users over the course of an analysis. +""" + from galaxy import exceptions from galaxy.model import orm +from galaxy.managers import base as manager_base +import galaxy.managers.hdas -class HistoryManager( object ): +import galaxy.web +import galaxy.dataset_collections.util + +import logging +log = logging.getLogger( __name__ ) + + +# ============================================================================= +class HistoryManager( manager_base.ModelManager ): + """ + Interface/service object for interacting with HDAs. + """ + #TODO: all the following would be more useful if passed the user instead of defaulting to trans.user + def __init__( self, *args, **kwargs ): + super( HistoryManager, self ).__init__( *args, **kwargs ) def get( self, trans, unencoded_id, check_ownership=True, check_accessible=True, deleted=None ): """ @@ -44,7 +67,7 @@ def secure( self, trans, history, check_ownership=True, check_accessible=True ): """ - checks if (a) user owns item or (b) item is accessible to user. + Checks if (a) user owns item or (b) item is accessible to user. """ # all items are accessible to an admin if trans.user and trans.user_is_admin(): @@ -56,15 +79,28 @@ return history def is_current( self, trans, history ): + """ + True if the given history is the user's current history. + + Returns False if the session has no current history. + """ + if trans.history is None: + return False return trans.history == history def is_owner( self, trans, history ): + """ + True if the current user is the owner of the given history. + """ # anon users are only allowed to view their current history if not trans.user: return self.is_current( trans, history ) return trans.user == history.user def check_ownership( self, trans, history ): + """ + Raises error if the current user is not the owner of the history. + """ if trans.user and trans.user_is_admin(): return history if not trans.user and not self.is_current( trans, history ): @@ -74,6 +110,9 @@ raise exceptions.ItemOwnershipException( "History is not owned by the current user", type='error' ) def is_accessible( self, trans, history ): + """ + True if the user can access (read) the current history. + """ # admin always have access if trans.user and trans.user_is_admin(): return True @@ -88,6 +127,103 @@ return False def check_accessible( self, trans, history ): + """ + Raises error if the current user can't access the history. + """ if self.is_accessible( trans, history ): return history raise exceptions.ItemAccessibilityException( "History is not accessible to the current user", type='error' ) + + #TODO: bleh... + def _get_history_data( self, trans, history ): + """ + Returns a dictionary containing ``history`` and ``contents``, serialized + history and an array of serialized history contents respectively. + """ + hda_mgr = galaxy.managers.hdas.HDAManager() + collection_dictifier = galaxy.dataset_collections.util.dictify_dataset_collection_instance + + history_dictionary = {} + contents_dictionaries = [] + try: + #for content in history.contents_iter( **contents_kwds ): + for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): + hda_dict = {} + + if isinstance( content, trans.app.model.HistoryDatasetAssociation ): + try: + hda_dict = hda_mgr.get_hda_dict( trans, content ) + except Exception, exc: + # don't fail entire list if hda err's, record and move on + log.exception( 'Error bootstrapping hda: %s', exc ) + hda_dict = hda_mgr.get_hda_dict_with_error( trans, content, str( exc ) ) + + elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): + try: + service = trans.app.dataset_collections_service + dataset_collection_instance = service.get_dataset_collection_instance( + trans=trans, + instance_type='history', + id=trans.security.encode_id( content.id ), + ) + hda_dict = collection_dictifier( dataset_collection_instance, + security=trans.security, parent=dataset_collection_instance.history, view="element" ) + + except Exception, exc: + log.exception( "Error in history API at listing dataset collection: %s", exc ) + #TODO: return some dict with the error + + contents_dictionaries.append( hda_dict ) + + # re-use the hdas above to get the history data... + history_dictionary = self.get_history_dict( trans, history, contents_dictionaries=contents_dictionaries ) + + except Exception, exc: + user_id = str( trans.user.id ) if trans.user else '(anonymous)' + log.exception( 'Error bootstrapping history for user %s: %s', user_id, str( exc ) ) + message = ( 'An error occurred getting the history data from the server. ' + + 'Please contact a Galaxy administrator if the problem persists.' ) + history_dictionary[ 'error' ] = message + + return { + 'history' : history_dictionary, + 'contents' : contents_dictionaries + } + + def get_history_dict( self, trans, history, contents_dictionaries=None ): + """ + Returns history data in the form of a dictionary. + """ + #TODO: to serializer + history_dict = history.to_dict( view='element', value_mapper={ 'id':trans.security.encode_id }) + history_dict[ 'user_id' ] = None + if history.user_id: + history_dict[ 'user_id' ] = trans.security.encode_id( history.user_id ) + + history_dict[ 'nice_size' ] = history.get_disk_size( nice_size=True ) + history_dict[ 'annotation' ] = history.get_item_annotation_str( trans.sa_session, history.user, history ) + if not history_dict[ 'annotation' ]: + history_dict[ 'annotation' ] = '' + + #TODO: item_slug url + if history_dict[ 'importable' ] and history_dict[ 'slug' ]: + username_and_slug = ( '/' ).join(( 'u', history.user.username, 'h', history_dict[ 'slug' ] )) + history_dict[ 'username_and_slug' ] = username_and_slug + +#TODO: re-add + #hda_summaries = hda_dictionaries if hda_dictionaries else self.get_hda_summary_dicts( trans, history ) + ##TODO remove the following in v2 + #( state_counts, state_ids ) = self._get_hda_state_summaries( trans, hda_summaries ) + #history_dict[ 'state_details' ] = state_counts + #history_dict[ 'state_ids' ] = state_ids + #history_dict[ 'state' ] = self._get_history_state_from_hdas( trans, history, state_counts ) + + return history_dict + + +# ============================================================================= +class HistorySerializer( manager_base.ModelSerializer ): + """ + Interface/service object for serializing histories into dictionaries. + """ + pass diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/webapps/galaxy/api/history_contents.py --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -92,7 +92,7 @@ else: types = [ 'dataset', "dataset_collection" ] - contents_kwds = {'types': types} + contents_kwds = { 'types': types } if ids: ids = map( lambda id: trans.security.decode_id( id ), ids.split( ',' ) ) contents_kwds[ 'ids' ] = ids @@ -110,14 +110,14 @@ details = util.listify( details ) for content in history.contents_iter( **contents_kwds ): - if isinstance(content, trans.app.model.HistoryDatasetAssociation): + if isinstance( content, trans.app.model.HistoryDatasetAssociation ): encoded_content_id = trans.security.encode_id( content.id ) detailed = details == 'all' or ( encoded_content_id in details ) if detailed: rval.append( self._detailed_hda_dict( trans, content ) ) else: rval.append( self._summary_hda_dict( trans, history_id, content ) ) - elif isinstance(content, trans.app.model.HistoryDatasetCollectionAssociation): + elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): rval.append( self.__collection_dict( trans, content ) ) return rval @@ -149,7 +149,8 @@ } def __collection_dict( self, trans, dataset_collection_instance, view="collection" ): - return dictify_dataset_collection_instance( dataset_collection_instance, security=trans.security, parent=dataset_collection_instance.history, view=view ) + return dictify_dataset_collection_instance( dataset_collection_instance, + security=trans.security, parent=dataset_collection_instance.history, view=view ) def _detailed_hda_dict( self, trans, hda ): """ @@ -201,8 +202,7 @@ ) return self.__collection_dict( trans, dataset_collection_instance, view="element" ) except Exception, e: - msg = "Error in history API at listing dataset collection: %s" % ( str(e) ) - log.error( msg, exc_info=True ) + log.exception( "Error in history API at listing dataset collection: %s", e ) trans.response.status = 500 return msg diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -4,6 +4,7 @@ import galaxy.util from galaxy import model from galaxy import web +from galaxy import managers from galaxy.datatypes.data import nice_size from galaxy.model.item_attrs import UsesAnnotations, UsesItemRatings from galaxy.model.orm import and_, eagerload_all, func @@ -194,6 +195,12 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesItemRatings, UsesHistoryMixin, UsesHistoryDatasetAssociationMixin, ExportsHistoryMixin, ImportsHistoryMixin ): + def __init__( self, app ): + super( HistoryController, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + histories=managers.histories.HistoryManager() + ) + @web.expose def index( self, trans ): return "" @@ -204,6 +211,7 @@ trans.response.set_content_type( 'text/xml' ) return trans.fill_template( "/history/list_as_xml.mako" ) + # ......................................................................... lists stored_list_grid = HistoryListGrid() shared_list_grid = SharedHistoryListGrid() published_list_grid = HistoryAllPublishedGrid() @@ -437,6 +445,7 @@ # Render the list view return self.shared_list_grid( trans, status=status, message=message, **kwargs ) + # ......................................................................... html @web.expose def display_structured( self, trans, id=None ): """ @@ -512,6 +521,435 @@ return trans.fill_template( "history/display_structured.mako", items=items, history=history ) @web.expose + def view( self, trans, id=None, show_deleted=False, show_hidden=False, use_panels=True ): + """ + View a history. If a history is importable, then it is viewable by any user. + """ + # Get history to view. + if not id: + return trans.show_error_message( "You must specify a history you want to view." ) + + show_deleted = galaxy.util.string_as_bool( show_deleted ) + show_hidden = galaxy.util.string_as_bool( show_hidden ) + use_panels = galaxy.util.string_as_bool( use_panels ) + + history_dictionary = {} + hda_dictionaries = [] + user_is_owner = False + try: + history_to_view = self.get_history( trans, id, check_ownership=False, check_accessible=False ) + if not history_to_view: + return trans.show_error_message( "The specified history does not exist." ) + if history_to_view.user == trans.user: + user_is_owner = True + + if( ( history_to_view.user != trans.user ) + # Admin users can view any history + and ( not trans.user_is_admin() ) + and ( not history_to_view.importable ) + and ( trans.user not in history_to_view.users_shared_with_dot_users ) ): + return trans.show_error_message( "Either you are not allowed to view this history" + + " or the owner of this history has not made it accessible." ) + + # include all datasets: hidden, deleted, and purged + history_data = self.mgrs.histories._get_history_data( trans, history_to_view ) + history_dictionary = history_data[ 'history' ] + hda_dictionaries = history_data[ 'contents' ] + + except Exception, exc: + user_id = str( trans.user.id ) if trans.user else '(anonymous)' + log.exception( 'Error bootstrapping history for user %s: %s', user_id, str( exc ) ) + history_dictionary[ 'error' ] = ( 'An error occurred getting the history data from the server. ' + + 'Please contact a Galaxy administrator if the problem persists.' ) + + return trans.fill_template_mako( "history/view.mako", + history=history_dictionary, hdas=hda_dictionaries, user_is_owner=user_is_owner, + show_deleted=show_deleted, show_hidden=show_hidden, use_panels=use_panels ) + + @web.expose + def display_by_username_and_slug( self, trans, username, slug ): + """ + Display history based on a username and slug. + """ + # Get history. + session = trans.sa_session + user = session.query( model.User ).filter_by( username=username ).first() + history = trans.sa_session.query( model.History ).filter_by( user=user, slug=slug, deleted=False ).first() + if history is None: + raise web.httpexceptions.HTTPNotFound() + # Security check raises error if user cannot access history. + self.security_check( trans, history, False, True) + + # Get rating data. + user_item_rating = 0 + if trans.get_user(): + user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), history ) + if user_item_rating: + user_item_rating = user_item_rating.rating + else: + user_item_rating = 0 + ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, history ) + + # create ownership flag for template, dictify models + user_is_owner = trans.user == history.user + history_data = self.mgrs.histories._get_history_data( trans, history ) + history_dict = history_data[ 'history' ] + hda_dicts = history_data[ 'contents' ] + + history_dict[ 'annotation' ] = self.get_item_annotation_str( trans.sa_session, history.user, history ) + # note: adding original annotation since this is published - get_dict returns user-based annos + #for hda_dict in hda_dicts: + # hda_dict[ 'annotation' ] = hda.annotation + # dataset.annotation = self.get_item_annotation_str( trans.sa_session, history.user, dataset ) + + return trans.stream_template_mako( "history/display.mako", item=history, item_data=[], + user_is_owner=user_is_owner, history_dict=history_dict, hda_dicts=hda_dicts, + user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings ) + + # ......................................................................... sharing & publishing + @web.expose + @web.require_login( "share Galaxy histories" ) + def sharing( self, trans, id=None, histories=[], **kwargs ): + """ Handle history sharing. """ + + # Get session and histories. + session = trans.sa_session + # Id values take precedence over histories passed in; last resort is current history. + if id: + ids = galaxy.util.listify( id ) + if ids: + histories = [ self.get_history( trans, history_id ) for history_id in ids ] + elif not histories: + histories = [ trans.history ] + + # Do operation on histories. + for history in histories: + if 'make_accessible_via_link' in kwargs: + self._make_item_accessible( trans.sa_session, history ) + elif 'make_accessible_and_publish' in kwargs: + self._make_item_accessible( trans.sa_session, history ) + history.published = True + elif 'publish' in kwargs: + if history.importable: + history.published = True + else: + # TODO: report error here. + pass + elif 'disable_link_access' in kwargs: + history.importable = False + elif 'unpublish' in kwargs: + history.published = False + elif 'disable_link_access_and_unpublish' in kwargs: + history.importable = history.published = False + elif 'unshare_user' in kwargs: + user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( kwargs[ 'unshare_user' ] ) ) + # Look for and delete sharing relation for history-user. + deleted_sharing_relation = False + husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all() + if husas: + deleted_sharing_relation = True + for husa in husas: + trans.sa_session.delete( husa ) + if not deleted_sharing_relation: + message = "History '%s' does not seem to be shared with user '%s'" % ( history.name, user.email ) + return trans.fill_template( '/sharing_base.mako', item=history, + message=message, status='error' ) + + + # Legacy issue: histories made accessible before recent updates may not have a slug. Create slug for any histories that need them. + for history in histories: + if history.importable and not history.slug: + self._make_item_accessible( trans.sa_session, history ) + + session.flush() + + return trans.fill_template( "/sharing_base.mako", item=history ) + + @web.expose + @web.require_login( "share histories with other users" ) + def share( self, trans, id=None, email="", **kwd ): + # If a history contains both datasets that can be shared and others that cannot be shared with the desired user, + # then the entire history is shared, and the protected datasets will be visible, but inaccessible ( greyed out ) + # in the copyd history + params = Params( kwd ) + user = trans.get_user() + # TODO: we have too many error messages floating around in here - we need + # to incorporate the messaging system used by the libraries that will display + # a message on any page. + err_msg = galaxy.util.restore_text( params.get( 'err_msg', '' ) ) + if not email: + if not id: + # Default to the current history + id = trans.security.encode_id( trans.history.id ) + id = galaxy.util.listify( id ) + send_to_err = err_msg + histories = [] + for history_id in id: + histories.append( self.get_history( trans, history_id ) ) + return trans.fill_template( "/history/share.mako", + histories=histories, + email=email, + send_to_err=send_to_err ) + histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email ) + if not send_to_users: + if not send_to_err: + send_to_err += "%s is not a valid Galaxy user. %s" % ( email, err_msg ) + return trans.fill_template( "/history/share.mako", + histories=histories, + email=email, + send_to_err=send_to_err ) + if params.get( 'share_button', False ): + # The user has not yet made a choice about how to share, so dictionaries will be built for display + can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \ + self._populate_restricted( trans, user, histories, send_to_users, None, send_to_err, unique=True ) + send_to_err += err_msg + if cannot_change and not no_change_needed and not can_change: + 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." + return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) + if can_change or cannot_change: + return trans.fill_template( "/history/share.mako", + histories=histories, + email=email, + send_to_err=send_to_err, + can_change=can_change, + cannot_change=cannot_change, + no_change_needed=unique_no_change_needed ) + if no_change_needed: + return self._share_histories( trans, user, send_to_err, histories=no_change_needed ) + elif not send_to_err: + # User seems to be sharing an empty history + send_to_err = "You cannot share an empty history. " + return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) + + @web.expose + @web.require_login( "share restricted histories with other users" ) + def share_restricted( self, trans, id=None, email="", **kwd ): + if 'action' in kwd: + action = kwd[ 'action' ] + else: + err_msg = "Select an action. " + return trans.response.send_redirect( url_for( controller='history', + action='share', + id=id, + email=email, + err_msg=err_msg, + share_button=True ) ) + user = trans.get_user() + user_roles = user.all_roles() + histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email ) + send_to_err = '' + # The user has made a choice, so dictionaries will be built for sharing + can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \ + self._populate_restricted( trans, user, histories, send_to_users, action, send_to_err ) + # Now that we've populated the can_change, cannot_change, and no_change_needed dictionaries, + # we'll populate the histories_for_sharing dictionary from each of them. + histories_for_sharing = {} + if no_change_needed: + # Don't need to change anything in cannot_change, so populate as is + histories_for_sharing, send_to_err = \ + self._populate( trans, histories_for_sharing, no_change_needed, send_to_err ) + if cannot_change: + # Can't change anything in cannot_change, so populate as is + histories_for_sharing, send_to_err = \ + self._populate( trans, histories_for_sharing, cannot_change, send_to_err ) + # The action here is either 'public' or 'private', so we'll continue to populate the + # histories_for_sharing dictionary from the can_change dictionary. + for send_to_user, history_dict in can_change.items(): + for history in history_dict: + # Make sure the current history has not already been shared with the current send_to_user + if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ + .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id, + trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \ + .count() > 0: + send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email ) + else: + # Only deal with datasets that have not been purged + for hda in history.activatable_datasets: + # If the current dataset is not public, we may need to perform an action on it to + # make it accessible by the other user. + if not trans.app.security_agent.can_access_dataset( send_to_user.all_roles(), hda.dataset ): + # The user with which we are sharing the history does not have access permission on the current dataset + if trans.app.security_agent.can_manage_dataset( user_roles, hda.dataset ) and not hda.dataset.library_associations: + # The current user has authority to change permissions on the current dataset because + # they have permission to manage permissions on the dataset and the dataset is not associated + # with a library. + if action == "private": + trans.app.security_agent.privately_share_dataset( hda.dataset, users=[ user, send_to_user ] ) + elif action == "public": + trans.app.security_agent.make_dataset_public( hda.dataset ) + # Populate histories_for_sharing with the history after performing any requested actions on + # its datasets to make them accessible by the other user. + if send_to_user not in histories_for_sharing: + histories_for_sharing[ send_to_user ] = [ history ] + elif history not in histories_for_sharing[ send_to_user ]: + histories_for_sharing[ send_to_user ].append( history ) + return self._share_histories( trans, user, send_to_err, histories=histories_for_sharing ) + + def _get_histories_and_users( self, trans, user, id, email ): + if not id: + # Default to the current history + id = trans.security.encode_id( trans.history.id ) + id = galaxy.util.listify( id ) + send_to_err = "" + histories = [] + for history_id in id: + histories.append( self.get_history( trans, history_id ) ) + send_to_users = [] + for email_address in galaxy.util.listify( email ): + email_address = email_address.strip() + if email_address: + if email_address == user.email: + send_to_err += "You cannot send histories to yourself. " + else: + send_to_user = trans.sa_session.query( trans.app.model.User ) \ + .filter( and_( trans.app.model.User.table.c.email==email_address, + trans.app.model.User.table.c.deleted==False ) ) \ + .first() + if send_to_user: + send_to_users.append( send_to_user ) + else: + send_to_err += "%s is not a valid Galaxy user. " % email_address + return histories, send_to_users, send_to_err + + def _populate( self, trans, histories_for_sharing, other, send_to_err ): + # This method will populate the histories_for_sharing dictionary with the users and + # histories in other, eliminating histories that have already been shared with the + # associated user. No security checking on datasets is performed. + # If not empty, the histories_for_sharing dictionary looks like: + # { userA: [ historyX, historyY ], userB: [ historyY ] } + # other looks like: + # { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } + for send_to_user, history_dict in other.items(): + for history in history_dict: + # Make sure the current history has not already been shared with the current send_to_user + if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ + .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id, + trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \ + .count() > 0: + send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email ) + else: + # Build the dict that will be used for sharing + if send_to_user not in histories_for_sharing: + histories_for_sharing[ send_to_user ] = [ history ] + elif history not in histories_for_sharing[ send_to_user ]: + histories_for_sharing[ send_to_user ].append( history ) + return histories_for_sharing, send_to_err + + def _populate_restricted( self, trans, user, histories, send_to_users, action, send_to_err, unique=False ): + # The user may be attempting to share histories whose datasets cannot all be accessed by other users. + # If this is the case, the user sharing the histories can: + # 1) action=='public': choose to make the datasets public if he is permitted to do so + # 2) action=='private': automatically create a new "sharing role" allowing protected + # datasets to be accessed only by the desired users + # This method will populate the can_change, cannot_change and no_change_needed dictionaries, which + # are used for either displaying to the user, letting them make 1 of the choices above, or sharing + # after the user has made a choice. They will be used for display if 'unique' is True, and will look + # like: {historyX : [hda, hda], historyY : [hda] } + # For sharing, they will look like: + # { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } + can_change = {} + cannot_change = {} + no_change_needed = {} + unique_no_change_needed = {} + user_roles = user.all_roles() + for history in histories: + for send_to_user in send_to_users: + # Make sure the current history has not already been shared with the current send_to_user + if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ + .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id, + trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \ + .count() > 0: + send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email ) + else: + # Only deal with datasets that have not been purged + for hda in history.activatable_datasets: + if trans.app.security_agent.can_access_dataset( send_to_user.all_roles(), hda.dataset ): + # The no_change_needed dictionary is a special case. If both of can_change + # and cannot_change are empty, no_change_needed will used for sharing. Otherwise + # unique_no_change_needed will be used for displaying, so we need to populate both. + # Build the dictionaries for display, containing unique histories only + if history not in unique_no_change_needed: + unique_no_change_needed[ history ] = [ hda ] + else: + unique_no_change_needed[ history ].append( hda ) + # Build the dictionaries for sharing + if send_to_user not in no_change_needed: + no_change_needed[ send_to_user ] = {} + if history not in no_change_needed[ send_to_user ]: + no_change_needed[ send_to_user ][ history ] = [ hda ] + else: + no_change_needed[ send_to_user ][ history ].append( hda ) + else: + # The user with which we are sharing the history does not have access permission on the current dataset + if trans.app.security_agent.can_manage_dataset( user_roles, hda.dataset ): + # The current user has authority to change permissions on the current dataset because + # they have permission to manage permissions on the dataset. + # NOTE: ( gvk )There may be problems if the dataset also has an ldda, but I don't think so + # because the user with which we are sharing will not have the "manage permission" permission + # on the dataset in their history. Keep an eye on this though... + if unique: + # Build the dictionaries for display, containing unique histories only + if history not in can_change: + can_change[ history ] = [ hda ] + else: + can_change[ history ].append( hda ) + else: + # Build the dictionaries for sharing + if send_to_user not in can_change: + can_change[ send_to_user ] = {} + if history not in can_change[ send_to_user ]: + can_change[ send_to_user ][ history ] = [ hda ] + else: + can_change[ send_to_user ][ history ].append( hda ) + else: + if action in [ "private", "public" ]: + # The user has made a choice, so 'unique' doesn't apply. Don't change stuff + # that the user doesn't have permission to change + continue + if unique: + # Build the dictionaries for display, containing unique histories only + if history not in cannot_change: + cannot_change[ history ] = [ hda ] + else: + cannot_change[ history ].append( hda ) + else: + # Build the dictionaries for sharing + if send_to_user not in cannot_change: + cannot_change[ send_to_user ] = {} + if history not in cannot_change[ send_to_user ]: + cannot_change[ send_to_user ][ history ] = [ hda ] + else: + cannot_change[ send_to_user ][ history ].append( hda ) + return can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err + + def _share_histories( self, trans, user, send_to_err, histories=None ): + # histories looks like: { userA: [ historyX, historyY ], userB: [ historyY ] } + histories = histories or {} + msg = "" + sent_to_emails = [] + for send_to_user in histories.keys(): + sent_to_emails.append( send_to_user.email ) + emails = ",".join( e for e in sent_to_emails ) + if not histories: + send_to_err += "No users have been specified or no histories can be sent without changing permissions or associating a sharing role. " + else: + for send_to_user, send_to_user_histories in histories.items(): + shared_histories = [] + for history in send_to_user_histories: + share = trans.app.model.HistoryUserShareAssociation() + share.history = history + share.user = send_to_user + trans.sa_session.add( share ) + self.create_item_slug( trans.sa_session, history ) + trans.sa_session.flush() + if history not in shared_histories: + shared_histories.append( history ) + if send_to_err: + msg += send_to_err + return self.sharing( trans, histories=shared_histories, msg=msg ) + + # ......................................................................... actions/orig. async + @web.expose def delete_hidden_datasets( self, trans ): """ This method deletes all hidden datasets in the current history. @@ -839,451 +1277,6 @@ #TODO: used in history/view, display, embed @web.expose - def view( self, trans, id=None, show_deleted=False, show_hidden=False, use_panels=True ): - """ - View a history. If a history is importable, then it is viewable by any user. - """ - # Get history to view. - if not id: - return trans.show_error_message( "You must specify a history you want to view." ) - - show_deleted = galaxy.util.string_as_bool( show_deleted ) - show_hidden = galaxy.util.string_as_bool( show_hidden ) - use_panels = galaxy.util.string_as_bool( use_panels ) - - history_dictionary = {} - hda_dictionaries = [] - user_is_owner = False - try: - history_to_view = self.get_history( trans, id, check_ownership=False, check_accessible=False ) - if not history_to_view: - return trans.show_error_message( "The specified history does not exist." ) - if history_to_view.user == trans.user: - user_is_owner = True - - if( ( history_to_view.user != trans.user ) - # Admin users can view any history - and ( not trans.user_is_admin() ) - and ( not history_to_view.importable ) - and ( trans.user not in history_to_view.users_shared_with_dot_users ) ): - return trans.show_error_message( "Either you are not allowed to view this history" - + " or the owner of this history has not made it accessible." ) - - # include all datasets: hidden, deleted, and purged - hdas = self.get_history_datasets( trans, history_to_view, - show_deleted=True, show_hidden=True, show_purged=True ) - for hda in hdas: - hda_dict = {} - try: - hda_dict = self.get_hda_dict( trans, hda ) - - except Exception, exc: - # don't fail entire list if hda err's, record and move on - log.error( 'Error bootstrapping hda %d: %s', hda.id, str( exc ), exc_info=True ) - hda_dict = self.get_hda_dict_with_error( trans, hda, str( exc ) ) - - hda_dictionaries.append( hda_dict ) - - # re-use the hdas above to get the history data... - history_dictionary = self.get_history_dict( trans, history_to_view, hda_dictionaries=hda_dictionaries ) - - except Exception, exc: - user_id = str( trans.user.id ) if trans.user else '(anonymous)' - log.exception( 'Error bootstrapping history for user %s: %s', user_id, str( exc ) ) - history_dictionary[ 'error' ] = ( 'An error occurred getting the history data from the server. ' - + 'Please contact a Galaxy administrator if the problem persists.' ) - - return trans.fill_template_mako( "history/view.mako", - history=history_dictionary, hdas=hda_dictionaries, user_is_owner=user_is_owner, - show_deleted=show_deleted, show_hidden=show_hidden, use_panels=use_panels ) - - @web.expose - def display_by_username_and_slug( self, trans, username, slug ): - """ - Display history based on a username and slug. - """ - # Get history. - session = trans.sa_session - user = session.query( model.User ).filter_by( username=username ).first() - history = trans.sa_session.query( model.History ).filter_by( user=user, slug=slug, deleted=False ).first() - if history is None: - raise web.httpexceptions.HTTPNotFound() - # Security check raises error if user cannot access history. - self.security_check( trans, history, False, True) - - # Get datasets and annotations - datasets = self.get_history_datasets( trans, history ) - history.annotation = self.get_item_annotation_str( trans.sa_session, history.user, history ) - for dataset in datasets: - dataset.annotation = self.get_item_annotation_str( trans.sa_session, history.user, dataset ) - - # Get rating data. - user_item_rating = 0 - if trans.get_user(): - user_item_rating = self.get_user_item_rating( trans.sa_session, trans.get_user(), history ) - if user_item_rating: - user_item_rating = user_item_rating.rating - else: - user_item_rating = 0 - ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans.sa_session, history ) - - # create ownership flag for template, dictify models - # note: adding original annotation since this is published - get_dict returns user-based annos - user_is_owner = trans.user == history.user - hda_dicts = [] - for hda in datasets: - hda_dict = self.get_hda_dict( trans, hda ) - hda_dict[ 'annotation' ] = hda.annotation - hda_dicts.append( hda_dict ) - history_dict = self.get_history_dict( trans, history, hda_dictionaries=hda_dicts ) - history_dict[ 'annotation' ] = history.annotation - - return trans.stream_template_mako( "history/display.mako", item=history, item_data=datasets, - user_is_owner=user_is_owner, history_dict=history_dict, hda_dicts=hda_dicts, - user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings ) - - @web.expose - @web.require_login( "share Galaxy histories" ) - def sharing( self, trans, id=None, histories=[], **kwargs ): - """ Handle history sharing. """ - - # Get session and histories. - session = trans.sa_session - # Id values take precedence over histories passed in; last resort is current history. - if id: - ids = galaxy.util.listify( id ) - if ids: - histories = [ self.get_history( trans, history_id ) for history_id in ids ] - elif not histories: - histories = [ trans.history ] - - # Do operation on histories. - for history in histories: - if 'make_accessible_via_link' in kwargs: - self._make_item_accessible( trans.sa_session, history ) - elif 'make_accessible_and_publish' in kwargs: - self._make_item_accessible( trans.sa_session, history ) - history.published = True - elif 'publish' in kwargs: - if history.importable: - history.published = True - else: - # TODO: report error here. - pass - elif 'disable_link_access' in kwargs: - history.importable = False - elif 'unpublish' in kwargs: - history.published = False - elif 'disable_link_access_and_unpublish' in kwargs: - history.importable = history.published = False - elif 'unshare_user' in kwargs: - user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( kwargs[ 'unshare_user' ] ) ) - # Look for and delete sharing relation for history-user. - deleted_sharing_relation = False - husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all() - if husas: - deleted_sharing_relation = True - for husa in husas: - trans.sa_session.delete( husa ) - if not deleted_sharing_relation: - message = "History '%s' does not seem to be shared with user '%s'" % ( history.name, user.email ) - return trans.fill_template( '/sharing_base.mako', item=history, - message=message, status='error' ) - - - # Legacy issue: histories made accessible before recent updates may not have a slug. Create slug for any histories that need them. - for history in histories: - if history.importable and not history.slug: - self._make_item_accessible( trans.sa_session, history ) - - session.flush() - - return trans.fill_template( "/sharing_base.mako", item=history ) - - @web.expose - @web.require_login( "share histories with other users" ) - def share( self, trans, id=None, email="", **kwd ): - # If a history contains both datasets that can be shared and others that cannot be shared with the desired user, - # then the entire history is shared, and the protected datasets will be visible, but inaccessible ( greyed out ) - # in the copyd history - params = Params( kwd ) - user = trans.get_user() - # TODO: we have too many error messages floating around in here - we need - # to incorporate the messaging system used by the libraries that will display - # a message on any page. - err_msg = galaxy.util.restore_text( params.get( 'err_msg', '' ) ) - if not email: - if not id: - # Default to the current history - id = trans.security.encode_id( trans.history.id ) - id = galaxy.util.listify( id ) - send_to_err = err_msg - histories = [] - for history_id in id: - histories.append( self.get_history( trans, history_id ) ) - return trans.fill_template( "/history/share.mako", - histories=histories, - email=email, - send_to_err=send_to_err ) - histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email ) - if not send_to_users: - if not send_to_err: - send_to_err += "%s is not a valid Galaxy user. %s" % ( email, err_msg ) - return trans.fill_template( "/history/share.mako", - histories=histories, - email=email, - send_to_err=send_to_err ) - if params.get( 'share_button', False ): - # The user has not yet made a choice about how to share, so dictionaries will be built for display - can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \ - self._populate_restricted( trans, user, histories, send_to_users, None, send_to_err, unique=True ) - send_to_err += err_msg - if cannot_change and not no_change_needed and not can_change: - 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." - return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) - if can_change or cannot_change: - return trans.fill_template( "/history/share.mako", - histories=histories, - email=email, - send_to_err=send_to_err, - can_change=can_change, - cannot_change=cannot_change, - no_change_needed=unique_no_change_needed ) - if no_change_needed: - return self._share_histories( trans, user, send_to_err, histories=no_change_needed ) - elif not send_to_err: - # User seems to be sharing an empty history - send_to_err = "You cannot share an empty history. " - return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) - - @web.expose - @web.require_login( "share restricted histories with other users" ) - def share_restricted( self, trans, id=None, email="", **kwd ): - if 'action' in kwd: - action = kwd[ 'action' ] - else: - err_msg = "Select an action. " - return trans.response.send_redirect( url_for( controller='history', - action='share', - id=id, - email=email, - err_msg=err_msg, - share_button=True ) ) - user = trans.get_user() - user_roles = user.all_roles() - histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email ) - send_to_err = '' - # The user has made a choice, so dictionaries will be built for sharing - can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err = \ - self._populate_restricted( trans, user, histories, send_to_users, action, send_to_err ) - # Now that we've populated the can_change, cannot_change, and no_change_needed dictionaries, - # we'll populate the histories_for_sharing dictionary from each of them. - histories_for_sharing = {} - if no_change_needed: - # Don't need to change anything in cannot_change, so populate as is - histories_for_sharing, send_to_err = \ - self._populate( trans, histories_for_sharing, no_change_needed, send_to_err ) - if cannot_change: - # Can't change anything in cannot_change, so populate as is - histories_for_sharing, send_to_err = \ - self._populate( trans, histories_for_sharing, cannot_change, send_to_err ) - # The action here is either 'public' or 'private', so we'll continue to populate the - # histories_for_sharing dictionary from the can_change dictionary. - for send_to_user, history_dict in can_change.items(): - for history in history_dict: - # Make sure the current history has not already been shared with the current send_to_user - if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ - .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id, - trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \ - .count() > 0: - send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email ) - else: - # Only deal with datasets that have not been purged - for hda in history.activatable_datasets: - # If the current dataset is not public, we may need to perform an action on it to - # make it accessible by the other user. - if not trans.app.security_agent.can_access_dataset( send_to_user.all_roles(), hda.dataset ): - # The user with which we are sharing the history does not have access permission on the current dataset - if trans.app.security_agent.can_manage_dataset( user_roles, hda.dataset ) and not hda.dataset.library_associations: - # The current user has authority to change permissions on the current dataset because - # they have permission to manage permissions on the dataset and the dataset is not associated - # with a library. - if action == "private": - trans.app.security_agent.privately_share_dataset( hda.dataset, users=[ user, send_to_user ] ) - elif action == "public": - trans.app.security_agent.make_dataset_public( hda.dataset ) - # Populate histories_for_sharing with the history after performing any requested actions on - # its datasets to make them accessible by the other user. - if send_to_user not in histories_for_sharing: - histories_for_sharing[ send_to_user ] = [ history ] - elif history not in histories_for_sharing[ send_to_user ]: - histories_for_sharing[ send_to_user ].append( history ) - return self._share_histories( trans, user, send_to_err, histories=histories_for_sharing ) - - def _get_histories_and_users( self, trans, user, id, email ): - if not id: - # Default to the current history - id = trans.security.encode_id( trans.history.id ) - id = galaxy.util.listify( id ) - send_to_err = "" - histories = [] - for history_id in id: - histories.append( self.get_history( trans, history_id ) ) - send_to_users = [] - for email_address in galaxy.util.listify( email ): - email_address = email_address.strip() - if email_address: - if email_address == user.email: - send_to_err += "You cannot send histories to yourself. " - else: - send_to_user = trans.sa_session.query( trans.app.model.User ) \ - .filter( and_( trans.app.model.User.table.c.email==email_address, - trans.app.model.User.table.c.deleted==False ) ) \ - .first() - if send_to_user: - send_to_users.append( send_to_user ) - else: - send_to_err += "%s is not a valid Galaxy user. " % email_address - return histories, send_to_users, send_to_err - - def _populate( self, trans, histories_for_sharing, other, send_to_err ): - # This method will populate the histories_for_sharing dictionary with the users and - # histories in other, eliminating histories that have already been shared with the - # associated user. No security checking on datasets is performed. - # If not empty, the histories_for_sharing dictionary looks like: - # { userA: [ historyX, historyY ], userB: [ historyY ] } - # other looks like: - # { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } - for send_to_user, history_dict in other.items(): - for history in history_dict: - # Make sure the current history has not already been shared with the current send_to_user - if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ - .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id, - trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \ - .count() > 0: - send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email ) - else: - # Build the dict that will be used for sharing - if send_to_user not in histories_for_sharing: - histories_for_sharing[ send_to_user ] = [ history ] - elif history not in histories_for_sharing[ send_to_user ]: - histories_for_sharing[ send_to_user ].append( history ) - return histories_for_sharing, send_to_err - - def _populate_restricted( self, trans, user, histories, send_to_users, action, send_to_err, unique=False ): - # The user may be attempting to share histories whose datasets cannot all be accessed by other users. - # If this is the case, the user sharing the histories can: - # 1) action=='public': choose to make the datasets public if he is permitted to do so - # 2) action=='private': automatically create a new "sharing role" allowing protected - # datasets to be accessed only by the desired users - # This method will populate the can_change, cannot_change and no_change_needed dictionaries, which - # are used for either displaying to the user, letting them make 1 of the choices above, or sharing - # after the user has made a choice. They will be used for display if 'unique' is True, and will look - # like: {historyX : [hda, hda], historyY : [hda] } - # For sharing, they will look like: - # { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } - can_change = {} - cannot_change = {} - no_change_needed = {} - unique_no_change_needed = {} - user_roles = user.all_roles() - for history in histories: - for send_to_user in send_to_users: - # Make sure the current history has not already been shared with the current send_to_user - if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ - .filter( and_( trans.app.model.HistoryUserShareAssociation.table.c.user_id == send_to_user.id, - trans.app.model.HistoryUserShareAssociation.table.c.history_id == history.id ) ) \ - .count() > 0: - send_to_err += "History (%s) already shared with user (%s)" % ( history.name, send_to_user.email ) - else: - # Only deal with datasets that have not been purged - for hda in history.activatable_datasets: - if trans.app.security_agent.can_access_dataset( send_to_user.all_roles(), hda.dataset ): - # The no_change_needed dictionary is a special case. If both of can_change - # and cannot_change are empty, no_change_needed will used for sharing. Otherwise - # unique_no_change_needed will be used for displaying, so we need to populate both. - # Build the dictionaries for display, containing unique histories only - if history not in unique_no_change_needed: - unique_no_change_needed[ history ] = [ hda ] - else: - unique_no_change_needed[ history ].append( hda ) - # Build the dictionaries for sharing - if send_to_user not in no_change_needed: - no_change_needed[ send_to_user ] = {} - if history not in no_change_needed[ send_to_user ]: - no_change_needed[ send_to_user ][ history ] = [ hda ] - else: - no_change_needed[ send_to_user ][ history ].append( hda ) - else: - # The user with which we are sharing the history does not have access permission on the current dataset - if trans.app.security_agent.can_manage_dataset( user_roles, hda.dataset ): - # The current user has authority to change permissions on the current dataset because - # they have permission to manage permissions on the dataset. - # NOTE: ( gvk )There may be problems if the dataset also has an ldda, but I don't think so - # because the user with which we are sharing will not have the "manage permission" permission - # on the dataset in their history. Keep an eye on this though... - if unique: - # Build the dictionaries for display, containing unique histories only - if history not in can_change: - can_change[ history ] = [ hda ] - else: - can_change[ history ].append( hda ) - else: - # Build the dictionaries for sharing - if send_to_user not in can_change: - can_change[ send_to_user ] = {} - if history not in can_change[ send_to_user ]: - can_change[ send_to_user ][ history ] = [ hda ] - else: - can_change[ send_to_user ][ history ].append( hda ) - else: - if action in [ "private", "public" ]: - # The user has made a choice, so 'unique' doesn't apply. Don't change stuff - # that the user doesn't have permission to change - continue - if unique: - # Build the dictionaries for display, containing unique histories only - if history not in cannot_change: - cannot_change[ history ] = [ hda ] - else: - cannot_change[ history ].append( hda ) - else: - # Build the dictionaries for sharing - if send_to_user not in cannot_change: - cannot_change[ send_to_user ] = {} - if history not in cannot_change[ send_to_user ]: - cannot_change[ send_to_user ][ history ] = [ hda ] - else: - cannot_change[ send_to_user ][ history ].append( hda ) - return can_change, cannot_change, no_change_needed, unique_no_change_needed, send_to_err - - def _share_histories( self, trans, user, send_to_err, histories=None ): - # histories looks like: { userA: [ historyX, historyY ], userB: [ historyY ] } - histories = histories or {} - msg = "" - sent_to_emails = [] - for send_to_user in histories.keys(): - sent_to_emails.append( send_to_user.email ) - emails = ",".join( e for e in sent_to_emails ) - if not histories: - send_to_err += "No users have been specified or no histories can be sent without changing permissions or associating a sharing role. " - else: - for send_to_user, send_to_user_histories in histories.items(): - shared_histories = [] - for history in send_to_user_histories: - share = trans.app.model.HistoryUserShareAssociation() - share.history = history - share.user = send_to_user - trans.sa_session.add( share ) - self.create_item_slug( trans.sa_session, history ) - trans.sa_session.flush() - if history not in shared_histories: - shared_histories.append( history ) - if send_to_err: - msg += send_to_err - return self.sharing( trans, histories=shared_histories, msg=msg ) - - @web.expose @web.require_login( "rename histories" ) def rename( self, trans, id=None, name=None, **kwd ): user = trans.get_user() @@ -1361,7 +1354,8 @@ name += " (active items only)" new_history = history.copy( name=name, target_user=user ) if len( histories ) == 1: - msg = 'New history "<a href="%s" target="_top">%s</a>" has been created.' % ( url_for( controller="history", action="switch_to_history", hist_id=trans.security.encode_id( new_history.id ) ) , new_history.name ) + switch_url = url_for( controller="history", action="switch_to_history", hist_id=trans.security.encode_id( new_history.id ) ) + msg = 'New history "<a href="%s" target="_top">%s</a>" has been created.' % ( switch_url, new_history.name ) else: msg = 'Copied and created %d new histories.' % len( histories ) return trans.show_ok_message( msg ) diff -r 1861cbcc610da6e519c7760e1c829ec3b66887f4 -r 65ec063664eb0ce100d76d985fba93f200ac5178 lib/galaxy/webapps/galaxy/controllers/root.py --- a/lib/galaxy/webapps/galaxy/controllers/root.py +++ b/lib/galaxy/webapps/galaxy/controllers/root.py @@ -7,20 +7,28 @@ from paste.httpexceptions import HTTPNotFound, HTTPBadGateway +from galaxy.web.base.controller import BaseUIController, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin +from galaxy import managers + from galaxy import web from galaxy.web import url_for from galaxy.model.item_attrs import UsesAnnotations +from galaxy import util from galaxy.util import listify, Params, string_as_bool, string_as_bool_or_none -from galaxy.web.base.controller import BaseUIController, UsesHistoryDatasetAssociationMixin, UsesHistoryMixin from galaxy.util.json import to_json_string -from galaxy.util.debugging import SimpleProfiler - import logging log = logging.getLogger( __name__ ) class RootController( BaseUIController, UsesHistoryMixin, UsesHistoryDatasetAssociationMixin, UsesAnnotations ): - """Controller class that maps to the url root of Galaxy (i.e. '/').""" + """ + Controller class that maps to the url root of Galaxy (i.e. '/'). + """ + def __init__( self, app ): + super( RootController, self ).__init__( app ) + self.mgrs = util.bunch.Bunch( + histories=managers.histories.HistoryManager() + ) @web.expose def default(self, trans, target1=None, target2=None, **kwd): @@ -100,42 +108,6 @@ show_deleted=string_as_bool( show_deleted ), show_hidden=string_as_bool( show_hidden ) ) - def _get_current_history_data( self, trans ): - history_dictionary = {} - hda_dictionaries = [] - - try: - history = trans.get_history( create=True ) - hdas = self.get_history_datasets( trans, history, - show_deleted=True, show_hidden=True, show_purged=True ) - - for hda in hdas: - hda_dict = {} - try: - hda_dict = self.get_hda_dict( trans, hda ) - - except Exception, exc: - # don't fail entire list if hda err's, record and move on - log.error( 'Error bootstrapping hda %d: %s', hda.id, str( exc ), exc_info=True ) - hda_dict = self.get_hda_dict_with_error( trans, hda, str( exc ) ) - - hda_dictionaries.append( hda_dict ) - - # re-use the hdas above to get the history data... - history_dictionary = self.get_history_dict( trans, history, hda_dictionaries=hda_dictionaries ) - - except Exception, exc: - user_id = str( trans.user.id ) if trans.user else '(anonymous)' - log.exception( 'Error bootstrapping history for user %s: %s', user_id, str( exc ) ) - message = ( 'An error occurred getting the history data from the server. ' - + 'Please contact a Galaxy administrator if the problem persists.' ) - history_dictionary[ 'error' ] = message - - return { - 'history' : history_dictionary, - 'hdas' : hda_dictionaries - } - @web.expose def history( self, trans, as_xml=False, show_deleted=None, show_hidden=None, **kwd ): """ @@ -154,11 +126,11 @@ show_hidden = string_as_bool_or_none( show_hidden ) history_dictionary = {} - hda_dictionaries = [] + hda_dictionaries = [] try: - history_data = self._get_current_history_data( trans ) + history_data = self.mgrs.histories._get_history_data( trans, trans.get_history( create=True ) ) history_dictionary = history_data[ 'history' ] - hda_dictionaries = history_data[ 'hdas' ] + hda_dictionaries = history_data[ 'contents' ] except Exception, exc: user_id = str( trans.user.id ) if trans.user else '(anonymous)' @@ -170,6 +142,7 @@ history = history_dictionary, hdas = hda_dictionaries, show_deleted=show_deleted, show_hidden=show_hidden ) + ## ---- Dataset display / editing ---------------------------------------- @web.expose def display( self, trans, id=None, hid=None, tofile=None, toext=".txt", encoded_id=None, **kwd ): Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.