[hg] galaxy 2460: Many history-related feature enhancements, som...
details: http://www.bx.psu.edu/hg/galaxy/rev/533ae45c4440 changeset: 2460:533ae45c4440 user: Greg Von Kuster <greg@bx.psu.edu> date: Mon Jun 29 16:30:16 2009 -0400 description: Many history-related feature enhancements, some fixes and enhanced functional tests: - history sharing is now modeled after workflow sharing - fixes ticket # 63. - all history features pass encoded history ids between requests - new, empty histories are no longer created when a user logs in, instead their last accessed history is displayed - some other things I'm forgetting, I'm sure This commit also includes a fix for remote user authentication where private roles and default permissions were not created for users ( Ross's and Assaf's issues ). Many functional test enhancements. 30 file(s) affected in this change: lib/galaxy/model/__init__.py lib/galaxy/model/mapping.py lib/galaxy/model/migrate/versions/0007_sharing_histories.py lib/galaxy/security/__init__.py lib/galaxy/tools/parameters/basic.py lib/galaxy/web/controllers/history.py lib/galaxy/web/controllers/root.py lib/galaxy/web/controllers/user.py lib/galaxy/web/controllers/workflow.py lib/galaxy/web/framework/__init__.py lib/galaxy/web/framework/helpers/grids.py lib/galaxy/web/security/__init__.py templates/grid.mako templates/history/grid.mako templates/history/list_as_xml.mako templates/history/list_shared.mako templates/history/options.mako templates/history/permissions.mako templates/history/rename.mako templates/history/share.mako templates/history/sharing.mako test/base/twilltestcase.py test/functional/__init__.py test/functional/test_DNAse_flanked_genes.py test/functional/test_get_data.py test/functional/test_history_functions.py test/functional/test_metadata_editing.py test/functional/test_security_and_libraries.py test/functional/test_sniffing_and_metadata_settings.py test/functional/test_toolbox.py diffs (truncated from 3697 to 3000 lines): diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/model/__init__.py Mon Jun 29 16:30:16 2009 -0400 @@ -214,22 +214,29 @@ if genome_build not in [None, '?']: self.genome_build = genome_build self.datasets.append( dataset ) - def copy( self, target_user = None ): + def copy( self, name=None, target_user=None ): + if not name: + name = self.name if not target_user: target_user = self.user - des = History( user = target_user ) - des.flush() - des.name = self.name + new_history = History( name=name, user=target_user ) + new_history.flush() for data in self.datasets: - new_data = data.copy( copy_children = True, target_history = des ) - des.add_dataset( new_data, set_hid = False ) + new_data = data.copy( copy_children=True, target_history=new_history ) + new_history.add_dataset( new_data, set_hid = False ) new_data.flush() - des.hid_counter = self.hid_counter - des.flush() - return des + new_history.hid_counter = self.hid_counter + new_history.flush() + return new_history @property def activatable_datasets( self ): - return [ hda for hda in self.datasets if not hda.dataset.deleted ] #this needs to be a list + # This needs to be a list + return [ hda for hda in self.datasets if not hda.dataset.deleted ] + +class HistoryUserShareAssociation( object ): + def __init__( self ): + self.history = None + self.user = None class UserRoleAssociation( object ): def __init__( self, user, role ): @@ -988,7 +995,7 @@ remote_host=None, remote_addr=None, referer=None, - current_history_id=None, + current_history=None, session_key=None, is_valid=False, prev_session_id=None ): @@ -997,12 +1004,11 @@ self.remote_host = remote_host self.remote_addr = remote_addr self.referer = referer - self.current_history_id = current_history_id + self.current_history = current_history self.session_key = session_key self.is_valid = is_valid self.prev_session_id = prev_session_id self.histories = [] - def add_history( self, history, association=None ): if association is None: self.histories.append( GalaxySessionToHistoryAssociation( self, history ) ) diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/model/mapping.py Mon Jun 29 16:30:16 2009 -0400 @@ -56,7 +56,14 @@ Column( "hid_counter", Integer, default=1 ), Column( "deleted", Boolean, index=True, default=False ), Column( "purged", Boolean, index=True, default=False ), - Column( "genome_build", TrimmedString( 40 ) ) ) + Column( "genome_build", TrimmedString( 40 ) ), + Column( "importable", Boolean, default=False ) ) + +HistoryUserShareAssociation.table = Table( "history_user_share_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ) + ) HistoryDatasetAssociation.table = Table( "history_dataset_association", metadata, Column( "id", Integer, primary_key=True ), @@ -575,6 +582,11 @@ active_datasets=relation( HistoryDatasetAssociation, primaryjoin=( ( HistoryDatasetAssociation.table.c.history_id == History.table.c.id ) & ( not_( HistoryDatasetAssociation.table.c.deleted ) ) ), order_by=asc( HistoryDatasetAssociation.table.c.hid ), lazy=False, viewonly=True ) ) ) +assign_mapper( context, HistoryUserShareAssociation, HistoryUserShareAssociation.table, + properties=dict( user=relation( User, backref='histories_shared_by_others' ), + history=relation( History, backref='users_shared_with' ) + ) ) + assign_mapper( context, User, User.table, properties=dict( histories=relation( History, backref="user", order_by=desc(History.table.c.update_time) ), diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/model/migrate/versions/0007_sharing_histories.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/model/migrate/versions/0007_sharing_histories.py Mon Jun 29 16:30:16 2009 -0400 @@ -0,0 +1,59 @@ +from sqlalchemy import * +from migrate import * + +import datetime +now = datetime.datetime.utcnow + +# Need our custom types, but don't import anything else from model +from galaxy.model.custom_types import * + +metadata = MetaData( migrate_engine ) + +HistoryUserShareAssociation_table = Table( "history_user_share_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ) + ) + +def upgrade(): + # Load existing tables + metadata.reflect() + # Create the history_user_share_association table + try: + HistoryUserShareAssociation_table.create() + except Exception, e: + log.debug( "Creating history_user_share_association table failed: %s" % str( e ) ) + # Add 1 column to the history table + try: + History_table = Table( "history", metadata, autoload=True ) + except NoSuchTableError: + History_table = None + log.debug( "Failed loading table history" ) + if History_table: + try: + col = Column( 'importable', Boolean, index=True, default=False ) + col.create( History_table ) + assert col is History_table.c.importable + except Exception, e: + log.debug( "Adding column 'importable' to history table failed: %s" % ( str( e ) ) ) + +def downgrade(): + # Load existing tables + metadata.reflect() + # Drop 1 column from the history table + try: + History_table = Table( "history", metadata, autoload=True ) + except NoSuchTableError: + History_table = None + log.debug( "Failed loading table history" ) + if History_table: + try: + col = History_table.c.importable + col.drop() + except Exception, e: + log.debug( "Dropping column 'importable' from history table failed: %s" % ( str( e ) ) ) + # Drop the history_user_share_association table + try: + HistoryUserShareAssociation_table.drop() + except Exception, e: + log.debug( "Dropping history_user_share_association table failed: %s" % str( e ) ) diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/security/__init__.py --- a/lib/galaxy/security/__init__.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/security/__init__.py Mon Jun 29 16:30:16 2009 -0400 @@ -275,6 +275,8 @@ def set_all_dataset_permissions( self, dataset, permissions={} ): # Set new permissions on a dataset, eliminating all current permissions # Delete all of the current permissions on the dataset + # TODO: If setting ACCESS permission, at least 1 user must have every role associated with this dataset, + # or the dataset is inaccessible. See admin/library_dataset_dataset_association() for dp in dataset.actions: dp.delete() dp.flush() @@ -285,8 +287,9 @@ for dp in [ self.model.DatasetPermissions( action, dataset, role ) for role in roles ]: dp.flush() def set_dataset_permission( self, dataset, permission={} ): - # TODO: is this method needed - see above method # Set a specific permission on a dataset, leaving all other current permissions on the dataset alone + # TODO: If setting ACCESS permission, at least 1 user must have every role associated with this dataset, + # or the dataset is inaccessible. See admin/library_dataset_dataset_association() for action, roles in permission.items(): if isinstance( action, Action ): action = action.action diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/tools/parameters/basic.py --- a/lib/galaxy/tools/parameters/basic.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/tools/parameters/basic.py Mon Jun 29 16:30:16 2009 -0400 @@ -1106,7 +1106,7 @@ except IndexError: pass #no valid options assert trans is not None, "DataToolParameter requires a trans" - history = trans.history + history = trans.get_history() assert history is not None, "DataToolParameter requires a history" if value is not None: if type( value ) != list: @@ -1170,11 +1170,10 @@ if trans.workflow_building_mode: return DummyDataset() assert trans is not None, "DataToolParameter requires a trans" - history = trans.history + history = trans.get_history() assert history is not None, "DataToolParameter requires a history" if self.optional: return None - history = trans.history most_recent_dataset = [None] filter_value = None if self.options: diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/controllers/history.py Mon Jun 29 16:30:16 2009 -0400 @@ -1,6 +1,9 @@ from galaxy.web.base.controller import * from galaxy.web.framework.helpers import time_ago, iff, grids from galaxy import util +from galaxy.model.mapping import desc +from galaxy.model.orm import * +from galaxy.util.json import * import webhelpers, logging from datetime import datetime from cgi import escape @@ -11,16 +14,19 @@ SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" class HistoryListGrid( grids.Grid ): - title = "Stored histories" model_class = model.History default_sort_key = "-create_time" columns = [ grids.GridColumn( "Name", key="name", - link=( lambda item: iff( item.deleted, None, dict( operation="switch", id=item.id ) ) ), - attach_popup=True ), + link=( lambda item: iff( item.deleted, None, dict( operation="switch", id=item.id ) ) ), + attach_popup=True ), grids.GridColumn( "Datasets (by state)", method='_build_datasets_by_state', ncells=4 ), - grids.GridColumn( "Status", method='_build_status' ), + grids.GridColumn( "Status", method='_build_status', + link=( lambda item: iff( item.users_shared_with, + dict( operation="sharing", id=item.id ), + None ) ), + attach_popup=False ), grids.GridColumn( "Age", key="create_time", format=time_ago ), grids.GridColumn( "Last update", key="update_time", format=time_ago ), # Valid for filtering but invisible @@ -39,13 +45,10 @@ grids.GridColumnFilter( "All", args=dict( deleted='All' ) ) ] default_filter = dict( deleted=False ) - def get_current_item( self, trans ): - return trans.history - + return trans.get_history() def apply_default_filter( self, trans, query ): return query.filter_by( user=trans.user, purged=False ) - def _build_datasets_by_state( self, trans, history ): rval = [] for state in ( 'ok', 'running', 'queued', 'error' ): @@ -55,10 +58,11 @@ else: rval.append( '' ) return rval - def _build_status( self, trans, history ): if history.deleted: return "deleted" + elif history.users_shared_with: + return "shared" return "" class HistoryController( BaseController ): @@ -68,9 +72,7 @@ return "" @web.expose def list_as_xml( self, trans ): - """ - XML history list for functional tests - """ + """XML history list for functional tests""" return trans.fill_template( "/history/list_as_xml.mako" ) list_grid = HistoryListGrid() @@ -79,29 +81,32 @@ @web.require_login( "work with multiple histories" ) def list( self, trans, **kwargs ): """List all available histories""" - current_history = trans.history + current_history = trans.get_history() status = message = None if 'operation' in kwargs: + history_ids = util.listify( kwargs.get( 'id', [] ) ) + histories = [] + shared_by_others = [] operation = kwargs['operation'].lower() if operation == "share": return self.share( trans, **kwargs ) elif operation == "rename": return self.rename( trans, **kwargs ) + elif operation == 'sharing': + return self.sharing( trans, id=kwargs['id'] ) # Display no message by default status, message = None, None refresh_history = False # Load the histories and ensure they all belong to the current user - history_ids = util.listify( kwargs.get( 'id', [] ) ) - histories = [] - for hid in history_ids: - history = model.History.get( hid ) + for history_id in history_ids: + history = get_history( trans, history_id ) if history: # Ensure history is owned by current user if history.user_id != None and trans.user: assert trans.user.id == history.user_id, "History does not belong to current user" histories.append( history ) else: - log.warn( "Invalid history id '%r' passed to list", hid ) + log.warn( "Invalid history id '%r' passed to list", history_id ) if histories: if operation == "switch": status, message = self._list_switch( trans, histories ) @@ -110,33 +115,36 @@ elif operation == "delete": status, message = self._list_delete( trans, histories ) if current_history in histories: + # Deleted the current history, so a new, empty history was + # created automatically, and we need to refresh the history frame trans.template_context['refresh_frames'] = ['history'] elif operation == "undelete": status, message = self._list_undelete( trans, histories ) trans.sa_session.flush() # Render the list view - return self.list_grid( trans, status=status, message=message, **kwargs ) + return self.list_grid( trans, status=status, message=message, template='/history/grid.mako', **kwargs ) def _list_delete( self, trans, histories ): """Delete histories""" n_deleted = 0 deleted_current = False + message_parts = [] for history in histories: - if not history.deleted: + if history.users_shared_with: + message_parts.append( "History (%s) has been shared with others, unshare it before deleting it. " % history.name ) + elif not history.deleted: # We'll not eliminate any DefaultHistoryPermissions in case we undelete the history later - # Mark history as deleted in db history.deleted = True # If deleting the current history, make a new current. - if history == trans.history: + if history == trans.get_history(): deleted_current = True trans.new_history() - trans.log_event( "History id %d marked as deleted" % history.id ) + trans.log_event( "History (%s) marked as deleted" % history.name ) n_deleted += 1 status = SUCCESS - message_parts = [] if n_deleted: - message_parts.append( "Deleted %d histories." % n_deleted ) + message_parts.append( "Deleted %d histories. " % n_deleted ) if deleted_current: - message_parts.append( "Your active history was deleted, a new empty history is now active.") + message_parts.append( "Your active history was deleted, a new empty history is now active. " ) status = INFO return ( status, " ".join( message_parts ) ) def _list_undelete( self, trans, histories ): @@ -151,20 +159,20 @@ if not history.default_permissions: # For backward compatibility - for a while we were deleting all DefaultHistoryPermissions on # the history when we deleted the history. We are no longer doing this. - # Need to add default DefaultHistoryPermissions since they were deleted when the history was deleted + # Need to add default DefaultHistoryPermissions in case they were deleted when the history was deleted default_action = trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS private_user_role = trans.app.security_agent.get_private_user_role( history.user ) default_permissions = {} default_permissions[ default_action ] = [ private_user_role ] trans.app.security_agent.history_set_default_permissions( history, default_permissions ) n_undeleted += 1 - trans.log_event( "History id %d marked as undeleted" % history.id ) + trans.log_event( "History (%s) %d marked as undeleted" % history.name ) status = SUCCESS message_parts = [] if n_undeleted: message_parts.append( "Undeleted %d histories." % n_undeleted ) if n_already_purged: - message_parts.append( "%d have already been purged and cannot be undeleted." % n_already_purged ) + message_parts.append( "%d histories have already been purged and cannot be undeleted." % n_already_purged ) status = WARNING return status, "".join( message_parts ) def _list_switch( self, trans, histories ): @@ -172,25 +180,39 @@ new_history = histories[0] galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() + association = trans.app.model.GalaxySessionToHistoryAssociation \ + .filter_by( session_id=galaxy_session.id, history_id=trans.security.decode_id( new_history.id ) ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) new_history.flush() trans.set_history( new_history ) - trans.log_event( "History switched to id: %s, name: '%s'" % (str(new_history.id), new_history.name ) ) # No message return None, None + @web.expose + def list_shared( self, trans, **kwd ): + """List histories shared with current user by others""" + params = util.Params( kwd ) + msg = util.restore_text( params.get( 'msg', '' ) ) + shared_by_others = trans.sa_session \ + .query( model.HistoryUserShareAssociation ) \ + .filter_by( user=trans.user ) \ + .join( 'history' ) \ + .filter( model.History.deleted == False ) \ + .order_by( desc( model.History.update_time ) ) \ + .all() + return trans.fill_template( "/history/list_shared.mako", shared_by_others=shared_by_others, msg=msg, messagetype='done' ) @web.expose def delete_current( self, trans ): """Delete just the active history -- this does not require a logged in user.""" history = trans.get_history() + if history.users_shared_with: + return trans.show_error_message( "History (%s) has been shared with others, unshare it before deleting it. " % history.name ) if not history.deleted: history.deleted = True history.flush() trans.log_event( "History id %d marked as deleted" % history.id ) - # Regardless of whether it was previously deleted, we make a new - # history active + # Regardless of whether it was previously deleted, we make a new history active trans.new_history() return trans.show_ok_message( "History deleted, a new history is active", refresh_frames=['history'] ) @web.expose @@ -200,7 +222,7 @@ # user (if logged in) or the current history assert history is not None if history.user is None: - assert history == trans.history + assert history == trans.get_history() else: assert history.user == trans.user # Rename @@ -208,48 +230,49 @@ trans.sa_session.flush() @web.expose def imp( self, trans, id=None, confirm=False, **kwd ): - # TODO clean this up and make sure functionally correct + """Import another user's history via a shared URL""" msg = "" user = trans.get_user() user_history = trans.get_history() if not id: - return trans.show_error_message( "You must specify a history you want to import.") - id = trans.security.decode_id( id ) - import_history = trans.app.model.History.get( id ) + return trans.show_error_message( "You must specify a history you want to import." ) + import_history = get_history( trans, id ) if not import_history: return trans.show_error_message( "The specified history does not exist.") + if not import_history.importable: + error( "The owner of this history has disabled imports via this link." ) if user: if import_history.user_id == user.id: - return trans.show_error_message( "You cannot import your own history.") - new_history = import_history.copy( target_user=trans.user ) - new_history.name = "imported: "+new_history.name + return trans.show_error_message( "You cannot import your own history." ) + new_history = import_history.copy( target_user=user ) + new_history.name = "imported: " + new_history.name new_history.user_id = user.id galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() + association = trans.app.model.GalaxySessionToHistoryAssociation \ + .filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) new_history.flush() if not user_history.datasets: trans.set_history( new_history ) - trans.log_event( "History imported, id: %s, name: '%s': " % (str(new_history.id) , new_history.name ) ) return trans.show_ok_message( """ History "%s" has been imported. Click <a href="%s">here</a> to begin.""" % ( new_history.name, web.url_for( '/' ) ) ) elif not user_history.datasets or confirm: new_history = import_history.copy() - new_history.name = "imported: "+new_history.name + new_history.name = "imported: " + new_history.name new_history.user_id = None galaxy_session = trans.get_galaxy_session() try: - association = trans.app.model.GalaxySessionToHistoryAssociation.filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() + association = trans.app.model.GalaxySessionToHistoryAssociation \ + .filter_by( session_id=galaxy_session.id, history_id=new_history.id ).first() except: association = None new_history.add_galaxy_session( galaxy_session, association=association ) new_history.flush() trans.set_history( new_history ) - trans.log_event( "History imported, id: %s, name: '%s': " % (str(new_history.id) , new_history.name ) ) return trans.show_ok_message( """ History "%s" has been imported. Click <a href="%s">here</a> to begin.""" % ( new_history.name, web.url_for( '/' ) ) ) @@ -262,106 +285,33 @@ 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 shared history + # in the cloned history params = util.Params( kwd ) - action = params.get( 'action', None ) - if action == "no_share": - trans.response.send_redirect( url_for( controller='root', action='history_options' ) ) - if not id: - id = trans.get_history().id - id = util.listify( id ) - send_to_err = "" - histories = [] - for hid in id: - histories.append( trans.app.model.History.get( hid ) ) + user = trans.get_user() if not email: - return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) - user = trans.get_user() - send_to_users = [] - for email_address in util.listify( email ): - email_address = email_address.strip() - if email_address: - if email_address == user.email: - send_to_err += "You can't send histories to yourself. " - else: - send_to_user = trans.app.model.User.filter( trans.app.model.User.table.c.email==email_address ).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 + if not id: + # Default to the current history + id = trans.security.encode_id( trans.history.id ) + id = util.listify( id ) + send_to_err = "" + histories = [] + for history_id in id: + histories.append( 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. " % email - return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) - if params.get( 'share_proceed_button', False ) and action == 'share': - # We need to filter out all histories that cannot be shared - filtered_histories = {} - for history in histories: - for send_to_user in send_to_users: - # Only deal with datasets that have not been purged - for hda in history.activatable_datasets: - # The history can be shared if it contains at least 1 public dataset or 1 dataset that the - # other user can access. Inaccessible datasets contained in the history will be displayed - # in the shared history, but "greyed out", so they cannot be viewed or used. - if trans.app.security_agent.dataset_is_public( hda.dataset ) or \ - trans.app.security_agent.allow_action( send_to_user, - trans.app.security_agent.permitted_actions.DATASET_ACCESS, - dataset=hda ): - if send_to_user in filtered_histories: - filtered_histories[ send_to_user ].append( history ) - else: - filtered_histories[ send_to_user ] = [ history ] - break - return self._share_histories( trans, user, send_to_users, send_to_err, filtered_histories=filtered_histories ) - elif params.get( 'history_share_btn', False ) or action != 'share': - # The user is attempting to share histories whose datasets cannot all be accessed by other users. In this case, - # the user sharing the histories can: - # 1) action=='public': chose 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 - # 3) action=='share': share only what can be shared when no permissions are changed - this case is handled above - # 4) action=='no_share': Do not share anything - this case is handled above. - can_change = {} - cannot_change = {} - no_change_needed = {} - for history in histories: - # Only deal with datasets that have not been purged - for hda in history.activatable_datasets: - if trans.app.security_agent.dataset_is_public( hda.dataset ): - if history not in no_change_needed: - no_change_needed[ history ] = [ hda ] - else: - no_change_needed[ history ].append( hda ) - elif not trans.app.security_agent.allow_action( send_to_user, - trans.app.security_agent.permitted_actions.DATASET_ACCESS, - dataset=hda ): - # The user with which we are sharing the history does not have access permission on the current dataset - if trans.app.security_agent.allow_action( user, - trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS, - dataset=hda ) 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 ) - elif history not in can_change: - # Build the set of histories / datasets on which the current user has authority - # to "manage permissions". This is used in /history/share.mako - can_change[ history ] = [ hda ] - else: - can_change[ history ].append( hda ) - else: - if action in [ "private", "public" ]: - # Don't change stuff that the user doesn't have permission to change - continue - elif history not in cannot_change: - # Build the set of histories / datasets on which the current user does - # not have authority to "manage permissions". This is used in /history/share.mako - cannot_change[ history ] = [ hda ] - else: - cannot_change[ history ].append( hda ) + return trans.fill_template( "/history/share.mako", + histories=histories, + email=email, + send_to_err=send_to_err ) + if params.get( 'share_button', False ): + can_change, cannot_change, no_change_needed = \ + self._populate_restricted( trans, user, histories, send_to_users, None ) if can_change or cannot_change: return trans.fill_template( "/history/share.mako", histories=histories, @@ -370,65 +320,253 @@ can_change=can_change, cannot_change=cannot_change, no_change_needed=no_change_needed ) - return self._share_histories( trans, user, send_to_users, send_to_err, histories=histories ) + 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 ) - def _share_histories( self, trans, user, send_to_users, send_to_err, histories=[], filtered_histories={} ): + @web.expose + @web.require_login( "share restricted histories with other users" ) + def share_restricted( self, trans, id=None, email="", **kwd ): + action = kwd[ 'action' ] + if action == "no_share": + trans.response.send_redirect( url_for( controller='root', action='history_options' ) ) + user = trans.get_user() + histories, send_to_users, send_to_err = self._get_histories_and_users( trans, user, id, email ) + send_to_err = '' + can_change, cannot_change, no_change_needed = \ + self._populate_restricted( trans, user, histories, send_to_users, action ) + # 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.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.allow_action( send_to_user, + trans.app.security_agent.permitted_actions.DATASET_ACCESS, + dataset=hda ): + # The user with which we are sharing the history does not have access permission on the current dataset + if trans.app.security_agent.allow_action( user, + trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS, + dataset=hda ) 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 + # it's 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 = util.listify( id ) + send_to_err = "" + histories = [] + for history_id in id: + histories.append( get_history( trans, history_id ) ) + send_to_users = [] + for email_address in 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.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.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 ): + # 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 + # 3) action=='share_anyway': share only what can be shared when no permissions are changed + # 4) action=='no_share': Do not share anything + # In addition, the user may be sharing a history with a user with which the history was already shared + # and it will not be shared twice. + # This method will populate the can_change, cannot_change and no_change_needed dictionaries. + can_change = {} + cannot_change = {} + no_change_needed = {} + 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.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.dataset_is_public( hda.dataset ): + # Build the dict that will show the user what doesn't need to be changed + 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 ) + elif not trans.app.security_agent.allow_action( send_to_user, + trans.app.security_agent.permitted_actions.DATASET_ACCESS, + dataset=hda ): + # The user with which we are sharing the history does not have access permission on the current dataset + if trans.app.security_agent.allow_action( user, + trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS, + dataset=hda ) 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 send_to_user not in can_change: + # Build the set of histories / datasets on which the current user has authority + # to "manage permissions". + 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" ]: + # Don't change stuff that the user doesn't have permission to change + continue + #elif send_to_user not in cannot_change: + if send_to_user not in cannot_change: + # Build the set of histories / datasets on which the current user does + # not have authority to "manage permissions". + 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 + def _share_histories( self, trans, user, send_to_err, histories={} ): + # histories looks like: { userA: [ historyX, historyY ], userB: [ historyY ] } msg = "" - if not send_to_users: - msg = "No users have been specified with which to share histories" sent_to_emails = [] - for sent_to_user in send_to_users: - sent_to_emails.append( sent_to_user.email ) + 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 and not filtered_histories: - msg = "No histories can be sent to (%s) without changing dataset permissions associating a sharing role with them" % emails - elif histories: - history_names = [] - for history in histories: - history_names.append( history.name ) - for send_to_user in send_to_users: - new_history = history.copy( target_user=send_to_user ) - new_history.name = history.name + " from " + user.email - new_history.user_id = send_to_user.id - self.app.model.flush() - msg = "Histories (%s) have been shared with: %s. " % ( ",".join( history_names ), emails ) - elif filtered_histories: - # filtered_histories is a dictionary like: { user: [ history, history ], user: [ history ] } - for send_to_user, histories in filtered_histories.items(): - history_names = [] - for history in histories: - history_names.append( history.name ) - new_history = history.copy( target_user=send_to_user ) - new_history.name = history.name + " from " + user.email - new_history.user_id = send_to_user.id - self.app.model.flush() - msg += "Histories (%s) have been shared with: %s. " % ( ",".join( history_names ), send_to_user.email ) + 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 + session = trans.sa_session + session.save_or_update( share ) + session.flush() + if history not in shared_histories: + shared_histories.append( history ) if send_to_err: msg += send_to_err - return trans.show_message( msg ) + return self.sharing( trans, histories=shared_histories, msg=msg ) + @web.expose + @web.require_login( "share histories with other users" ) + def sharing( self, trans, histories=[], id=None, **kwd ): + # histories looks like: [ historyX, historyY ] + params = util.Params( kwd ) + msg = util.restore_text ( params.get( 'msg', '' ) ) + if id: + histories = [ get_history( trans, id ) ] + for history in histories: + if params.get( 'enable_import_via_link', False ): + history.importable = True + history.flush() + elif params.get( 'disable_import_via_link', False ): + history.importable = False + history.flush() + elif params.get( 'unshare_user', False ): + user = trans.app.model.User.get( trans.security.decode_id( kwd[ 'unshare_user' ] ) ) + if not user: + msg = 'History (%s) does not seem to be shared with user (%s)' % ( history.name, user.email ) + return trans.fill_template( 'history/sharing.mako', histories=histories, msg=msg, messagetype='error' ) + association = trans.app.model.HistoryUserShareAssociation.filter_by( user=user, history=history ).one() + association.delete() + association.flush() + if not id: + shared_msg = "History (%s) now shared with: %d users. " % ( history.name, len( history.users_shared_with ) ) + msg = '%s%s' % ( shared_msg, msg ) + return trans.fill_template( 'history/sharing.mako', histories=histories, msg=msg, messagetype='done' ) @web.expose @web.require_login( "rename histories" ) def rename( self, trans, id=None, name=None, **kwd ): user = trans.get_user() - if not isinstance( id, list ): - if id != None: - id = [ id ] - if not isinstance( name, list ): - if name != None: - name = [ name ] + if not id: + # Default to the current history + history = trans.get_history() + if not history.user: + return trans.show_error_message( "You must save your history before renaming it." ) + id = trans.security.encode_id( history.id ) + id = util.listify( id ) + name = util.listify( name ) histories = [] cur_names = [] - if not id: - if not trans.get_history().user: - return trans.show_error_message( "You must save your history before renaming it." ) - id = [trans.get_history().id] for history_id in id: - history = trans.app.model.History.get( history_id ) + history = get_history( trans, history_id ) if history and history.user_id == user.id: - histories.append(history) - cur_names.append(history.name) - if not name or len(histories)!=len(name): - return trans.fill_template( "/history/rename.mako",histories=histories ) + histories.append( history ) + cur_names.append( history.name ) + if not name or len( histories ) != len( name ): + return trans.fill_template( "/history/rename.mako", histories=histories ) change_msg = "" for i in range(len(histories)): if histories[i].user_id == user.id: @@ -445,3 +583,38 @@ else: change_msg = change_msg + "<p>History: "+cur_names[i]+" does not appear to belong to you.</p>" return trans.show_message( "<p>%s" % change_msg, refresh_frames=['history'] ) + @web.expose + @web.require_login( "clone shared Galaxy history" ) + def clone( self, trans, id ): + history = get_history( trans, id, check_ownership=False ) + user = trans.get_user() + if history.user == user: + owner = True + else: + if trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ + .filter_by( user=user, history=history ).count() == 0: + return trans.show_error_message( "The history you are attempting to clone is not owned by you or shared with you. " ) + owner = False + name = "Clone of '%s'" % history.name + if not owner: + name += " shared by '%s'" % history.user.email + new_history = history.copy( name=name, target_user=user ) + # Render the list view + return trans.show_ok_message( 'Clone with name "%s" is now included in your list of stored histories.' % new_history.name ) + +## ---- Utility methods ------------------------------------------------------- + +def get_history( trans, id, check_ownership=True ): + """Get a History from the database by id, verifying ownership.""" + # Load history from database + id = trans.security.decode_id( id ) + history = trans.sa_session.query( model.History ).get( id ) + if not history: + err+msg( "History not found" ) + # Verify ownership + user = trans.get_user() + if not user: + error( "Must be logged in to manage histories" ) + if check_ownership and not( history.user == user ): + error( "History is not owned by current user" ) + return history diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/controllers/root.py --- a/lib/galaxy/web/controllers/root.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/controllers/root.py Mon Jun 29 16:30:16 2009 -0400 @@ -50,19 +50,18 @@ @web.expose def history( self, trans, as_xml=False, show_deleted=False ): """ - Display the current history, creating a new history if neccesary. - + Display the current history, creating a new history if necessary. NOTE: No longer accepts "id" or "template" options for security reasons. """ - history = trans.get_history() if trans.app.config.require_login and not trans.user: return trans.fill_template( '/no_access.mako', message = 'Please log in to access Galaxy histories.' ) + history = trans.get_history( create=True ) if as_xml: trans.response.set_content_type('text/xml') return trans.fill_template_mako( "root/history_as_xml.mako", history=history, show_deleted=util.string_as_bool( show_deleted ) ) else: template = "root/history.mako" - return trans.fill_template( "root/history.mako", history = history, show_deleted = util.string_as_bool( show_deleted ) ) + return trans.fill_template( "root/history.mako", history=history, show_deleted=util.string_as_bool( show_deleted ) ) @web.expose def dataset_state ( self, trans, id=None, stamp=None ): @@ -350,9 +349,10 @@ @web.expose def history_options( self, trans ): - """Displays a list of history related actions""" + """Displays a list of history related actions""" return trans.fill_template( "/history/options.mako", - user = trans.get_user(), history = trans.get_history() ) + user=trans.get_user(), + history=trans.get_history( create=True ) ) @web.expose def history_delete( self, trans, id ): """ @@ -421,7 +421,7 @@ @web.expose def history_new( self, trans, name=None ): trans.new_history( name=name ) - trans.log_event( "Created new History, id: %s." % str(trans.get_history().id) ) + trans.log_event( "Created new History, id: %s." % str(trans.history.id) ) return trans.show_message( "New history created", refresh_frames = ['history'] ) @web.expose def history_add_to( self, trans, history_id=None, file_data=None, name="Data Added to History",info=None,ext="txt",dbkey="?",copy_access_from=None,**kwd ): diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/controllers/user.py --- a/lib/galaxy/web/controllers/user.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/controllers/user.py Mon Jun 29 16:30:16 2009 -0400 @@ -81,6 +81,7 @@ @web.expose def login( self, trans, email='', password='' ): + log.debug( "###IN login, email:%s, password: %s" % ( email, password )) email_error = password_error = None # Attempt login if trans.app.config.require_login: diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/controllers/workflow.py --- a/lib/galaxy/web/controllers/workflow.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/controllers/workflow.py Mon Jun 29 16:30:16 2009 -0400 @@ -129,7 +129,7 @@ if stored.importable == False: error( "The owner of this workflow has disabled imports via this link" ) elif stored.user == trans.user: - error( "You are already the ownder of this workflow, can't import" ) + error( "You are already the owner of this workflow, can't import" ) elif stored.deleted: error( "This workflow has been deleted, can't import" ) elif session.query( model.StoredWorkflowUserShareAssociation ) \ diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/framework/__init__.py --- a/lib/galaxy/web/framework/__init__.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/framework/__init__.py Mon Jun 29 16:30:16 2009 -0400 @@ -180,7 +180,7 @@ except: event.message = message try: - event.history = self.history + event.history = self.get_history() except: event.history = None try: @@ -337,11 +337,11 @@ remote_host = self.request.remote_host, remote_addr = self.request.remote_addr, referer = self.request.headers.get( 'Referer', None ) ) - # Invalidated an existing sesssion for some reason, keep track if prev_galaxy_session: + # Invalidated an existing session for some reason, keep track galaxy_session.prev_session_id = prev_galaxy_session.id - # The new session should be immediately associated with a user if user_for_new_session: + # The new session should be associated with the user galaxy_session.user = user_for_new_session return galaxy_session def __get_or_create_remote_user( self, remote_user_email ): @@ -350,12 +350,23 @@ """ # remote_user middleware ensures HTTP_REMOTE_USER exists user = self.app.model.User.filter( self.app.model.User.table.c.email==remote_user_email ).first() - if user is None: + if user: + # GVK: June 29, 2009 - This is to correct the behavior of a previous bug where a private + # role and default user / history permissions were not set for remote users. When a + # remote user authenticates, we'll look for this information, and if missing, create it. + if not self.app.security_agent.get_private_user_role( user ): + self.app.security_agent.create_private_user_role( user ) + if not user.default_permissions: + self.app.security_agent.user_set_default_permissions( user, history=True, dataset=True ) + elif user is None: random.seed() user = self.app.model.User( email=remote_user_email ) user.set_password_cleartext( ''.join( random.sample( string.letters + string.digits, 12 ) ) ) user.external = True user.flush() + self.app.security_agent.create_private_user_role( user ) + # We set default user permissions, before we log in and set the default history permissions + self.app.security_agent.user_set_default_permissions( user ) #self.log_event( "Automatically created account '%s'", user.email ) elif user.deleted: return self.show_error_message( "Your account is no longer valid, contact your Galaxy administrator to activate your account." ) @@ -365,7 +376,6 @@ Update the session cookie to match the current session. """ self.set_cookie( self.security.encode_session_key( self.galaxy_session.session_key ), name=name ) - def handle_user_login( self, user ): """ Login a new user (possibly newly created) @@ -374,24 +384,39 @@ - if old session had a history and it was not associated with a user, associate it with the new session, otherwise associate the current session's history with the user """ + # Set the previous session prev_galaxy_session = self.galaxy_session prev_galaxy_session.is_valid = False + # Define a new current_session self.galaxy_session = self.__create_new_session( prev_galaxy_session, user ) - # If the session already had a history, we associate it with the new - # session, but only if it does not belong to a different user. - if prev_galaxy_session.current_history and \ - ( prev_galaxy_session.current_history.user == user or prev_galaxy_session.user is None ): - history = prev_galaxy_session.current_history - elif self.galaxy_session.current_history: - history = self.galaxy_session.current_history - else: - history = self.history + # Associated the current user's last accessed history (if exists) with their new session + history = None + try: + users_last_session = user.galaxy_sessions[0] + last_accessed = True + except: + users_last_session = None + last_accessed = False + if users_last_session and users_last_session.current_history: + history = users_last_session.current_history + if not history: + if prev_galaxy_session.current_history: + if prev_galaxy_session.current_history.user is None or prev_galaxy_session.current_history.user == user: + # If the previous galaxy session had a history, associate it with the new + # session, but only if it didn't belong to a different user. + history = prev_galaxy_session.current_history + elif self.galaxy_session.current_history: + history = self.galaxy_session.current_history + else: + history = self.get_history( create=True ) if history not in self.galaxy_session.histories: self.galaxy_session.add_history( history ) if history.user is None: history.user = user self.galaxy_session.current_history = history - self.app.security_agent.history_set_default_permissions( history, dataset=True, bypass_manage_permission=True ) + if not last_accessed: + # Only set default history permissions if current history is not from a previous session + self.app.security_agent.history_set_default_permissions( history, dataset=True, bypass_manage_permission=True ) self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session, history ] ) # This method is not called from the Galaxy reports, so the cookie will always be galaxysession self.__update_session_cookie( name='galaxysession' ) @@ -403,7 +428,7 @@ """ prev_galaxy_session = self.galaxy_session prev_galaxy_session.is_valid = False - self.galaxy_session = self.__create_new_session( prev_galaxy_session, None ) + self.galaxy_session = self.__create_new_session( prev_galaxy_session ) self.sa_session.flush( [ prev_galaxy_session, self.galaxy_session ] ) # This method is not called from the Galaxy reports, so the cookie will always be galaxysession self.__update_session_cookie( name='galaxysession' ) @@ -416,16 +441,15 @@ def get_history( self, create=False ): """ - Load the current history. - - NOTE: It looks like create was being ignored for a long time, so this - will currently *always* create a new history. This is wasteful - though, and we should verify that callers are using the create - flag correctly and fix. + Load the current history, creating a new one only if there is not + current history and we're told to create" """ history = self.galaxy_session.current_history - if history is None: - history = self.new_history() + if not history: + if util.string_as_bool( create ): + history = self.new_history() + else: + raise "get_history() returning None" return history def set_history( self, history ): if history and not history.deleted: @@ -557,7 +581,8 @@ the user (chromInfo in history). """ dbnames = list() - datasets = self.app.model.HistoryDatasetAssociation.filter_by(deleted=False, history_id=self.history.id, extension="len").all() + datasets = self.app.model.HistoryDatasetAssociation \ + .filter_by(deleted=False, history_id=self.history.id, extension="len").all() if len(datasets) > 0: dbnames.append( (util.dbnames.default_value, '--------- User Defined Builds ----------') ) for dataset in datasets: @@ -569,7 +594,8 @@ """ Returns the db_file dataset associated/needed by `dataset`, or `None`. """ - datasets = self.app.model.HistoryDatasetAssociation.filter_by(deleted=False, history_id=self.history.id, extension="len").all() + datasets = self.app.model.HistoryDatasetAssociation \ + .filter_by(deleted=False, history_id=self.history.id, extension="len").all() for ds in datasets: if dbkey == ds.dbkey: return ds diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/framework/helpers/grids.py Mon Jun 29 16:30:16 2009 -0400 @@ -3,7 +3,9 @@ from galaxy.web import url_for -import sys +import sys, logging + +log = logging.getLogger( __name__ ) class Grid( object ): """ @@ -12,6 +14,7 @@ title = "" exposed = True model_class = None + template = None columns = [] standard_filters = [] default_filter = None @@ -22,6 +25,7 @@ def __call__( self, trans, **kwargs ): status = kwargs.get( 'status', None ) message = kwargs.get( 'message', None ) + template = kwargs.get( 'template', None ) session = trans.sa_session # Build initial query query = self.build_initial_query( session ) @@ -70,8 +74,15 @@ if len(args) > 0: new_kwargs.update( args[0] ) new_kwargs.update( kwargs ) + # We need to encode item ids + if 'id' in new_kwargs: + id = new_kwargs[ 'id' ] + if isinstance( id, list ): + new_args[ 'id' ] = [ trans.security.encode_id( i ) for i in id ] + else: + new_kwargs[ 'id' ] = trans.security.encode_id( id ) return url_for( **new_kwargs ) - return trans.fill_template( "grid.mako", + return trans.fill_template( template, grid=self, query=query, sort_key=sort_key, diff -r b26e2ef8726c -r 533ae45c4440 lib/galaxy/web/security/__init__.py --- a/lib/galaxy/web/security/__init__.py Fri Jun 26 09:22:31 2009 -0400 +++ b/lib/galaxy/web/security/__init__.py Mon Jun 29 16:30:16 2009 -0400 @@ -30,9 +30,7 @@ random_pool.stir() return str( number.getRandomNumber( nbits, random_pool.get_bytes ) ) - class SecurityHelper( object ): - # TODO: checking if histories/datasets are owned by the current user) will be moved here. def __init__( self, **config ): self.id_secret = config['id_secret'] self.id_cipher = Blowfish.new( self.id_secret ) diff -r b26e2ef8726c -r 533ae45c4440 templates/grid.mako --- a/templates/grid.mako Fri Jun 26 09:22:31 2009 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,216 +0,0 @@ -<%inherit file="/base.mako"/> -<%def name="title()">${grid.title}</%def> - -%if message: - <p> - <div class="${message_type}message transient-message">${message}</div> - <div style="clear: both"></div> - </p> -%endif - -<%def name="javascripts()"> - ${parent.javascripts()} - <script type="text/javascript"> - ## TODO: generalize and move into galaxy.base.js - $(document).ready(function() { - $(".grid").each( function() { - var grid = this; - var checkboxes = $(this).find("input.grid-row-select-checkbox"); - var update = $(this).find( "span.grid-selected-count" ); - $(checkboxes).each( function() { - $(this).change( function() { - var n = $(checkboxes).filter("[checked]").size(); - update.text( n ); - }); - }) - }); - }); - - ## Can this be moved into base.mako? - %if refresh_frames: - %if 'masthead' in refresh_frames: - ## Refresh masthead == user changes (backward compatibility) - if ( parent.user_changed ) { - %if trans.user: - parent.user_changed( "${trans.user.email}", ${int( app.config.is_admin_user( trans.user ) )} ); - %else: - parent.user_changed( null, false ); - %endif - } - %endif - %if 'history' in refresh_frames: - if ( parent.frames && parent.frames.galaxy_history ) { - parent.frames.galaxy_history.location.href="${h.url_for( controller='root', action='history')}"; - if ( parent.force_right_panel ) { - parent.force_right_panel( 'show' ); - } - } - %endif - %if 'tools' in refresh_frames: - if ( parent.frames && parent.frames.galaxy_tools ) { - parent.frames.galaxy_tools.location.href="${h.url_for( controller='root', action='tool_menu')}"; - if ( parent.force_left_panel ) { - parent.force_left_panel( 'show' ); - } - } - %endif - %endif - </script> -</%def> - -<%def name="stylesheets()"> - <link href="${h.url_for('/static/style/base.css')}" rel="stylesheet" type="text/css" /> - <style> - ## Not generic to all grids -- move to base? - .count-box { - min-width: 1.1em; - padding: 5px; - border-width: 1px; - border-style: solid; - text-align: center; - display: inline-block; - } - </style> -</%def> - - -<div class="grid-header"> - <h2>${grid.title}</h2> - <span class="title">Filter:</span> - %for i, filter in enumerate( grid.standard_filters ): - %if i > 0: - <span>|</span> - %endif - <span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span> - %endfor -</div> - -<form name="history_actions" action="${url()}" method="post" > - - <table class="grid"> - <thead> - <tr> - <th></th> - %for column in grid.columns: - %if column.visible: - <% - href = "" - extra = "" - if column.sortable: - if sort_key == column.key: - if sort_order == "asc": - href = url( sort=( "-" + column.key ) ) - extra = "↓" - else: - href = url( sort=( column.key ) ) - extra = "↑" - else: - href = url( sort=column.key ) - %> - <th \ - %if column.ncells > 1: - colspan="${column.ncells}" - %endif - > - %if href: - <a href="${href}">${column.label}</a> - %else: - ${column.label} - %endif - <span>${extra}</span> - </th> - %endif - %endfor - <th></th> - - </tr> - </thead> - - <tbody> - - %for i, item in enumerate( query ): - - - <tr \ - %if current_item == item: - class="current" \ - %endif - > - - ## Item selection column - <td style="width: 1.5em;"><input type="checkbox" name="id" value=${item.id} class="grid-row-select-checkbox"></input></td> - - ## Data columns - %for column in grid.columns: - %if column.visible: - <% - # Link - if column.link and column.link( item ): - href = url( **column.link( item ) ) - else: - href = None - # Value (coerced to list so we can loop) - value = column.get_value( trans, grid, item ) - if column.ncells == 1: - value = [ value ] - %> - - %for cellnum, v in enumerate( value ): - <% - # Attach popup menu? - if column.attach_popup and cellnum == 0: - extra = '<a id="grid-%d-popup" class="popup-arrow" style="display: none;">▼</a>' % i - else: - extra = "" - %> - - %if href: - <td><a href="${href}">${v}</a> ${extra}</td> - %else: - <td >${v}${extra}</td> - %endif - </td> - %endfor - %endif - %endfor - - ## Actions column - <td> - <div popupmenu="grid-${i}-popup"> - - %for operation in grid.operations: - %if operation.allowed( item ): - <a class="action-button" href="${url( operation=operation.label, id=item.id )}">${operation.label}</a> - %endif - %endfor - - </div> - </td> - - </tr> - - %endfor - - </tbody> - - <tfoot> - <tr> - <td></td> - <td colspan="100"> - For <span class="grid-selected-count"></span> selected histories: - %for operation in grid.operations: - %if operation.allow_multiple: - <input type="submit" name="operation" value="${operation.label}" class="action-button"> - %endif - %endfor - - </td> - </tr> - </tfoot> - - </table> - - </form> - - - diff -r b26e2ef8726c -r 533ae45c4440 templates/history/grid.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/history/grid.mako Mon Jun 29 16:30:16 2009 -0400 @@ -0,0 +1,193 @@ +<%inherit file="/base.mako"/> +<%def name="title()">${grid.title}</%def> + +%if message: + <p> + <div class="${message_type}message transient-message">${message}</div> + <div style="clear: both"></div> + </p> +%endif + +<%def name="javascripts()"> + ${parent.javascripts()} + <script type="text/javascript"> + ## TODO: generalize and move into galaxy.base.js + $(document).ready(function() { + $(".grid").each( function() { + var grid = this; + var checkboxes = $(this).find("input.grid-row-select-checkbox"); + var update = $(this).find( "span.grid-selected-count" ); + $(checkboxes).each( function() { + $(this).change( function() { + var n = $(checkboxes).filter("[checked]").size(); + update.text( n ); + }); + }) + }); + }); + ## Can this be moved into base.mako? + %if refresh_frames: + %if 'masthead' in refresh_frames: + ## Refresh masthead == user changes (backward compatibility) + if ( parent.user_changed ) { + %if trans.user: + parent.user_changed( "${trans.user.email}", ${int( app.config.is_admin_user( trans.user ) )} ); + %else: + parent.user_changed( null, false ); + %endif + } + %endif + %if 'history' in refresh_frames: + if ( parent.frames && parent.frames.galaxy_history ) { + parent.frames.galaxy_history.location.href="${h.url_for( controller='root', action='history')}"; + if ( parent.force_right_panel ) { + parent.force_right_panel( 'show' ); + } + } + %endif + %if 'tools' in refresh_frames: + if ( parent.frames && parent.frames.galaxy_tools ) { + parent.frames.galaxy_tools.location.href="${h.url_for( controller='root', action='tool_menu')}"; + if ( parent.force_left_panel ) { + parent.force_left_panel( 'show' ); + } + } + %endif + %endif + </script> +</%def> + +<%def name="stylesheets()"> + <link href="${h.url_for('/static/style/base.css')}" rel="stylesheet" type="text/css" /> + <style> + ## Not generic to all grids -- move to base? + .count-box { + min-width: 1.1em; + padding: 5px; + border-width: 1px; + border-style: solid; + text-align: center; + display: inline-block; + } + </style> +</%def> + +<div class="grid-header"> + <h2>${grid.title}</h2> + <span class="title">Filter:</span> + %for i, filter in enumerate( grid.standard_filters ): + %if i > 0: + <span>|</span> + %endif + <span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span> + %endfor +</div> + +<form name="history_actions" action="${url()}" method="post" > + <table class="grid"> + <thead> + <tr> + <th></th> + %for column in grid.columns: + %if column.visible: + <% + href = "" + extra = "" + if column.sortable: + if sort_key == column.key: + if sort_order == "asc": + href = url( sort=( "-" + column.key ) ) + extra = "↓" + else: + href = url( sort=( column.key ) ) + extra = "↑" + else: + href = url( sort=column.key ) + %> + <th\ + %if column.ncells > 1: + colspan="${column.ncells}" + %endif + > + %if href: + <a href="${href}">${column.label}</a> + %else: + ${column.label} + %endif + <span>${extra}</span> + </th> + %endif + %endfor + <th></th> + </tr> + </thead> + <tbody> + %for i, item in enumerate( query ): + <tr \ + %if current_item == item: + class="current" \ + %endif + > + ## Item selection column + <td style="width: 1.5em;"> + <input type="checkbox" name="id" value=${trans.security.encode_id( item.id )} class="grid-row-select-checkbox" /> + </td> + ## Data columns + %for column in grid.columns: + %if column.visible: + <% + # Link + if column.link and column.link( item ): + href = url( **column.link( item ) ) + else: + href = None + # Value (coerced to list so we can loop) + value = column.get_value( trans, grid, item ) + if column.ncells == 1: + value = [ value ] + %> + %for cellnum, v in enumerate( value ): + <% + # Attach popup menu? + if column.attach_popup and cellnum == 0: + extra = '<a id="grid-%d-popup" class="popup-arrow" style="display: none;">▼</a>' % i + else: + extra = "" + %> + %if href: + <td><a href="${href}">${v}</a> ${extra}</td> + %else: + <td >${v}${extra}</td> + %endif + </td> + %endfor + %endif + %endfor + ## Actions column + <td> + <div popupmenu="grid-${i}-popup"> + %for operation in grid.operations: + %if operation.allowed( item ): + <a class="action-button" href="${url( operation=operation.label, id=trans.security.encode_id( item.id ) )}">${operation.label}</a> + %endif + %endfor + </div> + </td> + </tr> + %endfor + </tbody> + <tfoot> + <tr> + <td></td> + <td colspan="100"> + For <span class="grid-selected-count"></span> selected histories: + %for operation in grid.operations: + %if operation.allow_multiple: + <input type="submit" name="operation" value="${operation.label}" class="action-button"> + %endif + %endfor + </td> + </tr> + </tfoot> + </table> +</form> diff -r b26e2ef8726c -r 533ae45c4440 templates/history/list_as_xml.mako --- a/templates/history/list_as_xml.mako Fri Jun 26 09:22:31 2009 -0400 +++ b/templates/history/list_as_xml.mako Mon Jun 29 16:30:16 2009 -0400 @@ -1,7 +1,7 @@ <?xml version="1.0"?> <history_ids> %for i, history in enumerate( t.user.histories ): - <data id="${history.id}" hid="${i+1}" num="${len(history.datasets)}" name="${history.name}" create="${history.create_time}" update="${history.update_time}" > + <data id="${trans.security.encode_id( history.id )}" hid="${i+1}" num="${len(history.datasets)}" name="${history.name}" create="${history.create_time}" update="${history.update_time}" > </data> %endfor </history_ids> diff -r b26e2ef8726c -r 533ae45c4440 templates/history/list_shared.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/history/list_shared.mako Mon Jun 29 16:30:16 2009 -0400 @@ -0,0 +1,30 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +%if shared_by_others: + <table class="colored" border="0" cellspacing="0" cellpadding="0" width="100%"> + <tr class="header"> + <th>Name</th> + <th>Owner</th> + </tr> + %for i, association in enumerate( shared_by_others ): + <% history = association.history %> + <tr> + <td> + ${history.name} + <a id="shared-${i}-popup" class="popup-arrow" style="display: none;">▼</a> + <div popupmenu="shared-${i}-popup"> + <a class="action-button" href="${h.url_for( controller='history', action='clone', id=trans.security.encode_id( history.id ) )}">Clone</a> + </div> + </td> + <td>${history.user.email}</td> + </tr> + %endfor + </table> +%else: + No histories have been shared with you. +%endif diff -r b26e2ef8726c -r 533ae45c4440 templates/history/options.mako --- a/templates/history/options.mako Fri Jun 26 09:22:31 2009 -0400 +++ b/templates/history/options.mako Mon Jun 29 16:30:16 2009 -0400 @@ -11,16 +11,22 @@ %endif <ul> -%if user: - <li><a href="${h.url_for( controller='history', action='rename', id=history.id )}" target="galaxy_main">Rename</a> current history (stored as "${history.name}")</li> - <li><a href="${h.url_for( controller='history', action='list')}" target="galaxy_main">List</a> previously stored histories</li> - %if len( history.active_datasets ) > 0: - <li><a href="${h.url_for( controller='root', action='history_new' )}">Create</a> a new empty history</li> + %if user: + <li><a href="${h.url_for( controller='history', action='list')}" target="galaxy_main">List</a> previously stored histories</li> + %if len( history.active_datasets ) > 0: + <li><a href="${h.url_for( controller='root', action='history_new' )}">Create</a> a new empty history</li> + <li><a href="${h.url_for( controller='workflow', action='build_from_current_history' )}">Construct workflow</a> from current history</li> + <li><a href="${h.url_for( controller='history', action='clone', id=trans.security.encode_id( history.id ) )}">Clone</a> current history</li> + %endif + <li><a href="${h.url_for( controller='history', action='share' )}" target="galaxy_main">Share</a> current history</div> + <li><a href="${h.url_for( action='history_set_default_permissions' )}">Change default permissions</a> for current history</li> %endif - <li><a href="${h.url_for( controller='workflow', action='build_from_current_history' )}">Construct workflow</a> from the current history</li> - <li><a href="${h.url_for( controller='history', action='share' )}" target="galaxy_main">Share</a> current history</div> - <li><a href="${h.url_for( action='history_set_default_permissions' )}">Change default permissions</a> for the current history</li> -%endif - <li><a href="${h.url_for( controller='root', action='history', show_deleted=True)}" target="galaxy_history">Show deleted</a> datasets in history</li> + %if len( history.activatable_datasets ) > 0: + <li><a href="${h.url_for( controller='root', action='history', show_deleted=True)}" target="galaxy_history">Show deleted</a> datasets in current history</li> + %endif + <li><a href="${h.url_for( controller='history', action='rename', id=trans.security.encode_id( history.id ) )}" target="galaxy_main">Rename</a> current history (stored as "${history.name}")</li> <li><a href="${h.url_for( controller='history', action='delete_current' )}" confirm="Are you sure you want to delete the current history?">Delete</a> current history</div> + %if user and user.histories_shared_by_others: + <li><a href="${h.url_for( controller='history', action='list_shared')}" target="galaxy_main">List</a> histories shared with you by others</li> + %endif </ul> diff -r b26e2ef8726c -r 533ae45c4440 templates/history/permissions.mako --- a/templates/history/permissions.mako Fri Jun 26 09:22:31 2009 -0400 +++ b/templates/history/permissions.mako Mon Jun 29 16:30:16 2009 -0400 @@ -3,5 +3,6 @@ <%namespace file="/dataset/security_common.mako" import="render_permission_form" /> %if trans.user: - ${render_permission_form( trans.history, trans.history.name, h.url_for(), trans.user.all_roles() )} + <% history = trans.get_history() %> + ${render_permission_form( history, history.name, h.url_for(), trans.user.all_roles() )} %endif diff -r b26e2ef8726c -r 533ae45c4440 templates/history/rename.mako --- a/templates/history/rename.mako Fri Jun 26 09:22:31 2009 -0400 +++ b/templates/history/rename.mako Mon Jun 29 16:30:16 2009 -0400 @@ -11,7 +11,7 @@ <tr> <td> <div class="form-row"> - <input type="hidden" name="id" value="${history.id}"> + <input type="hidden" name="id" value="${trans.security.encode_id( history.id )}"> <label>${_('Current Name')}</label> <div style="float: left; width: 250px; margin-right: 10px;"> ${history.name} diff -r b26e2ef8726c -r 533ae45c4440 templates/history/share.mako --- a/templates/history/share.mako Fri Jun 26 09:22:31 2009 -0400 +++ b/templates/history/share.mako Mon Jun 29 16:30:16 2009 -0400 @@ -5,14 +5,16 @@ <div class="toolForm"> <div class="toolFormTitle">Share ${len( histories)} histories</div> <div class="toolFormBody"> - %if not can_change and not cannot_change: - <form action="${h.url_for( controller="history", action='share' )}" method="post" > + %if not can_change and not cannot_change and not no_change_needed: + ## We are sharing histories that contain only public datasets + <form name='share' id='share' action="${h.url_for( controller="history", action='share' )}" method="post" > %for history in histories: + <input type="hidden" name="id" value="${trans.security.encode_id( history.id )}"> <div class="toolForm"> <div class="form-row"> <label>${_('History Name:')}</label> <div style="float: left; width: 250px; margin-right: 10px;"> - ${history.name}<input type="hidden" name="id" value="${history.id}"> + ${history.name} </div> </div> <div style="clear: both"></div> @@ -27,15 +29,7 @@ </td> </div> </div> - ## TODO: this feature is not currently working - ##<div style="clear: both"></div> - ##<div class="form-row"> - ## <label>${_('Share Link')}</label> - ## <div style="float: left; width: 250px; margin-right: 10px;"> - ## <a href="${h.url_for( controller='history', action='imp', id=trans.security.encode_id(history.id) )}">${_('copy link to share')}</a> - ## </div> - ##</div> - ##<div style="clear: both"></div> + <div style="clear: both"></div> <p/> </div> %endfor @@ -58,15 +52,17 @@ %endif <div style="clear: both"></div> <div class="form-row"> - <input type="submit" name="history_share_btn" value="Submit"> + <input type="submit" name="share_button" value="Submit"> </div> </form> %else: - <form action="${h.url_for( controller='history', action='share' )}" method="post"> + ## We are sharing restricted histories + <form name='share_restricted' id=share_restricted' action="${h.url_for( controller='history', action='share_restricted' )}" method="post"> + ## Needed for rebuilding dicts + <input type="hidden" name="email" value="${email}" size="40"> %for history in histories: - <input type="hidden" name="id" value="${history.id}"> + <input type="hidden" name="id" value="${trans.security.encode_id( history.id )}"> %endfor - <input type="hidden" name="email" value="${email}"> %if no_change_needed: <div style="clear: both"></div> <div class="form-row"> @@ -74,7 +70,29 @@ The following datasets can be shared with ${email} with no changes </div> </div> - %for history, hdas in no_change_needed.items(): + ## no_change_needed looks like: + ## { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } + <% + # TODO: move generation of these unique dictionaries to the history controller + # Build the list of unique histories and datasets + unique_stuff = {} + %> + %for user, history_dict in no_change_needed.items(): + %for history, hdas in history_dict.items(): + <% + if history in unique_stuff: + for hda in hdas: + if hda not in unique_stuff[ history ]: + unique_stuff[ history ].append( hda ) + else: + unique_stuff[ history ] = [] + for hda in hdas: + if hda not in unique_stuff[ history ]: + unique_stuff[ history ].append( hda ) + %> + %endfor + %endfor + %for history, hdas in unique_stuff.items(): <div class="form-row"> <label>History</label> ${history.name} @@ -97,7 +115,29 @@ The following datasets can be shared with ${email} by updating their permissions </div> </div> - %for history, hdas in can_change.items(): + ## can_change looks like: + ## { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } + <% + # TODO: move generation of these unique dictionaries to the history controller + # Build the list of unique histories and datasets + unique_stuff = {} + %> + %for user, history_dict in can_change.items(): + %for history, hdas in history_dict.items(): + <% + if history in unique_stuff: + for hda in hdas: + if hda not in unique_stuff[ history ]: + unique_stuff[ history ].append( hda ) + else: + unique_stuff[ history ] = [] + for hda in hdas: + if hda not in unique_stuff[ history ]: + unique_stuff[ history ].append( hda ) + %> + %endfor + %endfor + %for history, hdas in unique_stuff.items(): <div class="form-row"> <label>History</label> ${history.name} @@ -121,7 +161,29 @@ change the permissions on them </div> </div> - %for history, hdas in cannot_change.items(): + ## cannot_change looks like: + ## { userA: {historyX : [hda, hda], historyY : [hda]}, userB: {historyY : [hda]} } + <% + # TODO: move generation of these unique dictionaries to the history controller + # Build the list of unique histories and datasets + unique_stuff = {} + %> + %for user, history_dict in can_change.items(): + %for history, hdas in history_dict.items(): + <% + if history in unique_stuff: + for hda in hdas: + if hda not in unique_stuff[ history ]: + unique_stuff[ history ].append( hda ) + else: + unique_stuff[ history ] = [] + for hda in hdas: + if hda not in unique_stuff[ history ]: + unique_stuff[ history ].append( hda ) + %> + %endfor + %endfor + %for history, hdas in unique_stuff.items(): <div class="form-row"> <label>History</label> ${history.name} @@ -154,19 +216,17 @@ %endif </div> %endif - %if no_change_needed: - <div class="form-row"> - <input type="radio" name="action" value="share"> Share anyway - %if can_change: - (don't change any permissions) - %endif - </div> - %endif + <div class="form-row"> + <input type="radio" name="action" value="share_anyway"> Share anyway + %if can_change: + (don't change any permissions) + %endif + </div> <div class="form-row"> <input type="radio" name="action" value="no_share"> Don't share </div> <div class="form-row"> - <input type="submit" name="share_proceed_button" value="Go"><br/> + <input type="submit" name="share_restricted_button" value="Go"><br/> </div> </form> %endif diff -r b26e2ef8726c -r 533ae45c4440 templates/history/sharing.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/history/sharing.mako Mon Jun 29 16:30:16 2009 -0400 @@ -0,0 +1,75 @@ +<%inherit file="/base.mako"/> +<%namespace file="/message.mako" import="render_msg" /> + +<h2>Public access via link</h2> + +%if msg: + ${render_msg( msg, messagetype )} +%endif + +%for history in histories: + <p> + %if history.importable: + Send the following URL to users as an easy way for them to import the history, making a copy of their own: + <% url = h.url_for( controller='history', action='imp', id=trans.security.encode_id(history.id), qualified=True ) %> + <blockquote> + <a href="${url}">${url}</a> + </blockquote> + <br/> + <form action="${h.url_for( controller='history', action='sharing', id=trans.security.encode_id( history.id ) )}" method="POST"> + <input class="action-button" type="submit" name="disable_import_via_link" value="Disable import via link"> + </form> + %else: + This history is currently restricted (only you and the users listed below + can access it). Enabling the following option will generate a URL that you + can give to a user to allow them to import this history. + <br/> + <form action="${h.url_for( action='sharing', id=trans.security.encode_id(history.id) )}" method="POST"> + <input class="action-button" type="submit" name="enable_import_via_link" value="Enable import via link"> + </form> + %endif + </p> + <h2>Sharing with specific users</h2> + %if history.users_shared_with: + <ul class="manage-table-actions"> + <li> + <a class="action-button" href="${h.url_for( controller='history', action='share', id=trans.security.encode_id( history.id ) )}"> + <span>Share with another user</span> + </a> + </li> + </ul> + <p> + The following users will see this history in their list of histories + shared with them by others, and they will be able to create their own copy of it: + </p> + <table class="colored" border="0" cellspacing="0" cellpadding="0" width="100%"> + <tr class="header"> + <th>History '${history.name}' currently shared with</th> + <th></th> + </tr> + %for i, association in enumerate( history.users_shared_with ): + <% user = association.user %> + <tr> + <td> + ${user.email} + <a id="user-${i}-popup" class="popup-arrow" style="display: none;">▼</a> + </td> + <td> + %if len( histories ) == 1: + ## Only allow unsharing if we're dealing with 1 history, otherwise + ## page refreshes screw things up + <div popupmenu="user-${i}-popup"> + <a class="action-button" href="${h.url_for( controller='history', action='sharing', id=trans.security.encode_id( history.id ), unshare_user=trans.security.encode_id( user.id ) )}">Unshare</a> + </div> + %endif + </td> + </tr> + %endfor + </table> + %else: + <p>You have not shared this history with any users.</p> + <a class="action-button" href="${h.url_for( controller='history', action='share', id=trans.security.encode_id(history.id) )}"> + <span>Share with another user</span> + </a> + %endif +%endfor diff -r b26e2ef8726c -r 533ae45c4440 test/base/twilltestcase.py --- a/test/base/twilltestcase.py Fri Jun 26 09:22:31 2009 -0400 +++ b/test/base/twilltestcase.py Mon Jun 29 16:30:16 2009 -0400 @@ -9,7 +9,8 @@ from twill.other_packages._mechanize_dist import ClientForm pkg_resources.require( "elementtree" ) from elementtree import ElementTree - +from galaxy.web import security + buffer = StringIO.StringIO() #Force twill to log to a buffer -- FIXME: Should this go to stdout and be captured by nose? @@ -23,13 +24,15 @@ class TwillTestCase( unittest.TestCase ): def setUp( self ): + # Security helper + self.security = security.SecurityHelper( id_secret='changethisinproductiontoo' ) self.history_id = os.environ.get( 'GALAXY_TEST_HISTORY_ID', None ) self.host = os.environ.get( 'GALAXY_TEST_HOST' ) self.port = os.environ.get( 'GALAXY_TEST_PORT' ) self.url = "http://%s:%s" % ( self.host, self.port ) self.file_dir = os.environ.get( 'GALAXY_TEST_FILE_DIR' ) self.home() - self.set_history() + #self.set_history() # Functions associated with files def files_diff( self, file1, file2 ): @@ -71,6 +74,7 @@ def upload_file( self, filename, ftype='auto', dbkey='unspecified (?)' ): """Uploads a file""" filename = self.get_filename(filename) + self.home() self.visit_page( "tool_runner/index?tool_id=upload1" ) try: tc.fv("1","file_type", ftype) @@ -79,9 +83,16 @@ tc.submit("runtool_btn") self.home() except AssertionError, err: - errmsg = 'The file doesn\'t exsit. Please check' % file + errmsg = "The file (%s) doesn't exist." % filename errmsg += str( err ) raise AssertionError( errmsg ) + # Make sure every history item has a valid hid + hids = self.get_hids_in_history() + for hid in hids: + try: + valid_hid = int( hid ) + except: + raise AssertionError, "Invalid hid (%s) created when uploading file %s" % ( hid, filename ) def upload_url_paste( self, url_paste, ftype='auto', dbkey='unspecified (?)' ): """Pasted data in the upload utility""" self.visit_page( "tool_runner/index?tool_id=upload1" ) @@ -94,6 +105,13 @@ except Exception, e: errmsg = "Problem executing upload utility using url_paste: %s" % str( e ) raise AssertionError( e ) + # Make sure every history item has a valid hid + hids = self.get_hids_in_history() + for hid in hids: + try: + valid_hid = int( hid ) + except: + raise AssertionError, "Invalid hid (%s) created when pasting %s" % ( hid, url_paste ) # Functions associated with histories def check_history_for_errors( self ): @@ -115,27 +133,30 @@ self.visit_page( "clear_history" ) self.check_history_for_string( 'Your history is empty' ) self.home() - def delete_history( self, id='' ): + def delete_history( self, id ): """Deletes one or more histories""" - history_list = self.get_histories() + history_list = self.get_histories_as_data_list() self.assertTrue( history_list ) - num_deleted = 1 - if not id: - history = history_list[0] - id = history.get( 'id' ) - else: - num_deleted = len( id.split( ',' ) ) + num_deleted = len( id.split( ',' ) ) + self.home() self.visit_page( "history/list?operation=delete&id=%s" % ( id ) ) check_str = 'Deleted %d histories' % num_deleted self.check_page_for_string( check_str ) self.home() - def get_histories( self ): - """Returns all histories""" + def delete_current_history( self, check_str='' ): + """Deletes the current history""" + self.home() + self.visit_page( "history/delete_current" ) + if check_str: + self.check_page_for_string( check_str ) + self.home() + def get_histories_as_data_list( self ): + """Returns the data elements of all histories""" tree = self.histories_as_xml_tree() data_list = [ elem for elem in tree.findall("data") ] return data_list - def get_history( self, show_deleted=False ): - """Returns a history""" + def get_history_as_data_list( self, show_deleted=False ): + """Returns the data elements of a history""" tree = self.history_as_xml_tree( show_deleted=show_deleted ) data_list = [ elem for elem in tree.findall("data") ] return data_list @@ -153,31 +174,24 @@ xml = self.last_page() tree = ElementTree.fromstring(xml) return tree - def history_options( self, check_str='', upload=False ): + def history_options( self, user=False, active_datasets=False, activatable_datasets=False, histories_shared_by_others=False ): """Mimics user clicking on history options link""" - self.visit_page( "history_options" ) - if check_str: - self.check_page_for_string( check_str ) - else: - self.check_page_for_string( 'Rename</a> current history' ) + self.home() + self.visit_page( "root/history_options" ) + if user: self.check_page_for_string( 'List</a> previously stored histories' ) - self.check_page_for_string( 'Construct workflow</a> from the current history' ) + if active_datasets: + self.check_page_for_string( 'Create</a> a new empty history' ) + self.check_page_for_string( 'Construct workflow</a> from current history' ) + self.check_page_for_string( 'Clone</a> current history' ) self.check_page_for_string( 'Share</a> current history' ) - # Tests for changing default history permissions are done in test_security_and_libraries.py - self.check_page_for_string( 'Change default permissions</a> for the current history' ) - self.check_page_for_string( 'Show deleted</a> datasets in history' ) - self.check_page_for_string( 'Delete</a> current history' ) - # Need to add a history item in order to create a new empty history - try: - self.check_page_for_string( 'Create</a> a new empty history' ) - raise AssertionError, "Incorrectly able to create a new empty history when the current history is empty." - except: - pass - if upload: - self.upload_file( '1.bed', dbkey='hg18' ) - self.home() - self.visit_page( "history_options" ) - self.check_page_for_string( 'Create</a> a new empty history' ) + self.check_page_for_string( 'Change default permissions</a> for current history' ) + if histories_shared_by_others: + self.check_page_for_string( 'List</a> histories shared with you by others' ) + if activatable_datasets: + self.check_page_for_string( 'Show deleted</a> datasets in current history' ) + self.check_page_for_string( 'Rename</a> current history' ) + self.check_page_for_string( 'Delete</a> current history' ) self.home() def new_history( self, name=None ): """Creates a new, empty history""" @@ -198,26 +212,63 @@ def set_history( self ): """Sets the history (stores the cookies for this run)""" if self.history_id: + self.home() self.visit_page( "history?id=%s" % self.history_id ) else: self.new_history() self.home() - def share_history( self, id, email, check_str, check_str2='', action=None, action_check_str=None ): - """Share a history different users""" - self.visit_url( "%s/history/share?id=%s&email=%s&history_share_btn=Submit" % ( self.url, id, email ) ) - self.check_page_for_string( check_str ) + def share_current_history( self, email, check_str='', check_str_after_submit='', check_str_after_submit2='', + action='', action_check_str='', action_check_str_after_submit='' ): + """Share the current history with different users""" + self.visit_url( "%s/history/share" % self.url ) + if check_str: + self.check_page_for_string( check_str ) + tc.fv( 'share', 'email', email ) + tc.submit( 'share_button' ) + if check_str_after_submit: + self.check_page_for_string( check_str_after_submit ) + if check_str_after_submit2: + self.check_page_for_string( check_str_after_submit2 ) + if action: + # If we have an action, then we are sharing datasets with users that do not have access permissions on them + if action_check_str: + self.check_page_for_string( action_check_str ) + tc.fv( 'share_restricted', 'action', action ) + tc.submit( "share_restricted_button" ) + if action_check_str_after_submit: + self.check_page_for_string( action_check_str_after_submit ) + self.home() + def share_histories_with_users( self, ids, emails, check_str1='', check_str2='', + check_str_after_submit='', action=None, action_check_str=None ): + """Share one or more histories with one or more different users""" + self.visit_url( "%s/history/list?id=%s&operation=Share" % ( self.url, ids ) ) + if check_str1: + self.check_page_for_string( check_str1 ) if check_str2: self.check_page_for_string( check_str2 ) + tc.fv( 'share', 'email', emails ) + tc.submit( 'share_button' ) + if check_str_after_submit: + self.check_page_for_string( check_str_after_submit ) if action: # If we have an action, then we are sharing datasets with users that do not have access permissions on them - tc.fv( '1', 'action', action ) - tc.submit( "share_proceed_button" ) + tc.fv( 'share_restricted', 'action', action ) + tc.submit( "share_restricted_button" ) if action_check_str: self.check_page_for_string( action_check_str ) self.home() + def unshare_history( self, history_id, user_id, check_str1='', check_str2='', check_str_after_submit='' ): + """Unshare a history that has been shared with another user""" + self.visit_url( "%s/history/list?id=%s&operation=sharing" % ( self.url, history_id ) ) + if check_str1: + self.check_page_for_string( check_str1 ) + if check_str2: + self.check_page_for_string( check_str2 ) + self.visit_url( "%s/history/sharing?unshare_user=%s&id=%s" % ( self.url, user_id, history_id ) ) + self.home() def switch_history( self, id='', name='' ): """Switches to a history in the current list of histories""" - data_list = self.get_histories() + data_list = self.get_histories_as_data_list() self.assertTrue( data_list ) if not id: history = history_list[0] @@ -227,6 +278,7 @@ self.check_history_for_string( name ) self.home() def view_stored_active_histories( self, check_str='' ): + self.home() self.visit_page( "history/list" ) self.check_page_for_string( 'Stored histories' ) self.check_page_for_string( '<input type="checkbox" name="id" value=' ) @@ -237,12 +289,57 @@ self.check_page_for_string( check_str ) self.home() def view_stored_deleted_histories( self, check_str='' ): + self.home() self.visit_page( "history/list?f-deleted=True" ) self.check_page_for_string( 'Stored histories' ) self.check_page_for_string( '<input type="checkbox" name="id" value=' ) self.check_page_for_string( 'operation=Undelete&id' ) if check_str: self.check_page_for_string( check_str ) + self.home() + def view_shared_histories( self, check_str='', check_str2='' ): + self.home() + self.visit_page( "history/list_shared" ) + if check_str: + self.check_page_for_string( check_str ) + if check_str2: + self.check_page_for_string( check_str2 ) + self.home() + def clone_history( self, history_id, check_str1='' ): + self.home() + self.visit_page( "history/clone?id=%s" % history_id ) + if check_str1: + self.check_page_for_string( check_str1 ) + self.home() + def enable_import_via_link( self, history_id, check_str='', check_str_after_submit='' ): + self.home() + self.visit_page( "history/list?operation=sharing&id=%s" % history_id ) + if check_str: + self.check_page_for_string( check_str ) + # twill barfs on this form, possibly because it contains no fields, but not sure. + # In any case, we have to mimic the form submission + self.home() + self.visit_page( 'history/sharing?id=%s&enable_import_via_link=True' % history_id ) + if check_str_after_submit: + self.check_page_for_string( check_str_after_submit ) + self.home() + def disable_import_via_link( self, history_id, check_str='', check_str_after_submit='' ): + self.home() + self.visit_page( "history/list?operation=sharing&id=%s" % history_id ) + if check_str: + self.check_page_for_string( check_str ) + # twill barfs on this form, possibly because it contains no fields, but not sure. + # In any case, we have to mimic the form submission + self.home() + self.visit_page( 'history/sharing?id=%s&disable_import_via_link=True' % history_id ) + if check_str_after_submit: + self.check_page_for_string( check_str_after_submit ) + self.home() + def import_history_via_url( self, history_id, email, check_str_after_submit='' ): + self.home() + self.visit_page( "history/imp?&id=%s" % history_id ) + if check_str_after_submit: + self.check_page_for_string( check_str_after_submit ) self.home() # Functions associated with datasets (history items) and meta data @@ -260,7 +357,7 @@ def check_metadata_for_string( self, patt, hid=None ): """Looks for 'patt' in the edit page when editing a dataset""" - data_list = self.get_history() + data_list = self.get_history_as_data_list() self.assertTrue( data_list ) if hid is None: # take last hid elem = data_list[-1] @@ -276,7 +373,7 @@ except: raise AssertionError, "Invalid hid '%s' - must be int" % hid hid = str(hid) - data_list = self.get_history() + data_list = self.get_history_as_data_list() self.assertTrue( data_list ) elems = [ elem for elem in data_list if elem.get( 'hid' ) == hid ] self.assertEqual( len( elems ), 1 ) @@ -291,7 +388,7 @@ except: raise AssertionError, "Invalid hid '%s' - must be int" % hid hid = str( hid ) - data_list = self.get_history( show_deleted=show_deleted ) + data_list = self.get_history_as_data_list( show_deleted=show_deleted ) self.assertTrue( data_list ) elems = [ elem for elem in data_list if elem.get( 'hid' ) == hid ] self.assertEqual( len( elems ), 1 ) @@ -348,7 +445,8 @@ tc.submit( 'change' ) self.check_page_for_string( 'Edit Attributes' ) self.home() - def copy_history_item( self, source_dataset_ids='', target_history_ids=[], all_target_history_ids=[], deleted_history_ids=[] ): + def copy_history_item( self, source_dataset_ids='', target_history_ids=[], all_target_history_ids=[], + deleted_history_ids=[] ): """Copy 1 or more history_dataset_associations to 1 or more histories""" self.home() self.visit_url( "%s/dataset/copy_datasets?source_dataset_ids=%s" % ( self.url, source_dataset_ids ) ) @@ -371,31 +469,28 @@ check_str = '%d datasets copied to %d histories.' % ( no_source_ids, len( target_history_ids ) ) self.check_page_for_string( check_str ) self.home() - def get_dataset_ids_in_history( self ): - """Returns the ids of datasets in a history""" - data_list = self.get_history() + def get_hids_in_history( self ): + """Returns the list of hid values for items in a history""" + data_list = self.get_history_as_data_list() hids = [] for elem in data_list: hid = elem.get('hid') hids.append(hid) return hids - - def get_dataset_ids_in_histories( self ): - """Returns the ids of datasets in all histories""" - data_list = self.get_histories() + def get_hids_in_histories( self ): + """Returns the list of hids values for items in all histories""" + data_list = self.get_histories_as_data_list() hids = [] for elem in data_list: hid = elem.get('hid') hids.append(hid) return hids - def verify_dataset_correctness( self, filename, hid=None, wait=True ): """Verifies that the attributes and contents of a history item meet expectations""" - if wait: self.wait() #wait for job to finish - - data_list = self.get_history() + if wait: + self.wait() #wait for job to finish + data_list = self.get_history_as_data_list() self.assertTrue( data_list ) - if hid is None: # take last hid elem = data_list[-1] hid = str( elem.get('hid') ) @@ -404,10 +499,8 @@ elems = [ elem for elem in data_list if elem.get('hid') == hid ] self.assertTrue( len(elems) == 1 ) elem = elems[0] - self.assertTrue( hid ) self._assert_dataset_state( elem, 'ok' ) - if self.is_zipped( filename ): errmsg = 'History item %s is a zip archive which includes invalid files:\n' % hid zip_file = zipfile.ZipFile( filename, "r" ) @@ -422,6 +515,7 @@ else: local_name = self.get_filename( filename ) temp_name = self.get_filename( 'temp_%s' % filename ) + self.home() self.visit_page( "display?hid=" + hid ) data = self.last_page() file( temp_name, 'wb' ).write(data) @@ -455,7 +549,7 @@ def verify_genome_build( self, dbkey='hg17' ): """Verifies that the last used genome_build at history id 'hid' is as expected""" - data_list = self.get_history() + data_list = self.get_history_as_data_list() self.assertTrue( data_list ) elems = [ elem for elem in data_list ] elem = elems[-1] diff -r b26e2ef8726c -r 533ae45c4440 test/functional/__init__.py --- a/test/functional/__init__.py Fri Jun 26 09:22:31 2009 -0400 +++ b/test/functional/__init__.py Mon Jun 29 16:30:16 2009 -0400 @@ -24,7 +24,7 @@ # server (for running the tests against a running instance) default_galaxy_test_host = "localhost" -default_galaxy_test_port = "9999" +default_galaxy_test_port = "8777" default_galaxy_locales = 'en' galaxy_test_file_dir = "test-data" server = None @@ -63,10 +63,10 @@ default_cluster_job_runner = os.environ['GALAXY_TEST_DEF_RUNNER'] else: default_cluster_job_runner = 'local:///' - app = UniverseApplication( job_queue_workers = 5, start_job_runners = start_job_runners, default_cluster_job_runner = default_cluster_job_runner, + id_secret = 'changethisinproductiontoo', template_path = "templates", database_connection = database_connection, file_path = file_path, diff -r b26e2ef8726c -r 533ae45c4440 test/functional/test_DNAse_flanked_genes.py --- a/test/functional/test_DNAse_flanked_genes.py Fri Jun 26 09:22:31 2009 -0400 +++ b/test/functional/test_DNAse_flanked_genes.py Mon Jun 29 16:30:16 2009 -0400 @@ -6,10 +6,12 @@ class AnalysisDNAseHSSFlankedGenes( TwillTestCase ): def test_get_DNAseHSS_flanked_genes( self ): - self.login() - self.new_history() - global history1 - history1 = galaxy.model.History.query() \ + self.logout() + self.login( email='test@bx.psu.edu' ) + admin_user = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test@bx.psu.edu' ).one() + self.new_history( name='DNAseHSS_flanked_genes' ) + history1 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() track_params = dict( db="hg17", @@ -85,5 +87,5 @@ self.run_tool( 'Filter1', input="4", cond="c17==1000" ) self.wait() self.verify_dataset_correctness( 'filteredJoinedFlanksDNAse.dat' ) - self.delete_history() + self.delete_history( self.security.encode_id( history1.id ) ) self.logout() diff -r b26e2ef8726c -r 533ae45c4440 test/functional/test_get_data.py --- a/test/functional/test_get_data.py Fri Jun 26 09:22:31 2009 -0400 +++ b/test/functional/test_get_data.py Mon Jun 29 16:30:16 2009 -0400 @@ -6,8 +6,12 @@ def test_000_upload_files_from_disk( self ): """Test uploading data files from disk""" self.logout() - self.login( email='tst@bx.psu.edu' ) - history1 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + self.login( email='test@bx.psu.edu' ) + global admin_user + admin_user = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test@bx.psu.edu' ).one() + history1 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() self.upload_file( '1.bed' ) hda1 = galaxy.model.HistoryDatasetAssociation.query() \ .order_by( desc( galaxy.model.HistoryDatasetAssociation.table.c.create_time ) ).first() @@ -38,24 +42,28 @@ .order_by( desc( galaxy.model.HistoryDatasetAssociation.table.c.create_time ) ).first() assert hda6 is not None, "Problem retrieving hda6 from database" self.verify_dataset_correctness( '1.scf.zip', hid=str( hda6.hid ) ) - self.delete_history( id=str( history1.id ) ) + self.delete_history( id=self.security.encode_id( history1.id ) ) def test_005_url_paste( self ): """Test url paste behavior""" # Deleting the current history should have created a new history self.check_history_for_string( 'Your history is empty' ) - history2 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history2 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() self.upload_url_paste( 'hello world' ) self.check_history_for_string( 'Pasted Entry' ) self.check_history_for_string( 'hello world' ) self.upload_url_paste( u'hello world' ) self.check_history_for_string( 'Pasted Entry' ) self.check_history_for_string( 'hello world' ) - self.delete_history( id=str( history2.id ) ) + self.delete_history( id=self.security.encode_id( history2.id ) ) def test_010_upload_encode_data( self ): """Test uploading encode data""" # Deleting the current history should have created a new history self.check_history_for_string( 'Your history is empty' ) - history3 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history3 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() self.run_tool( 'encode_import_chromatin_and_chromosomes1', hg17=['cc.EarlyRepSeg.20051216.bed'] ) self.wait() hda7 = galaxy.model.HistoryDatasetAssociation.query() \ @@ -68,7 +76,4 @@ .order_by( desc( galaxy.model.HistoryDatasetAssociation.table.c.create_time ) ).first() assert hda8 is not None, "Problem retrieving hda8 from database" self.verify_dataset_correctness( 'sc_3D_cds.bed', hid=str( hda8.hid ) ) - self.delete_history( id=str( history3.id ) ) - def test_015_reset_data_for_later_test_runs( self ): - """Reseting data to enable later test runs to pass""" - self.logout() + self.delete_history( id=self.security.encode_id( history3.id ) ) diff -r b26e2ef8726c -r 533ae45c4440 test/functional/test_history_functions.py --- a/test/functional/test_history_functions.py Fri Jun 26 09:22:31 2009 -0400 +++ b/test/functional/test_history_functions.py Mon Jun 29 16:30:16 2009 -0400 @@ -5,12 +5,11 @@ class TestHistory( TwillTestCase ): - def test_000_history_options_when_not_logged_in( self ): - """Testing history options when not logged in""" + def test_000_history_behavior_between_logout_login( self ): + """Testing history behavior between logout and login""" self.logout() - check_str = 'logged in</a> to store or switch histories.' - self.history_options( check_str=check_str ) - # Make sure we have created the following accounts + self.history_options() + # Make sure we have created the following 4 accounts self.login( email='test1@bx.psu.edu' ) global regular_user1 regular_user1 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test1@bx.psu.edu' ).first() @@ -25,14 +24,12 @@ global regular_user3 regular_user3 = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test3@bx.psu.edu' ).first() assert regular_user3 is not None, 'Problem retrieving user with email "test3@bx.psu.edu" from the database' - def test_005_deleting_histories( self ): - """Testing deleting histories""" self.logout() self.login( email='test@bx.psu.edu' ) global admin_user - admin_user = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test@bx.psu.edu' ).first() + admin_user = galaxy.model.User.filter( galaxy.model.User.table.c.email=='test@bx.psu.edu' ).one() assert admin_user is not None, 'Problem retrieving user with email "test@bx.psu.edu" from the database' - # Get the admin_user private role + # Get the admin_user private role for later use global admin_user_private_role admin_user_private_role = None for role in admin_user.all_roles(): @@ -41,27 +38,48 @@ break if not admin_user_private_role: raise AssertionError( "Private role not found for user '%s'" % admin_user.email ) - latest_history = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() - assert latest_history is not None, "Problem retrieving latest history from database" - assert not latest_history.deleted, "After login, associated history is deleted" - self.delete_history( str( latest_history.id ) ) - latest_history.refresh() - if not latest_history.deleted: - raise AssertionError, "Problem deleting history id %d" % latest_history.id + historyA = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + assert historyA is not None, "Problem retrieving historyA from database" + assert not historyA.deleted, "After login, historyA is deleted" + # Make sure the last used history is set for the next session after login + self.logout() + self.login( email=admin_user.email ) + historyB = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + assert historyB is not None, "Problem retrieving historyB from database" + assert historyA.id == historyB.id, "After the same user logged out and back in, their last used history was not associated with their new session" + def test_005_deleting_histories( self ): + """Testing deleting histories""" + # Logged in as admin_user + historyB = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + assert historyB is not None, "Problem retrieving historyB from database" + self.delete_history( self.security.encode_id( historyB.id ) ) + historyB.refresh() + if not historyB.deleted: + raise AssertionError, "Problem deleting history id %d" % historyB.id # Since we deleted the current history, make sure the history frame was refreshed self.check_history_for_string( 'Your history is empty.' ) # We'll now test deleting a list of histories # After deleting the current history, a new one should have been created global history1 - history1 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history1 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() assert history1 is not None, "Problem retrieving history1 from database" self.upload_file( '1.bed', dbkey='hg18' ) self.new_history( name=urllib.quote( 'history2' ) ) global history2 - history2 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history2 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() assert history2 is not None, "Problem retrieving history2 from database" self.upload_file( '2.bed', dbkey='hg18' ) - ids = '%s,%s' % ( str( history1.id ), str( history2.id ) ) + ids = '%s,%s' % ( self.security.encode_id( history1.id ), self.security.encode_id( history2.id ) ) self.delete_history( ids ) # Since we deleted the current history, make sure the history frame was refreshed self.check_history_for_string( 'Your history is empty.' ) @@ -87,116 +105,153 @@ raise AssertionError, "Problem deleting history id %d" % history2.id if not history2.default_permissions: raise AssertionError, "Default permissions were incorrectly deleted from the db for history id %d when it was deleted" % history2.id - def test_010_history_options_when_logged_in( self ): - """Testing history options when logged in""" - self.history_options() - def test_015_history_rename( self ): + # Current history is empty + self.history_options( user=True ) + def test_010_history_rename( self ): """Testing renaming a history""" + # Logged in as admin_user global history3 - history3 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history3 = galaxy.model.History \ + .filter( galaxy.model.History.table.c.deleted==False ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ) \ + .first() assert history3 is not None, "Problem retrieving history3 from database" if history3.deleted: raise AssertionError, "History id %d deleted when it should not be" % latest_history.id - self.rename_history( str( history3.id ), history3.name, new_name=urllib.quote( 'history 3' ) ) - def test_020_history_list( self ): - """Testing viewing previously stored histories""" + self.rename_history( self.security.encode_id( history3.id ), history3.name, new_name=urllib.quote( 'history 3' ) ) + history3.refresh() + def test_015_history_list( self ): + """Testing viewing previously stored active histories""" + # Logged in as admin_user self.view_stored_active_histories() - def test_025_history_share( self ): - """Testing sharing histories containing only public datasets""" + def test_020_share_current_history( self ): + """Testing sharing the current history which contains only public datasets""" + # Logged in as admin_user + # Test sharing an empty history - current history is history3 + self.share_current_history( regular_user1.email, + check_str=history3.name, + check_str_after_submit='You cannot share an empty history.' ) + # Make history3 sharable by adding a dataset + self.upload_file( '1.bed', dbkey='hg18' ) + # Current history is no longer empty + self.history_options( user=True, active_datasets=True, activatable_datasets=True ) + # Test sharing history3 with yourself + self.share_current_history( admin_user.email, + check_str=history3.name, + check_str_after_submit='You cannot send histories to yourself.' ) + # Share history3 with 1 valid user + self.share_current_history( regular_user1.email, + check_str=history3.name, + check_str_after_submit='History (%s) now shared with: 1 users' % history3.name ) + # Check out list of histories to make sure history3 was shared + self.view_stored_active_histories( check_str='operation=sharing&id=%s">shared' % self.security.encode_id( history3.id ) ) + # Enable importing history3 via a URL + self.enable_import_via_link( self.security.encode_id( history3.id ), + check_str='Unshare', + check_str_after_submit='Send the following URL to users' ) + # Make sure history3 is now import-able history3.refresh() - self.upload_file( '1.bed', dbkey='hg18' ) - # Test sharing a history with yourself - check_str = "You can't send histories to yourself." - self.share_history( str( history3.id ), 'test@bx.psu.edu', check_str ) - # Share a history with 1 valid user - check_str = 'Histories (%s) have been shared with: %s' % ( history3.name, regular_user1.email ) - self.share_history( str( history3.id ), regular_user1.email, check_str ) - # We need to keep track of all shared histories so they can later be deleted - global history3_copy1 - history3_copy1 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() - assert history3_copy1 is not None, "Problem retrieving history3_copy1 from database" + if not history3.importable: + raise AssertionError, "History 3 is not marked as importable after enable_import_via_link" + # Try importing history3 + self.import_history_via_url( self.security.encode_id( history3.id ), + admin_user.email, + check_str_after_submit='You cannot import your own history.' ) + # Disable the import link for history3 + self.disable_import_via_link( self.security.encode_id( history3.id ), + check_str='Send the following URL to users', + check_str_after_submit='Enable import via link' ) + # Try importing history3 after disabling the URL + self.import_history_via_url( self.security.encode_id( history3.id ), + admin_user.email, + check_str_after_submit='The owner of this history has disabled imports via this link.' ) + # Test sharing history3 with an invalid user + self.share_current_history( 'jack@jill.com', + check_str_after_submit='jack@jill.com is not a valid Galaxy user.' ) + + + + + + def test_025_delete_shared_current_history( self ): + """Testing deleting the current history after it was shared""" + # Logged in as admin_user + self.delete_current_history( check_str="History (%s) has been shared with others, unshare it before deleting it." % history3.name ) + def test_030_clone_shared_history( self ): + """Testing cloning a shared history""" + # logged in as admin user self.logout() self.login( email=regular_user1.email ) - check_str = '%s from %s' % ( history3.name, admin_user.email ) + # Shared history3 affects history options + self.history_options( user=True, histories_shared_by_others=True ) + # Shared history3 should be in regular_user1's list of shared histories + self.view_shared_histories( check_str=history3.name, check_str2=admin_user.email ) + self.clone_history( self.security.encode_id( history3.id ), + check_str1='is now included in your list of stored histories.' ) + global history3_clone1 + history3_clone1 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==regular_user1.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + assert history3_clone1 is not None, "Problem retrieving history3_clone1 from database" + # Check list of histories to make sure shared history3 was cloned + check_str = "Clone of '%s' shared by '%s'" % ( history3.name, admin_user.email ) self.view_stored_active_histories( check_str=check_str ) - # Need to delete history3_copy1 - self.delete_history( id=str( history3_copy1.id ) ) + def test_035_clone_current_history( self ): + """Testing cloning the current history""" + # logged in as regular_user1 self.logout() self.login( email=admin_user.email ) - # Test sharing a history with an invalid user - email = 'jack@jill.com' - check_str = '%s is not a valid Galaxy user.' % email - self.share_history( str( history3.id ), email, check_str ) - # Test sharing multiple histories with multiple users + self.clone_history( self.security.encode_id( history3.id ), + check_str1='is now included in your list of stored histories.' ) + global history3_clone2 + history3_clone2 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + assert history3_clone2 is not None, "Problem retrieving history3_clone2 from database" + # Check list of histories to make sure shared history3 was cloned + self.view_stored_active_histories( check_str="Clone of '%s'" % history3.name ) + def test_040_sharing_mulitple_histories_with_multiple_users( self ): + """Testing sharing multiple histories containing only public datasets with multiple users""" + # Logged in as admin_user self.new_history() global history4 - history4 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history4 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() assert history4 is not None, "Problem retrieving history4 from database" - self.rename_history( str( history4.id ), history4.name, new_name=urllib.quote( 'history 4' ) ) + self.rename_history( self.security.encode_id( history4.id ), history4.name, new_name=urllib.quote( 'history 4' ) ) history4.refresh() self.upload_file( '2.bed', dbkey='hg18' ) - id = '%s,%s' % ( str( history3.id ), str( history4.id ) ) - name = '%s,%s' % ( history3.name, history4.name ) - email = '%s,%s' % ( regular_user2.email, regular_user3.email ) - check_str = 'Histories (%s) have been shared with: %s' % ( name, email ) - self.share_history( id, email, check_str ) - # We need to keep track of all shared histories so they can later be deleted - history3_copy_name = "%s from %s" % ( history3.name, admin_user.email ) - history3_to_use_for_regular_user2 = galaxy.model.History \ - .filter( and_( galaxy.model.History.table.c.name==history3_copy_name, - galaxy.model.History.table.c.user_id==regular_user2.id, - galaxy.model.History.table.c.deleted==False ) ) \ - .order_by( desc( galaxy.model.History.table.c.create_time ) ) \ - .first() - assert history3_to_use_for_regular_user2 is not None, "Problem retrieving history3_to_use_for_regular_user2 from database" - history3_to_use_for_regular_user3 = galaxy.model.History \ - .filter( and_( galaxy.model.History.table.c.name==history3_copy_name, - galaxy.model.History.table.c.user_id==regular_user3.id, - galaxy.model.History.table.c.deleted==False ) ) \ - .order_by( desc( galaxy.model.History.table.c.create_time ) ) \ - .first() - assert history3_to_use_for_regular_user3 is not None, "Problem retrieving history3_to_use_for_regular_user3 from database" - history4_copy_name = "%s from %s" % ( history4.name, admin_user.email ) - history4_to_use_for_regular_user2 = galaxy.model.History \ - .filter( and_( galaxy.model.History.table.c.name==history4_copy_name, - galaxy.model.History.table.c.user_id==regular_user2.id, - galaxy.model.History.table.c.deleted==False ) ) \ - .order_by( desc( galaxy.model.History.table.c.create_time ) ) \ - .first() - assert history4_to_use_for_regular_user2 is not None, "Problem retrieving history4_to_use_for_regular_user2 from database" - history4_to_use_for_regular_user3 = galaxy.model.History \ - .filter( and_( galaxy.model.History.table.c.name==history4_copy_name, - galaxy.model.History.table.c.user_id==regular_user3.id, - galaxy.model.History.table.c.deleted==False ) ) \ - .order_by( desc( galaxy.model.History.table.c.create_time ) ) \ - .first() - assert history4_to_use_for_regular_user3 is not None, "Problem retrieving history4_to_use_for_regular_user3 from database" + ids = '%s,%s' % ( self.security.encode_id( history3.id ), self.security.encode_id( history4.id ) ) + emails = '%s,%s' % ( regular_user2.email, regular_user3.email ) + check_str_after_submit = 'History (%s) now shared with: 3 users.' % history3.name + self.share_histories_with_users( ids, + emails, + check_str1='Share 2 histories', + check_str2=history4.name, + check_str_after_submit=check_str_after_submit ) self.logout() self.login( email=regular_user2.email ) - check_str = '%s from %s' % ( history3.name, admin_user.email ) - self.view_stored_active_histories( check_str=check_str ) - check_str = '%s from %s' % ( history4.name, admin_user.email ) - self.view_stored_active_histories( check_str=check_str ) - # Need to delete the copied histories, so later test runs are valid - self.delete_history( id=str( history3_to_use_for_regular_user2.id ) ) - self.delete_history( id=str( history4_to_use_for_regular_user2.id ) ) + # Shared history3 should be in regular_user2's list of shared histories + self.view_shared_histories( check_str=history3.name, check_str2=admin_user.email ) self.logout() self.login( email=regular_user3.email ) - check_str = '%s from %s' % ( history3.name, admin_user.email ) - self.view_stored_active_histories( check_str=check_str ) - check_str = '%s from %s' % ( history4.name, admin_user.email ) - self.view_stored_active_histories( check_str=check_str ) - # Need to delete the copied histories, so later test runs are valid - self.delete_history( id=str( history3_to_use_for_regular_user3.id ) ) - self.delete_history( id=str( history4_to_use_for_regular_user3.id ) ) + # Shared history3 should be in regular_user3's list of shared histories + self.view_shared_histories( check_str=history3.name, check_str2=admin_user.email ) + def test_045_change_permissions_on_current_history( self ): + """Testing changing permissions on the current history""" + # Logged in as regular_user3 self.logout() self.login( email=admin_user.email ) - def test_030_change_permissions_on_current_history( self ): - """Testing changing permissions on the current history""" + # Current history is history4 + self.new_history() global history5 - history5 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + history5 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==admin_user.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() assert history5 is not None, "Problem retrieving history5 from database" - self.rename_history( str( history5.id ), history5.name, new_name=urllib.quote( 'history5' ) ) + self.rename_history( self.security.encode_id( history5.id ), history5.name, new_name=urllib.quote( 'history 5' ) ) + # Current history is hostory5 history5.refresh() # Due to the limitations of twill ( not functional with the permissions forms ), we're forced # to do this manually. At this point, we just want to restrict the access permission on history5 @@ -205,81 +260,126 @@ access_action = galaxy.model.Dataset.permitted_actions.DATASET_ACCESS.action dhp = galaxy.model.DefaultHistoryPermissions( history5, access_action, admin_user_private_role ) dhp.flush() + history5.refresh() + global history5_default_permissions + history5_default_permissions = [ dhp.action for dhp in history5.default_permissions ] + # Sort for later comparison + history5_default_permissions.sort() self.upload_file( '1.bed', dbkey='hg18' ) history5_dataset1 = None for hda in history5.datasets: if hda.name == '1.bed': history5_dataset1 = hda.dataset + break assert history5_dataset1 is not None, "Problem retrieving history5_dataset1 from the database" # The permissions on the dataset should be restricted from sharing with anyone due to the # inherited history permissions - restricted = False - for action in history5_dataset1.actions: - if action.action == access_action: - restricted = True - break - if not restricted: - raise AssertionError, "The 'access' permission is not set for history5_dataset1.actions" - def test_035_sharing_history_by_making_datasets_public( self ): + dataset_permissions = [ a.action for a in history5_dataset1.actions ] + dataset_permissions.sort() + if dataset_permissions != history5_default_permissions: + err_msg = "Dataset permissions for history5_dataset1 (%s) were not correctly inherited from history permissions (%s)" \ + % ( str( dataset_permissions ), str( history5_default_permissions ) ) + raise AssertionError, err_msg + # Make sure when we logout and login, the history default permissions are preserved + self.logout() + self.login( email=admin_user.email ) + history5.refresh() + current_history_permissions = [ dhp.action for dhp in history5.default_permissions ] + current_history_permissions.sort() + if current_history_permissions != history5_default_permissions: + raise AssertionError, "With logout and login, the history default permissions are not preserved" + def test_050_sharing_restricted_history_by_making_datasets_public( self ): """Testing sharing a restricted history by making the datasets public""" - # We're still logged in as admin_user.email - check_str = 'The following datasets can be shared with %s by updating their permissions' % regular_user1.email - action_check_str = 'Histories (%s) have been shared with: %s' % ( history5.name, regular_user1.email ) - self.share_history( str( history5.id ), regular_user1.email, check_str, action='public', action_check_str=action_check_str ) - history5_copy1 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() - assert history5_copy1 is not None, "Problem retrieving history5_copy1 from database" + # Logged in as admin_user + action_check_str = 'The following datasets can be shared with %s by updating their permissions' % regular_user1.email + action_check_str_after_submit = 'History (%s) now shared with: 1 users.' % history5.name + # Current history is history5 + self.share_current_history( regular_user1.email, + action='public', + action_check_str=action_check_str, + action_check_str_after_submit=action_check_str_after_submit ) self.logout() self.login( email=regular_user1.email ) - self.visit_url( "%s/history/list" % self.url ) - self.check_page_for_string( history5_copy1.name ) - # Need to delete history5_copy1 on the history list page for regular_user1 - self.delete_history( id=str( history5_copy1.id ) ) + # Shared history5 should be in regular_user1's list of shared histories + self.view_shared_histories( check_str=history5.name, check_str2=admin_user.email ) + # Clone restricted history5 + self.clone_history( self.security.encode_id( history5.id ), + check_str1='is now included in your list of stored histories.' ) + global history5_clone1 + history5_clone1 = galaxy.model.History.filter( and_( galaxy.model.History.table.c.deleted==False, + galaxy.model.History.table.c.user_id==regular_user1.id ) ) \ + .order_by( desc( galaxy.model.History.table.c.create_time ) ).first() + assert history5_clone1 is not None, "Problem retrieving history5_clone1 from database" + # Check list of histories to make sure shared history5 was cloned + self.view_stored_active_histories( check_str="Clone of '%s'" % history5.name ) + # Make sure the dataset is accessible + self.switch_history( id=self.security.encode_id( history5_clone1.id ), name=history5_clone1.name ) + self.check_history_for_string( 'chr1' ) self.logout() self.login( email=admin_user.email ) - def test_040_sharing_history_by_making_new_sharing_role( self ): + def test_055_sharing_restricted_history_by_making_new_sharing_role( self ): """Testing sharing a restricted history by associating a new sharing role with protected datasets""" - self.switch_history( id=str( history5.id ), name=history5.name ) # At this point, history5 should have 1 item, 1.bed, which is public. We'll add another # item which will be private to admin_user due to the permissions on history5 self.upload_file( '2.bed', dbkey='hg18' ) - check_str = 'The following datasets can be shared with %s with no changes' % regular_user1.email - check_str2 = 'The following datasets can be shared with %s by updating their permissions' % regular_user1.email - action_check_str = 'Histories (%s) have been shared with: %s' % ( history5.name, regular_user1.email ) - self.share_history( str( history5.id ), - regular_user1.email, - check_str, - check_str2=check_str2, - action='private', - action_check_str=action_check_str ) - history5_copy2 = galaxy.model.History.query().order_by( desc( galaxy.model.History.table.c.create_time ) ).first() - assert history5_copy2 is not None, "Problem retrieving history5_copy2 from database" + check_str_after_submit = 'The following datasets can be shared with %s with no changes' % regular_user2.email + check_str_after_submit2 = 'The following datasets can be shared with %s by updating their permissions' % regular_user2.email + action_check_str_after_submit = 'History (%s) now shared with: 2 users.' % history5.name + self.share_current_history( regular_user2.email,
participants (1)
-
Greg Von Kuster