details: http://www.bx.psu.edu/hg/galaxy/rev/0a8d73234704 changeset: 3253:0a8d73234704 user: jeremy goecks <jeremy.goecks@emory.edu> date: Wed Jan 20 12:26:13 2010 -0500 description: Unified sharing model for histories, workflows, and pages; these items can now be private, shared with a set of users, accessible via link for viewing and importing, or published. Specific features added are: (1) added published flag to histories and workflows, added importable flag to pages; (2) added unified sharing interface for setting sharing options; (3) updated grid framework to use new sharing model; (4) updated functional tests to reflect new sharing model; (5) added legacy support for making pretty URLs for items already made importable. Also, new sharing model does not yet allow sharing of multiple items at once, so functional test for this feature has been commented out. Fixes #229; Fixes #182. diffstat: lib/galaxy/model/__init__.py | 4 + lib/galaxy/model/mapping.py | 7 +- lib/galaxy/model/migrate/versions/0033_published_cols_for_histories_and_workflows.py | 101 ++++ lib/galaxy/web/base/controller.py | 97 +++- lib/galaxy/web/controllers/history.py | 225 +++++---- lib/galaxy/web/controllers/workflow.py | 48 +- lib/galaxy/web/framework/helpers/__init__.py | 1 + templates/display_base.mako | 74 ++- templates/history/list_published.mako | 4 + templates/history/share.mako | 2 +- templates/sharing_base.mako | 187 ++++++++ templates/workflow/list.mako | 2 +- templates/workflow/list_published.mako | 4 + templates/workflow/sharing.mako | 226 ++++++--- test/base/twilltestcase.py | 14 +- test/functional/test_history_functions.py | 28 +- 16 files changed, 762 insertions(+), 262 deletions(-) diffs (1401 lines): diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py Tue Jan 19 18:15:27 2010 -0500 +++ b/lib/galaxy/model/__init__.py Wed Jan 20 12:26:13 2010 -0500 @@ -184,6 +184,7 @@ self.deleted = False self.purged = False self.genome_build = None + self.published = False # Relationships self.user = user self.datasets = [] @@ -1115,6 +1116,7 @@ self.user = None self.name = None self.slug = None + self.published = False self.latest_workflow_id = None self.workflows = [] @@ -1391,6 +1393,8 @@ self.slug = None self.latest_revision_id = None self.revisions = [] + self.importable = None + self.published = None class PageRevision( object ): def __init__( self ): diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py Tue Jan 19 18:15:27 2010 -0500 +++ b/lib/galaxy/model/mapping.py Wed Jan 20 12:26:13 2010 -0500 @@ -79,7 +79,8 @@ Column( "purged", Boolean, index=True, default=False ), Column( "genome_build", TrimmedString( 40 ) ), Column( "importable", Boolean, default=False ), - Column( "slug", TEXT, index=True ) ) + Column( "slug", TEXT, index=True ), + Column( "published", Boolean, index=True ) ) HistoryUserShareAssociation.table = Table( "history_user_share_association", metadata, Column( "id", Integer, primary_key=True ), @@ -513,7 +514,8 @@ Column( "name", TEXT ), Column( "deleted", Boolean, default=False ), Column( "importable", Boolean, default=False ), - Column( "slug", TEXT, index=True ) + Column( "slug", TEXT, index=True ), + Column( "published", Boolean, index=True ) ) Workflow.table = Table( "workflow", metadata, @@ -667,6 +669,7 @@ ForeignKey( "page_revision.id", use_alter=True, name='page_latest_revision_id_fk' ), index=True ), Column( "title", TEXT ), Column( "slug", TEXT, unique=True, index=True ), + Column( "importable", Boolean, index=True, default=False ), Column( "published", Boolean, index=True, default=False ), Column( "deleted", Boolean, index=True, default=False ), ) diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/model/migrate/versions/0033_published_cols_for_histories_and_workflows.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/model/migrate/versions/0033_published_cols_for_histories_and_workflows.py Wed Jan 20 12:26:13 2010 -0500 @@ -0,0 +1,101 @@ +""" +Migration script to add necessary columns for distinguishing between viewing/importing and publishing histories, \ +workflows, and pages. Script adds published column to histories and workflows and importable column to pages. +""" + +from sqlalchemy import * +from sqlalchemy.orm import * +from migrate import * +from migrate.changeset import * + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) +db_session = scoped_session( sessionmaker( bind=migrate_engine, autoflush=False, autocommit=True ) ) + +def upgrade(): + print __doc__ + metadata.reflect() + + # Create published column in history table. + History_table = Table( "history", metadata, autoload=True ) + c = Column( "published", Boolean, index=True ) + try: + c.create( History_table ) + assert c is History_table.c.published + except Exception, e: + print "Adding published column to history table failed: %s" % str( e ) + log.debug( "Adding published column to history table failed: %s" % str( e ) ) + + + # Create index for published column in history table. + try: + i = Index( "ix_history_published", History_table.c.published ) + i.create() + except: + # Mysql doesn't have a named index, but alter should work + History_table.c.published.alter( unique=False ) + + # Create published column in stored workflows table. + StoredWorkflow_table = Table( "stored_workflow", metadata, autoload=True ) + c = Column( "published", Boolean, index=True ) + try: + c.create( StoredWorkflow_table ) + assert c is StoredWorkflow_table.c.published + except Exception, e: + print "Adding published column to stored_workflow table failed: %s" % str( e ) + log.debug( "Adding published column to stored_workflow table failed: %s" % str( e ) ) + + # Create index for published column in stored workflows table. + try: + i = Index( "ix_stored_workflow_published", StoredWorkflow_table.c.published ) + i.create() + except: + # Mysql doesn't have a named index, but alter should work + StoredWorkflow_table.c.published.alter( unique=False ) + + # Create importable column in page table. + Page_table = Table( "page", metadata, autoload=True ) + c = Column( "importable", Boolean, index=True ) + try: + c.create( Page_table ) + assert c is Page_table.c.importable + except Exception, e: + print "Adding importable column to page table failed: %s" % str( e ) + log.debug( "Adding importable column to page table failed: %s" % str( e ) ) + + # Create index for importable column in page table. + try: + i = Index( "ix_page_importable", Page_table.c.importable ) + i.create() + except: + # Mysql doesn't have a named index, but alter should work + Page_table.c.importable.alter( unique=False ) + +def downgrade(): + metadata.reflect() + + # Drop published column from history table. + History_table = Table( "history", metadata, autoload=True ) + try: + History_table.c.published.drop() + except Exception, e: + print "Dropping column published from history table failed: %s" % str( e ) + log.debug( "Dropping column published from history table failed: %s" % str( e ) ) + + # Drop published column from stored_workflow table. + StoredWorkflow_table = Table( "stored_workflow", metadata, autoload=True ) + try: + StoredWorkflow_table.c.published.drop() + except Exception, e: + print "Dropping column published from stored_workflow table failed: %s" % str( e ) + log.debug( "Dropping column published from stored_workflow table failed: %s" % str( e ) ) + + # Drop importable column from page table. + Page_table = Table( "page", metadata, autoload=True ) + try: + Page_table.c.importable.drop() + except Exception, e: + print "Dropping column importable from page table failed: %s" % str( e ) + log.debug( "Dropping column importable from page table failed: %s" % str( e ) ) \ No newline at end of file diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py Tue Jan 19 18:15:27 2010 -0500 +++ b/lib/galaxy/web/base/controller.py Wed Jan 20 12:26:13 2010 -0500 @@ -9,6 +9,7 @@ from galaxy.web import error, form, url_for from galaxy.model.orm import * from galaxy.web.framework.helpers import grids +from galaxy.util.odict import odict from Cheetah.Template import Template @@ -57,22 +58,94 @@ if history.user != user: error( "History is not owned by current user" ) return history + +Root = BaseController + +class SharingStatusColumn( grids.GridColumn ): + """ Grid column to indicate sharing status. """ + def get_value( self, trans, grid, item ): + # Delete items cannot be shared. + if item.deleted: + return "" - def make_item_importable( self, sa_session, item ): - """ Makes item importable and sets item's slug. Does not flush/commit changes, however. Item must have name, user, importable, and slug attributes. """ + # Build a list of sharing for this item. + sharing_statuses = [] + if item.users_shared_with: + sharing_statuses.append( "Shared" ) + if item.importable: + sharing_statuses.append( "Accessible" ) + if item.published: + sharing_statuses.append( "Published" ) + return ", ".join( sharing_statuses ) + + def get_link( self, trans, grid, item ): + if not item.deleted and ( item.users_shared_with or item.importable ): + return dict( operation="share or publish", id=item.id ) + return None + + def filter( self, db_session, user, query, column_filter ): + """ Modify query to filter histories by sharing status. """ + if column_filter == "All": + pass + elif column_filter: + if column_filter == "private": + query = query.filter( self.model_class.users_shared_with == None ) + query = query.filter( self.model_class.importable == False ) + elif column_filter == "shared": + query = query.filter( self.model_class.users_shared_with != None ) + elif column_filter == "accessible": + query = query.filter( self.model_class.importable == True ) + elif column_filter == "published": + query = query.filter( self.model_class.published == True ) + return query + + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = odict() + accepted_filter_labels_and_vals["private"] = "private" + accepted_filter_labels_and_vals["shared"] = "shared" + accepted_filter_labels_and_vals["accessible"] = "accessible" + accepted_filter_labels_and_vals["published"] = "published" + accepted_filter_labels_and_vals["all"] = "All" + accepted_filters = [] + for label, val in accepted_filter_labels_and_vals.items(): + args = { self.key: val } + accepted_filters.append( grids.GridColumnFilter( label, args) ) + return accepted_filters + +class Sharable: + """ Mixin for a controller that manages and item that can be shared. """ + + # Abstract methods. + + @web.expose + @web.require_login( "share Galaxy items" ) + def sharing( self, trans, id, **kwargs ): + """ Handle item sharing. """ + pass + + @web.expose + def display_by_username_and_slug( self, trans, username, slug ): + """ Display item by username and slug. """ + pass + + # Helper methods. + + def _make_item_accessible( self, sa_session, item ): + """ Makes item accessible--viewable and importable--and sets item's slug. Does not flush/commit changes, however. Item must have name, user, importable, and slug attributes. """ item.importable = True - # Set history slug. Slug must be unique among user's importable pages. - slug_base = re.sub( "\s+", "-", item.name.lower() ) - slug = slug_base - count = 1 - while sa_session.query( item.__class__ ).filter_by( user=item.user, slug=slug, importable=True ).count() != 0: - # Slug taken; choose a new slug based on count. This approach can handle numerous histories with the same name gracefully. - slug = '%s-%i' % ( slug_base, count ) - count += 1 - item.slug = slug + # Set item slug. Slug must be unique among user's importable items for item's class. + if item.slug is None: + slug_base = re.sub( "\s+", "-", item.name.lower() ) + slug = slug_base + count = 1 + while sa_session.query( item.__class__ ).filter_by( user=item.user, slug=slug, importable=True ).count() != 0: + # Slug taken; choose a new slug based on count. This approach can handle numerous histories with the same name gracefully. + slug = '%s-%i' % ( slug_base, count ) + count += 1 + item.slug = slug -Root = BaseController """ Deprecated: `BaseController` used to be available under the name `Root` """ diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Tue Jan 19 18:15:27 2010 -0500 +++ b/lib/galaxy/web/controllers/history.py Wed Jan 20 12:26:13 2010 -0500 @@ -4,7 +4,6 @@ from galaxy.model.mapping import desc from galaxy.model.orm import * from galaxy.util.json import * -from galaxy.util.odict import odict from galaxy.tags.tag_handler import TagHandler from sqlalchemy.sql.expression import ClauseElement import webhelpers, logging, operator @@ -32,20 +31,6 @@ else: rval.append( '' ) return rval - - class StatusColumn( grids.GridColumn ): - def get_value( self, trans, grid, history ): - if history.deleted: - return "deleted" - elif history.users_shared_with: - return "shared" - elif history.importable: - return "importable" - return "" - def get_link( self, trans, grid, item ): - if item.users_shared_with or item.importable: - return dict( operation="sharing", id=item.id ) - return None class DeletedColumn( grids.GridColumn ): def get_accepted_filters( self ): @@ -56,33 +41,6 @@ args = { self.key: val } accepted_filters.append( grids.GridColumnFilter( label, args) ) return accepted_filters - - class SharingColumn( grids.GridColumn ): - def filter( self, db_session, user, query, column_filter ): - """ Modify query to filter histories by sharing status. """ - if column_filter == "All": - pass - elif column_filter: - if column_filter == "private": - query = query.filter( model.History.users_shared_with == None ) - query = query.filter( model.History.importable == False ) - elif column_filter == "shared": - query = query.filter( model.History.users_shared_with != None ) - elif column_filter == "importable": - query = query.filter( model.History.importable == True ) - return query - def get_accepted_filters( self ): - """ Returns a list of accepted filters for this column. """ - accepted_filter_labels_and_vals = odict() - accepted_filter_labels_and_vals["private"] = "private" - accepted_filter_labels_and_vals["shared"] = "shared" - accepted_filter_labels_and_vals["importable"] = "importable" - accepted_filter_labels_and_vals["all"] = "All" - accepted_filters = [] - for label, val in accepted_filter_labels_and_vals.items(): - args = { self.key: val } - accepted_filters.append( grids.GridColumnFilter( label, args) ) - return accepted_filters # Grid definition title = "Saved Histories" @@ -95,12 +53,11 @@ attach_popup=True, filterable="advanced" ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), grids.IndividualTagsColumn( "Tags", "tags", model.History, model.HistoryTagAssociation, filterable="advanced", grid_name="HistoryListGrid" ), - StatusColumn( "Status", attach_popup=False ), + SharingStatusColumn( "Sharing", key="sharing", model_class=model.History, filterable="advanced", sortable=False ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), # Columns that are valid for filtering but are not visible. - DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ), - SharingColumn( "Shared", key="shared", visible=False, filterable="advanced" ), + DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ) ] columns.append( grids.MulticolFilterColumn( @@ -111,20 +68,17 @@ operations = [ grids.GridOperation( "Switch", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ), - grids.GridOperation( "Share", condition=( lambda item: not item.deleted ), async_compatible=False ), - grids.GridOperation( "Unshare", condition=( lambda item: not item.deleted ), async_compatible=False ), + grids.GridOperation( "Share or Publish", allow_multiple=False, condition=( lambda item: not item.deleted ), async_compatible=False ), grids.GridOperation( "Rename", condition=( lambda item: not item.deleted ), async_compatible=False ), grids.GridOperation( "Delete", condition=( lambda item: not item.deleted ), async_compatible=True ), grids.GridOperation( "Undelete", condition=( lambda item: item.deleted ), async_compatible=True ), - grids.GridOperation( "Enable import via link", condition=( lambda item: item.deleted ), async_compatible=True ), - grids.GridOperation( "Disable import via link", condition=( lambda item: item.deleted ), async_compatible=True ) ] standard_filters = [ grids.GridColumnFilter( "Active", args=dict( deleted=False ) ), grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ), grids.GridColumnFilter( "All", args=dict( deleted='All' ) ), ] - default_filter = dict( name="All", deleted="False", tags="All", shared="All" ) + default_filter = dict( name="All", deleted="False", tags="All", sharing="All" ) num_rows_per_page = 50 preserve_state = False use_async = True @@ -198,10 +152,10 @@ # Join so that searching history.user makes sense. return session.query( self.model_class ).join( model.User.table ) def apply_default_filter( self, trans, query, **kwargs ): - # A public history is importable, has a slug, and is not deleted. - return query.filter( self.model_class.importable==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) + # A public history is published, has a slug, and is not deleted. + return query.filter( self.model_class.published == True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) -class HistoryController( BaseController ): +class HistoryController( BaseController, Sharable ): @web.expose def index( self, trans ): return "" @@ -231,13 +185,11 @@ status = message = None if 'operation' in kwargs: operation = kwargs['operation'].lower() - if operation == "share": - return self.share( trans, **kwargs ) + if operation == "share or publish": + return self.sharing( trans, **kwargs ) if operation == "rename": return self.rename( trans, **kwargs ) history_ids = util.listify( kwargs.get( 'id', [] ) ) - if operation == "sharing": - return self.sharing( trans, id=history_ids ) # Display no message by default status, message = None, None refresh_history = False @@ -519,9 +471,8 @@ if not history_to_view: return trans.show_error_message( "The specified history does not exist." ) # Admin users can view any history - # TODO: Use a new flag to determine if history is viewable? if not trans.user_is_admin and not history_to_view.importable: - error( "Either you are not allowed to view this history or the owner of this history has not published it." ) + error( "Either you are not allowed to view this history or the owner of this history has not made it accessible." ) # View history. query = trans.sa_session.query( model.HistoryDatasetAssociation ) \ .filter( model.HistoryDatasetAssociation.history == history_to_view ) \ @@ -559,6 +510,111 @@ item = history, item_data = query.all() ) @web.expose + @web.require_login( "share Galaxy histories" ) + def sharing( self, trans, id=None, histories=[], **kwargs ): + """ Handle history sharing. """ + + # Get session and histories. + session = trans.sa_session + if id: + ids = util.listify( id ) + if ids: + histories = [ self.get_history( trans, history_id ) for history_id in ids ] + else: + # Use histories passed in. + pass + + # Do operation on histories. + for history in histories: + if 'make_accessible_via_link' in kwargs: + self._make_item_accessible( trans.sa_session, history ) + elif 'make_accessible_and_publish' in kwargs: + self._make_item_accessible( trans.sa_session, history ) + history.published = True + elif 'publish' in kwargs: + if history.importable: + history.published = True + else: + # TODO: report error here. + pass + elif 'disable_link_access' in kwargs: + history.importable = False + elif 'unpublish' in kwargs: + history.published = False + elif 'disable_link_access_and_unpubish' in kwargs: + history.importable = stored.published = False + elif 'unshare_user' in kwargs: + user = trans.sa_session.query( trans.app.model.User ).get( trans.security.decode_id( kwargs[ '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' ) + husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all() + if husas: + for husa in husas: + trans.sa_session.delete( husa ) + + # Legacy issue: histories made accessible before recent updates may not have a slug. Create slug for any histories that need them. + for history in histories: + if history.importable and not history.slug: + self._make_item_accessible( trans.sa_session, history ) + + session.flush() + + return trans.fill_template( "/sharing_base.mako", item=history ) + + ## TODO: remove this method when history sharing has been verified to work correctly with new sharing() method. + @web.expose + @web.require_login( "share histories with other users" ) + def sharing_old( self, trans, histories=[], id=None, **kwd ): + """Performs sharing of histories among users.""" + # histories looks like: [ historyX, historyY ] + params = util.Params( kwd ) + msg = util.restore_text ( params.get( 'msg', '' ) ) + if id: + ids = util.listify( id ) + if ids: + histories = [ self.get_history( trans, history_id ) for history_id in ids ] + for history in histories: + trans.sa_session.add( history ) + if params.get( 'enable_import_via_link', False ): + self.make_item_importable( trans.sa_session, history ) + trans.sa_session.flush() + elif params.get( 'disable_import_via_link', False ): + history.importable = False + trans.sa_session.flush() + elif params.get( 'unshare_user', False ): + user = trans.sa_session.query( 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' ) + husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all() + if husas: + for husa in husas: + trans.sa_session.delete( husa ) + trans.sa_session.flush() + histories = [] + # Get all histories that have been shared with others + husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ + .join( "history" ) \ + .filter( and_( trans.app.model.History.user == trans.user, + trans.app.model.History.deleted == False ) ) \ + .order_by( trans.app.model.History.table.c.name ) + for husa in husas: + history = husa.history + if history not in histories: + histories.append( history ) + # Get all histories that are importable + importables = trans.sa_session.query( trans.app.model.History ) \ + .filter_by( user=trans.user, importable=True, deleted=False ) \ + .order_by( trans.app.model.History.table.c.name ) + for importable in importables: + if importable not in histories: + histories.append( importable ) + # Sort the list of histories by history.name + histories.sort( key=operator.attrgetter( 'name') ) + return trans.fill_template( 'history/sharing.mako', histories=histories, msg=msg, messagetype='done' ) + + @web.expose @web.require_login( "share histories with other users" ) def share( self, trans, id=None, email="", **kwd ): # If a history contains both datasets that can be shared and others that cannot be shared with the desired user, @@ -613,6 +669,7 @@ # User seems to be sharing an empty history send_to_err = "You cannot share an empty history. " return trans.fill_template( "/history/share.mako", histories=histories, email=email, send_to_err=send_to_err ) + @web.expose @web.require_login( "share restricted histories with other users" ) def share_restricted( self, trans, id=None, email="", **kwd ): @@ -833,56 +890,6 @@ 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 ): - """Performs sharing of histories among users.""" - # histories looks like: [ historyX, historyY ] - params = util.Params( kwd ) - msg = util.restore_text ( params.get( 'msg', '' ) ) - if id: - ids = util.listify( id ) - if ids: - histories = [ self.get_history( trans, history_id ) for history_id in ids ] - for history in histories: - trans.sa_session.add( history ) - if params.get( 'enable_import_via_link', False ): - self.make_item_importable( trans.sa_session, history ) - trans.sa_session.flush() - elif params.get( 'disable_import_via_link', False ): - history.importable = False - trans.sa_session.flush() - elif params.get( 'unshare_user', False ): - user = trans.sa_session.query( 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' ) - husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=user, history=history ).all() - if husas: - for husa in husas: - trans.sa_session.delete( husa ) - trans.sa_session.flush() - histories = [] - # Get all histories that have been shared with others - husas = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) \ - .join( "history" ) \ - .filter( and_( trans.app.model.History.user == trans.user, - trans.app.model.History.deleted == False ) ) \ - .order_by( trans.app.model.History.table.c.name ) - for husa in husas: - history = husa.history - if history not in histories: - histories.append( history ) - # Get all histories that are importable - importables = trans.sa_session.query( trans.app.model.History ) \ - .filter_by( user=trans.user, importable=True, deleted=False ) \ - .order_by( trans.app.model.History.table.c.name ) - for importable in importables: - if importable not in histories: - histories.append( importable ) - # Sort the list of histories by history.name - histories.sort( key=operator.attrgetter( 'name') ) - 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() diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/web/controllers/workflow.py --- a/lib/galaxy/web/controllers/workflow.py Tue Jan 19 18:15:27 2010 -0500 +++ b/lib/galaxy/web/controllers/workflow.py Wed Jan 20 12:26:13 2010 -0500 @@ -51,8 +51,8 @@ def apply_default_filter( self, trans, query, **kwargs ): return query.filter_by( user=trans.user, deleted=False ) -class PublicStoredWorkflowListGrid( grids.Grid ): - title = "Public Workflows" +class StoredWorkflowAllPublishedGrid( grids.Grid ): + title = "Published Workflows" model_class = model.StoredWorkflow default_sort_key = "-update_time" default_filter = dict( public_url="All", username="All", tags="All" ) @@ -74,12 +74,12 @@ # Join so that searching stored_workflow.user makes sense. return session.query( self.model_class ).join( model.User.table ) def apply_default_filter( self, trans, query, **kwargs ): - # A public workflow is importable, has a slug, and is not deleted. - return query.filter( self.model_class.importable==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) + # A public workflow is published, has a slug, and is not deleted. + return query.filter( self.model_class.published==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) -class WorkflowController( BaseController ): +class WorkflowController( BaseController, Sharable ): stored_list_grid = StoredWorkflowListGrid() - public_list_grid = PublicStoredWorkflowListGrid() + published_list_grid = StoredWorkflowAllPublishedGrid() @web.expose def index( self, trans ): @@ -146,7 +146,7 @@ @web.expose def list_published( self, trans, **kwargs ): - grid = self.public_list_grid( trans, **kwargs ) + grid = self.published_list_grid( trans, **kwargs ) if 'async' in kwargs: return grid else: @@ -228,15 +228,27 @@ @web.expose @web.require_login( "use Galaxy workflows" ) def sharing( self, trans, id, **kwargs ): + """ Handle workflow sharing. """ + + # Get session and workflow. session = trans.sa_session stored = get_stored_workflow( trans, id ) session.add( stored ) - if 'enable_import_via_link' in kwargs: - self.make_item_importable( trans.sa_session, stored ) - session.flush() - elif 'disable_import_via_link' in kwargs: + + # Do operation on workflow. + if 'make_accessible_via_link' in kwargs: + self._make_item_accessible( trans.sa_session, stored ) + elif 'make_accessible_and_publish' in kwargs: + self._make_item_accessible( trans.sa_session, stored ) + stored.published = True + elif 'publish' in kwargs: + stored.published = True + elif 'disable_link_access' in kwargs: stored.importable = False - session.flush() + elif 'unpublish' in kwargs: + stored.published = False + elif 'disable_link_access_and_unpubish' in kwargs: + stored.importable = stored.published = False elif 'unshare_user' in kwargs: user = session.query( model.User ).get( trans.security.decode_id( kwargs['unshare_user' ] ) ) if not user: @@ -244,10 +256,16 @@ association = session.query( model.StoredWorkflowUserShareAssociation ) \ .filter_by( user=user, stored_workflow=stored ).one() session.delete( association ) - session.flush() - return trans.fill_template( "workflow/sharing.mako", - stored=stored ) + + # Legacy issue: workflows made accessible before recent updates may not have a slug. Create slug for any workflows that need them. + if stored.importable and not stored.slug: + self._make_item_accessible( trans.sa_session, stored ) + + session.flush() + return trans.fill_template( "/sharing_base.mako", + item=stored ) + @web.expose @web.require_login( "use Galaxy workflows" ) def imp( self, trans, id, **kwargs ): diff -r 2916f1b9b82c -r 0a8d73234704 lib/galaxy/web/framework/helpers/__init__.py --- a/lib/galaxy/web/framework/helpers/__init__.py Tue Jan 19 18:15:27 2010 -0500 +++ b/lib/galaxy/web/framework/helpers/__init__.py Wed Jan 20 12:26:13 2010 -0500 @@ -42,3 +42,4 @@ in a better way. """ return "\n".join( [ javascript_include_tag( "/static/scripts/" + name + ".js?v=2" ) for name in args ] ) + diff -r 2916f1b9b82c -r 0a8d73234704 templates/display_base.mako --- a/templates/display_base.mako Tue Jan 19 18:15:27 2010 -0500 +++ b/templates/display_base.mako Wed Jan 20 12:26:13 2010 -0500 @@ -3,8 +3,24 @@ <%! from galaxy.model import History, StoredWorkflow, Page + from galaxy.web.framework.helpers import iff %> +## Get display name for a class. +<%def name="get_class_display_name( a_class )"> +<% + ## Start with exceptions, end with default. + if a_class is StoredWorkflow: + return "Workflow" + else: + return a_class.__name__ +%> +</%def> + +<%def name="title()"> + Galaxy :: ${iff( item.published, "Published ", "Accessible " ) + self.get_class_display_name( item.__class__ )} : ${item.name} +</%def> + <%def name="init()"> <% self.has_left_panel=False @@ -85,10 +101,12 @@ %> <div class="unified-panel-header" unselectable="on"> - <div class="unified-panel-header-inner"> - <a href="${href_to_all_items}">Published ${item_plural}</a> | - <a href="${href_to_user_items}">${item.user.username}</a> | ${self.get_item_name( item )} - </div> + %if item.published: + <div class="unified-panel-header-inner"> + <a href="${href_to_all_items}">Published ${item_plural}</a> | + <a href="${href_to_user_items}">${item.user.username}</a> | ${self.get_item_name( item )} + </div> + %endif </div> <div class="unified-panel-body"> @@ -101,31 +119,33 @@ ${self.render_item( item, item_data )} </div> - <div class="page-meta"> - ## Page meta. - <div><strong>Related ${item_plural}</strong></div> - <p> - <a href="${href_to_all_items}">All published ${item_plural.lower()}</a><br> - <a href="${href_to_user_items}">${item_plural} owned by ${item.user.username}</a> + %if item.published: + <div class="page-meta"> + ## Page meta. + <div><strong>Related ${item_plural}</strong></div> + <p> + <a href="${href_to_all_items}">All published ${item_plural.lower()}</a><br> + <a href="${href_to_user_items}">${item_plural} owned by ${item.user.username}</a> - ## Tags. - <div><strong>Tags</strong></div> - <p> - ## Community tags. - <div> - Community: - ${render_community_tagging_element( tagged_item=item, tag_click_fn='community_tag_click', use_toggle_link=False )} - %if len ( item.tags ) == 0: - none - %endif + ## Tags. + <div><strong>Tags</strong></div> + <p> + ## Community tags. + <div> + Community: + ${render_community_tagging_element( tagged_item=item, tag_click_fn='community_tag_click', use_toggle_link=False )} + %if len ( item.tags ) == 0: + none + %endif + </div> + ## Individual tags. + <p> + <div> + Yours: + ${render_individual_tagging_element( user=trans.get_user(), tagged_item=item, elt_context='view.mako', use_toggle_link=False, tag_click_fn='community_tag_click' )} + </div> </div> - ## Individual tags. - <p> - <div> - Yours: - ${render_individual_tagging_element( user=trans.get_user(), tagged_item=item, elt_context='view.mako', use_toggle_link=False, tag_click_fn='community_tag_click' )} - </div> - </div> + %endif </div> </div> </%def> diff -r 2916f1b9b82c -r 0a8d73234704 templates/history/list_published.mako --- a/templates/history/list_published.mako Tue Jan 19 18:15:27 2010 -0500 +++ b/templates/history/list_published.mako Wed Jan 20 12:26:13 2010 -0500 @@ -9,6 +9,10 @@ %> </%def> +<%def name="title()"> + Galaxy :: Published Histories +</%def> + <%def name="center_panel()"> ## <iframe name="galaxy_main" id="galaxy_main" frameborder="0" style="position: absolute; width: 100%; height: 100%;" src="${h.url_for( controller="page", action="list" )}"> </iframe> diff -r 2916f1b9b82c -r 0a8d73234704 templates/history/share.mako --- a/templates/history/share.mako Tue Jan 19 18:15:27 2010 -0500 +++ b/templates/history/share.mako Wed Jan 20 12:26:13 2010 -0500 @@ -7,7 +7,7 @@ <div class="toolFormBody"> %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" > + <form name='share' id='share' action="${h.url_for( action='share' )}" method="post" > <div class="form-title-row"><b>Histories to be shared:</b></div> <div class="form-row" style="padding-left: 2em;"> <table width="100%"> diff -r 2916f1b9b82c -r 0a8d73234704 templates/sharing_base.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/sharing_base.mako Wed Jan 20 12:26:13 2010 -0500 @@ -0,0 +1,187 @@ +## +## Base template for sharing an item. Template expects the following parameters: +## (a) item - item to be shared. +## + +<%inherit file="/base.mako"/> +<%! from galaxy import model %> + +## +## Page methods. +## + +<%def name="title()"> + Sharing and Publishing ${get_class_display_name( item.__class__ )} '${item.name}' +</%def> + +<%def name="stylesheets()"> + ${parent.stylesheets()} + <style> + div.indent + { + margin-left: 1em; + } + input.action-button + { + margin-left: 0; + } + </style> +</%def> + +## Get display name for a class. +<%def name="get_class_display_name( a_class )"> +<% + ## Start with exceptions, end with default. + if a_class is model.StoredWorkflow: + return "Workflow" + else: + return a_class.__name__ +%> +</%def> + +## Get plural display name for a class. +<%def name="get_class_plural_display_name( a_class )"> +<% + ## Start with exceptions, end with default. + if a_class is model.History: + return "Histories" + else: + return get_class_display_name( a_class ) + "s" +%> +</%def> + +## +## Page content. +## +<% + # + # Setup and variables needed for page. + # + + # Get class name strings. + item_class_name = get_class_display_name( item.__class__ ) + item_class_name_lc = item_class_name.lower() + item_class_plural_name = get_class_plural_display_name( item.__class__ ) + item_class_plural_name_lc = item_class_plural_name.lower() +%> + +<h2>Sharing and Publishing ${item_class_name} '${item.name}'</h2> + +<div class="indent" style="margin-top: 2em"> +<h3>Making ${item_class_name} Accessible via Link and Publishing It</h3> + + <div class="indent"> + %if item.importable: + <% + item_status = "accessible via link" + if item.published: + item_status = item_status + " and published" + %> + This ${item_class_name_lc} <strong>${item_status}</strong>. + <div class="indent"> + <p>Anyone can view and import this ${item_class_name_lc} by visiting the following URL: + <% url = h.url_for( action='display_by_username_and_slug', username=trans.get_user().username, slug=item.slug, qualified=True ) %> + <blockquote> + <a href="${url}" target="_top">${url}</a> + </blockquote> + + %if item.published: + This ${item_class_name_lc} is publicly listed and searchable in Galaxy's <a href='${h.url_for( action='list_published' )}' target="_top">Published ${item_class_plural_name}</a> section. + %endif + </div> + + <p>You can: + <div class="indent"> + <form action="${h.url_for( action='sharing', id=trans.security.encode_id( item.id ) )}" + method="POST"> + %if not item.published: + ## Item is importable but not published. User can disable importable or publish. + <input class="action-button" type="submit" name="disable_link_access" value="Disable Access to ${item_class_name} Link"> + <div class="toolParamHelp">Disables ${item_class_name_lc}'s link so that it is not accessible.</div> + <br> + <input class="action-button" type="submit" name="publish" value="Publish ${item_class_name}" method="POST"> + <div class="toolParamHelp">Publishes the ${item_class_name_lc} to Galaxy's <a href='${h.url_for( action='list_published' )}' target="_top">Published ${item_class_plural_name}</a> section, where it is publicly listed and searchable.</div> + + <br> + %else: ## item.published == True + ## Item is importable and published. User can unpublish or disable import and unpublish. + <input class="action-button" type="submit" name="unpublish" value="Unpublish ${item_class_name}"> + <div class="toolParamHelp">Removes ${item_class_name_lc} from Galaxy's <a href='${h.url_for( action='list_published' )}' target="_top">Published ${item_class_plural_name}</a> section so that it is not publicly listed or searchable.</div> + <br> + <input class="action-button" type="submit" name="disable_link_access_and_unpubish" value="Disable Access to ${item_class_name} via Link and Unpublish"> + <div class="toolParamHelp">Disables ${item_class_name_lc}'s link so that it is not accessible and removes ${item_class_name_lc} from Galaxy's <a href='${h.url_for( action='list_published' )}' target='_top'>Published ${item_class_plural_name}</a> section so that it is not publicly listed or searchable.</div> + %endif + + </form> + </div> + + %else: + + This ${item_class_name_lc} is currently restricted so that only you and the users listed below can access it. You can: + <p> + <form action="${h.url_for( action='sharing', id=trans.security.encode_id(item.id) )}" method="POST"> + <input class="action-button" type="submit" name="make_accessible_via_link" value="Make ${item_class_name} Accessible via Link"> + <div class="toolParamHelp">Generates a web link that you can share with other people so that they can view and import the ${item_class_name_lc}.</div> + + <br> + <input class="action-button" type="submit" name="make_accessible_and_publish" value="Make ${item_class_name} Accessible and Publish" method="POST"> + <div class="toolParamHelp">Makes the ${item_class_name_lc} accessible via link (see above) and publishes the ${item_class_name_lc} to Galaxy's <a href='${h.url_for( action='list_published' )}' target='_top'>Published ${item_class_plural_name}</a> section, where it is publicly listed and searchable.</div> + </form> + + %endif + </div> + +<h3>Sharing ${item_class_name} with Specific Users</h3> + + <div class="indent"> + %if item.users_shared_with: + + <p> + The following users will see this ${item_class_name_lc} in their ${item_class_name_lc} list and will be + able to run/view and import it. + </p> + + <ul class="manage-table-actions"> + <li> + <a class="action-button" href="${h.url_for( action='share', id=trans.security.encode_id(item.id) )}"> + <span>Share with another user</span> + </a> + </li> + </ul> + + <table class="colored" border="0" cellspacing="0" cellpadding="0" width="100%"> + <tr class="header"> + <th>Email</th> + <th></th> + </tr> + %for i, association in enumerate( item.users_shared_with ): + <% user = association.user %> + <tr> + <td> + ${user.email} + <a id="user-${i}-popup" class="popup-arrow" style="display: none;">▼</a> + </td> + <td> + <div popupmenu="user-${i}-popup"> + <a class="action-button" href="${h.url_for( action='sharing', id=trans.security.encode_id( item.id ), unshare_user=trans.security.encode_id( user.id ) )}">Unshare</a> + </div> + </td> + </tr> + %endfor + </table> + + %else: + + <p>You have not shared this ${item_class_name_lc} with any users.</p> + + <a class="action-button" href="${h.url_for( action='share', id=trans.security.encode_id(item.id) )}"> + <span>Share with a user</span> + </a> + <br> + + %endif + </div> +</div> + +<p><br><br> +<a href=${h.url_for( action="list" )}>Back to ${item_class_plural_name} List</a> \ No newline at end of file diff -r 2916f1b9b82c -r 0a8d73234704 templates/workflow/list.mako --- a/templates/workflow/list.mako Tue Jan 19 18:15:27 2010 -0500 +++ b/templates/workflow/list.mako Wed Jan 20 12:26:13 2010 -0500 @@ -47,9 +47,9 @@ <div popupmenu="wf-${i}-popup"> <a class="action-button" href="${h.url_for( action='editor', id=trans.security.encode_id(workflow.id) )}" target="_parent">Edit</a> <a class="action-button" href="${h.url_for( controller='root', action='index', workflow_id=trans.security.encode_id(workflow.id) )}" target="_parent">Run</a> + <a class="action-button" href="${h.url_for( action='sharing', id=trans.security.encode_id(workflow.id) )}">Share or Publish</a> <a class="action-button" href="${h.url_for( action='clone', id=trans.security.encode_id(workflow.id) )}">Clone</a> <a class="action-button" href="${h.url_for( action='rename', id=trans.security.encode_id(workflow.id) )}">Rename</a> - <a class="action-button" href="${h.url_for( action='sharing', id=trans.security.encode_id(workflow.id) )}">Sharing</a> <a class="action-button" confirm="Are you sure you want to delete workflow '${workflow.name}'?" href="${h.url_for( action='delete', id=trans.security.encode_id(workflow.id) )}">Delete</a> </div> </td> diff -r 2916f1b9b82c -r 0a8d73234704 templates/workflow/list_published.mako --- a/templates/workflow/list_published.mako Tue Jan 19 18:15:27 2010 -0500 +++ b/templates/workflow/list_published.mako Wed Jan 20 12:26:13 2010 -0500 @@ -9,6 +9,10 @@ %> </%def> +<%def name="title()"> + Galaxy :: Published Workflows +</%def> + <%def name="center_panel()"> ## <iframe name="galaxy_main" id="galaxy_main" frameborder="0" style="position: absolute; width: 100%; height: 100%;" src="${h.url_for( controller="page", action="list" )}"> </iframe> diff -r 2916f1b9b82c -r 0a8d73234704 templates/workflow/sharing.mako --- a/templates/workflow/sharing.mako Tue Jan 19 18:15:27 2010 -0500 +++ b/templates/workflow/sharing.mako Wed Jan 20 12:26:13 2010 -0500 @@ -1,87 +1,161 @@ <%inherit file="/base.mako"/> +<%! from galaxy import model %> -<h2>Public access via link</h2> +## +## Page methods. +## -<p> - %if stored.importable: - <p> - Anyone can view this workflow by visiting the following URL: - <% url = h.url_for( action='display_by_username_and_slug', username=trans.get_user().username, slug=stored.slug, qualified=True ) %> - <blockquote> - <a href="${url}">${url}</a> - </blockquote> +<%def name="title()"> + Sharing Workflow '${stored.name}' +</%def> + +<%def name="stylesheets()"> + ${parent.stylesheets()} + <style> + div.indent + { + margin-left: 1em; + } + input.action-button + { + margin-left: 0; + } + </style> +</%def> + +## Get display name for a class. +<%def name="get_class_display_name( a_class )"> +<% + if a_class is model.History: + return "History" + elif a_class is model.StoredWorkflow: + return "Workflow" + elif a_class is model.Page: + return "Page" +%> +</%def> + +## +## Page content. +## + +<h2>Sharing Workflow '${stored.name}'</h2> + +<div class="indent" style="margin-top: 2em"> +<h3>Making Workflow Accessible via Link and Publishing It</h3> - <p> - Anyone can import this workflow into their history via the following URL: - <% url = h.url_for( action='imp', id=trans.security.encode_id(stored.id), qualified=True ) %> - <blockquote> - <a href="${url}">${url}</a> - </blockquote> - <br> + <div class="indent"> + %if stored.importable: + <% + item_status = "accessible via link" + if stored.published: + item_status = item_status + " and published" + %> + This workflow <strong>${item_status}</strong>. + <div class="indent"> + <p>Anyone can view and import this workflow by visiting the following URL: + <% url = h.url_for( action='display_by_username_and_slug', username=trans.get_user().username, slug=stored.slug, qualified=True ) %> + <blockquote> + <a href="${url}">${url}</a> + </blockquote> + + %if stored.published: + This workflow is publicly listed and searchable in Galaxy's <a href='${h.url_for( action='list_published' )}'>Published Workflows</a> section. + %endif + </div> + + <p>You can: + <div class="indent"> + <form action="${h.url_for( action='sharing', id=trans.security.encode_id(stored.id) )}" + method="POST"> + %if not stored.published: + ## Item is importable but not published. User can disable importable or publish. + <input class="action-button" type="submit" name="disable_link_access" value="Disable Access to Workflow Link"> + <div class="toolParamHelp">Disables workflow's link so that it is not accessible.</div> + <br> + <input class="action-button" type="submit" name="publish" value="Publish Workflow" method="POST"> + <div class="toolParamHelp">Publishes the workflow to Galaxy's <a href='${h.url_for( action='list_published' )}'>Published Workflows</a> section, where it is publicly listed and searchable.</div> + + <br> + %else: ## stored.published == True + ## Item is importable and published. User can unpublish or disable import and unpublish. + <input class="action-button" type="submit" name="unpublish" value="Unpublish Workflow"> + <div class="toolParamHelp">Removes workflow from Galaxy's <a href='${h.url_for( action='list_published' )}'>Published Workflows</a> section so that it is not publicly listed or searchable.</div> + <br> + <input class="action-button" type="submit" name="disable_link_access_and_unpubish" value="Disable Access to Workflow via Link and Unpublish"> + <div class="toolParamHelp">Disables workflow's link so that it is not accessible and removes workflow from Galaxy's <a href='${h.url_for( action='list_published' )}' target='_top'>Published Workflows</a> section so that it is not publicly listed or searchable.</div> + %endif + + </form> + </div> + + %else: + + This workflow is currently restricted so that only you and the users listed below can access it. You can: + <p> + <form action="${h.url_for( action='sharing', id=trans.security.encode_id(stored.id) )}" method="POST"> + <input class="action-button" type="submit" name="make_accessible_via_link" value="Make Workflow Accessible via Link"> + <div class="toolParamHelp">Generates a web link that you can share with other people so that they can view and import the workflow.</div> + + <br> + <input class="action-button" type="submit" name="make_accessible_and_publish" value="Make Workflow Accessible and Publish" method="POST"> + <div class="toolParamHelp">Makes the workflow accessible via link (see above) and publishes the workflow to Galaxy's <a href='${h.url_for( action='list_published' )}' target='_top'>Published Workflows</a> section, where it is publicly listed and searchable.</div> + </form> - <form action="${h.url_for( action='sharing', id=trans.security.encode_id(stored.id) )}" method="POST"> - <input class="action-button" type="submit" name="disable_import_via_link" value="Disable import via link"> - </form> + %endif + </div> + +<h3>Sharing Workflow with Specific Users</h3> + + <div class="indent"> + %if stored.users_shared_with: + + <p> + The following users will see this workflow in their workflow list and will be + able to run/view and import it. + </p> - %else: + <ul class="manage-table-actions"> + <li> + <a class="action-button" href="${h.url_for( action='share', id=trans.security.encode_id(stored.id) )}"> + <span>Share with another user</span> + </a> + </li> + </ul> + + <table class="colored" border="0" cellspacing="0" cellpadding="0" width="100%"> + <tr class="header"> + <th>Email</th> + <th></th> + </tr> + %for i, association in enumerate( stored.users_shared_with ): + <% user = association.user %> + <tr> + <td> + ${user.email} + <a id="user-${i}-popup" class="popup-arrow" style="display: none;">▼</a> + </td> + <td> + <div popupmenu="user-${i}-popup"> + <a class="action-button" href="${h.url_for( id=trans.security.encode_id( stored.id ), unshare_user=trans.security.encode_id( user.id ) )}">Unshare</a> + </div> + </td> + </tr> + %endfor + </table> + + %else: + + <p>You have not shared this workflow with any users.</p> - This workflow 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 anyone to allow them to import this workflow. - - <br> - - <form action="${h.url_for( action='sharing', id=trans.security.encode_id(stored.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 stored.users_shared_with: - - <p> - The following users will see this workflow in thier workflow list, and be - able to run it or create their own copy of it: - </p> - - <ul class="manage-table-actions"> - <li> <a class="action-button" href="${h.url_for( action='share', id=trans.security.encode_id(stored.id) )}"> <span>Share with another user</span> </a> - </li> - </ul> - - <table class="colored" border="0" cellspacing="0" cellpadding="0" width="100%"> - <tr class="header"> - <th>Email</th> - <th></th> - </tr> - %for i, association in enumerate( stored.users_shared_with ): - <% user = association.user %> - <tr> - <td> - ${user.email} - <a id="user-${i}-popup" class="popup-arrow" style="display: none;">▼</a> - </td> - <td> - <div popupmenu="user-${i}-popup"> - <a class="action-button" href="${h.url_for( id=trans.security.encode_id( stored.id ), unshare_user=trans.security.encode_id( user.id ) )}">Unshare</a> - </div> - </td> - </tr> - %endfor - </table> + <br> + + %endif + </div> +</div> -%else: - - <p>You have not shared this workflow with any users.</p> - - <a class="action-button" href="${h.url_for( action='share', id=trans.security.encode_id(stored.id) )}"> - <span>Share with another user</span> - </a> - -%endif \ No newline at end of file +<p><br><br> +<a href=${h.url_for( action="list" )}>Back to Workflows List</a> \ No newline at end of file diff -r 2916f1b9b82c -r 0a8d73234704 test/base/twilltestcase.py --- a/test/base/twilltestcase.py Tue Jan 19 18:15:27 2010 -0500 +++ b/test/base/twilltestcase.py Wed Jan 20 12:26:13 2010 -0500 @@ -301,7 +301,7 @@ 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 ) ) + self.visit_url( "%s/history/list?id=%s&operation=share+or+publish" % ( self.url, history_id ) ) if check_str1: self.check_page_for_string( check_str1 ) if check_str2: @@ -352,27 +352,27 @@ if check_str_after_submit: self.check_page_for_string( check_str_after_submit ) self.home() - def enable_import_via_link( self, history_id, check_str='', check_str_after_submit='' ): + def make_accessible_via_link( self, history_id, check_str='', check_str_after_submit='' ): self.home() - self.visit_page( "history/list?operation=sharing&id=%s" % history_id ) + self.visit_page( "history/list?operation=share+or+publish&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 ) + self.visit_page( 'history/sharing?id=%s&make_accessible_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='' ): + def disable_access_via_link( self, history_id, check_str='', check_str_after_submit='' ): self.home() - self.visit_page( "history/list?operation=sharing&id=%s" % history_id ) + self.visit_page( "history/list?operation=share+or+publish&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 ) + self.visit_page( 'history/sharing?id=%s&disable_link_access=True' % history_id ) if check_str_after_submit: self.check_page_for_string( check_str_after_submit ) self.home() diff -r 2916f1b9b82c -r 0a8d73234704 test/functional/test_history_functions.py --- a/test/functional/test_history_functions.py Tue Jan 19 18:15:27 2010 -0500 +++ b/test/functional/test_history_functions.py Wed Jan 20 12:26:13 2010 -0500 @@ -179,24 +179,24 @@ self.share_current_history( regular_user1.email, check_str=history3.name ) # Check out list of histories to make sure history3 was shared - self.view_stored_active_histories( check_str='operation=sharing' ) - # 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 above link to users' ) - # Make sure history3 is now import-able + self.view_stored_active_histories( check_str='operation=share' ) + # Make history3 accessible via link. + self.make_accessible_via_link( self.security.encode_id( history3.id ), + check_str='Make History Accessible via Link', + check_str_after_submit='Anyone can view and import this history' ) + # Make sure history3 is now accessible. sa_session.refresh( history3 ) if not history3.importable: - raise AssertionError, "History 3 is not marked as importable after enable_import_via_link" + raise AssertionError, "History 3 is not marked as importable after make_accessible_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 above link to users', - check_str_after_submit='Enable import via link' ) - # Try importing history3 after disabling the URL + # Disable access via link for history3. + self.disable_access_via_link( self.security.encode_id( history3.id ), + check_str='Anyone can view and import this history', + check_str_after_submit='Make History Accessible via Link' ) + # Try importing history3 after disabling access via link. 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.' ) @@ -314,6 +314,9 @@ assert history4 is not None, "Problem retrieving history4 from database" self.rename_history( self.security.encode_id( history4.id ), history4.name, new_name=urllib.quote( 'history 4' ) ) sa_session.refresh( history4 ) + # Galaxy's new history sharing code does not yet support sharing multiple histories; when support for sharing multiple histories is added, + # this test will be uncommented and updated. + """ self.upload_file( '2.bed', dbkey='hg18' ) ids = '%s,%s' % ( self.security.encode_id( history3.id ), self.security.encode_id( history4.id ) ) emails = '%s,%s' % ( regular_user2.email, regular_user3.email ) @@ -329,6 +332,7 @@ self.login( email=regular_user3.email ) # 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