diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 8d71f18a678..15737ce77b4 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -538,6 +538,11 @@ class WorkflowStepConnection( object ): self.input_step_id = None self.input_name = None +class StoredWorkflowUserShareAssociation( object ): + def __init__( self ): + self.stored_workflow = None + self.user = None + ## ---- Utility methods ------------------------------------------------------- def directory_hash_id( id ): diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 27d8d4342f6..dd15ec11338 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -223,6 +223,12 @@ WorkflowStepConnection.table = Table( "workflow_step_connection", metadata, Column( "input_name", String) ) +StoredWorkflowUserShareAssociation.table = Table( "stored_workflow_user_share_connection", metadata, + Column( "id", Integer, primary_key=True ), + Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ) + ) + # With the tables defined we can define the mappers and setup the # relationships between the model objects. @@ -327,6 +333,12 @@ assign_mapper( context, StoredWorkflow, StoredWorkflow.table, primaryjoin=( StoredWorkflow.table.c.latest_workflow_id == Workflow.table.c.id ) ) ) ) +assign_mapper( context, StoredWorkflowUserShareAssociation, StoredWorkflowUserShareAssociation.table, + properties=dict( user=relation( User, backref='workflows_shared_by_others' ), + stored_workflow=relation( StoredWorkflow ) + ) ) + + def db_next_hid( self ): """ Override __next_hid to generate from the database in a concurrency diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index fac5e8e8d44..30b77890310 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -6,7 +6,7 @@ import os, time, logging # Pieces of Galaxy to make global in every controller from galaxy import config, tools, web, model, util -from galaxy.web import error +from galaxy.web import error, url_for from Cheetah.Template import Template diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index b885285ce71..8713bd37dbd 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -22,7 +22,82 @@ class WorkflowController( BaseController ): """ user = trans.get_user() workflows = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, deleted=False ).all() - return trans.fill_template( "workflow/index.mako", workflows = workflows ) + shared_by_others = trans.sa_session \ + .query( model.StoredWorkflowUserShareAssociation ) \ + .filter_by( user=user ) \ + .filter( model.StoredWorkflow.c.deleted == False ) \ + .all() + return trans.fill_template( "workflow/index.mako", + workflows = workflows, + shared_by_others = shared_by_others ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def share( self, trans, id, email="" ): + msg = mtype = None + # Load workflow from database + stored = get_stored_workflow( trans, id ) + if email: + other = model.User.get_by( email=email ) + if not other: + mtype = "error" + msg = ( "User '%s' does not exist" % email ) + elif trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ + .filter_by( user=other, stored_workflow=stored ).count() > 0: + mtype = "error" + msg = ( "Workflow already shared with '%s'" % email ) + else: + + share = model.StoredWorkflowUserShareAssociation() + share.stored_workflow = stored + share.user = other + session = trans.sa_session + session.save( share ) + session.flush() + ## trans.template_context['message'] = "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) + ## return trans.send_redirect( url_for( controller='workflow', action='index' ) ) + return trans.show_message( "Workflow '%s' shared with user '%s'" % ( stored.name, other.email ) ) + return trans.fill_template( "workflow/share.mako", + message = msg, + messagetype = mtype, + stored=stored, + email=email ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def rename( self, trans, id, new_name=None ): + stored = get_stored_workflow( trans, id ) + if new_name is not None: + stored.name = new_name + trans.sa_session.flush() + return trans.response.send_redirect( url_for( controller='workflow', action='index' ) ) + else: + return trans.fill_template( "workflow/rename.mako", stored=stored ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def clone( self, trans, id ): + stored = get_stored_workflow( trans, id, check_ownership=False ) + user = trans.get_user() + if stored.user == user: + owner = True + else: + if trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ + .filter_by( user=user, stored_workflow=stored ).count() == 0: + error( "Workflow is not owned by or shared with current user" ) + owner = False + new_stored = model.StoredWorkflow() + new_stored.name = "Clone of '%s'" % stored.name + new_stored.latest_workflow = stored.latest_workflow + if not owner: + new_stored.name += "shared by '%s'" % stored.user.email + new_stored.user = user + # Persist + session = trans.sa_session + session.save( new_stored ) + session.flush() + # Display the management page + return trans.response.send_redirect( url_for( controller='workflow', action='index' ) ) @web.expose @web.require_login( "create workflows" ) @@ -47,7 +122,7 @@ class WorkflowController( BaseController ): session.save( stored_workflow ) session.flush() # Display the management page - return self.index( trans ) + return trans.response.send_redirect( url_for( controller='workflow', action='index' ) ) @web.expose def delete( self, trans, id=None ): @@ -60,7 +135,7 @@ class WorkflowController( BaseController ): stored.deleted = True stored.flush() # Display the management page - return self.index( trans ) + return trans.response.send_redirect( url_for( controller='workflow', action='index' ) ) @web.expose @web.require_login( "edit workflows" ) @@ -348,7 +423,12 @@ class WorkflowController( BaseController ): @web.expose def run( self, trans, id, **kwargs ): - stored = get_stored_workflow( trans, id ) + stored = get_stored_workflow( trans, id, check_ownership=False ) + user = trans.get_user() + if stored.user != user: + if trans.sa_session.query( model.StoredWorkflowUserShareAssociation ) \ + .filter_by( user=user, stored_workflow=stored ).count() == 0: + error( "Workflow is not owned by or shared with current user" ) # Get the latest revision workflow = stored.latest_workflow # It is possible for a workflow to have 0 steps @@ -392,7 +472,7 @@ class WorkflowController( BaseController ): step.input_connections_by_name = dict( ( conn.input_name, conn ) for conn in step.input_connections ) if not errors: # Run each step, connecting outputs to inputs - outputs = {} + outputs = odict() for step in workflow.steps: if step.type == 'tool' or step.type is None: tool = trans.app.toolbox.tools_by_id[ step.tool_id ] @@ -453,7 +533,7 @@ class WorkflowController( BaseController ): ## ---- Utility methods ------------------------------------------------------- -def get_stored_workflow( trans, id ): +def get_stored_workflow( trans, id, check_ownership=True ): """ Get a StoredWorkflow from the database by id, verifying ownership. """ @@ -466,7 +546,7 @@ def get_stored_workflow( trans, id ): user = trans.get_user() if not user: error( "Must be logged in to use workflows" ) - if not( stored.user == user ): + if check_ownership and not( stored.user == user ): error( "Workflow is not owned by current user" ) # Looks good return stored diff --git a/templates/workflow/index.mako b/templates/workflow/index.mako index 71fe19bbe7f..686e80343c2 100644 --- a/templates/workflow/index.mako +++ b/templates/workflow/index.mako @@ -47,26 +47,63 @@ -

Stored workflows

+

Your workflows

%if workflows: - + + + + ## + + %for workflow in workflows: - + + ## %endfor
Name# of StepsLast UpdatedActions
Name# of StepsLast UpdatedActions
${workflow.name}${len(workflow.latest_workflow.steps)} - ${str(workflow.update_time)[:19]}${len(workflow.latest_workflow.steps)}${str(workflow.update_time)[:19]} run | edit + | rename + | share | delete +
%else: - You have no stored workflows. + You have no workflows. + + %endif + +

Workflows shared with you by others

+ + %if shared_by_others: + + + + + + + + %for association in shared_by_others: + <% workflow = association.stored_workflow %> + + + + + + + %endfor +
NameOwner# of StepsActions
${workflow.name}${workflow.user.email}${len(workflow.latest_workflow.steps)} + run + clone +
+ %else: + + No workflows have been shared with you. %endif diff --git a/templates/workflow/rename.mako b/templates/workflow/rename.mako new file mode 100644 index 00000000000..62c9d709e5d --- /dev/null +++ b/templates/workflow/rename.mako @@ -0,0 +1,36 @@ +<%inherit file="/base.mako"/> + +%if message: +<% +if messagetype is UNDEFINED: + mt = "done" +else: + mt = messagetype +%> +

+

+ ${message} +
+

+%endif + +

+
Rename workflow '${stored.name}'
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ \ No newline at end of file diff --git a/templates/workflow/share.mako b/templates/workflow/share.mako new file mode 100644 index 00000000000..3b7b4848c5a --- /dev/null +++ b/templates/workflow/share.mako @@ -0,0 +1,36 @@ +<%inherit file="/base.mako"/> + +%if message: +<% +if messagetype is UNDEFINED: + mt = "done" +else: + mt = messagetype +%> +

+

+ ${message} +
+

+%endif + +

+
Share workflow '${stored.name}'
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+ \ No newline at end of file