details: http://www.bx.psu.edu/hg/galaxy/rev/5f8672ff2ae9 changeset: 3261:5f8672ff2ae9 user: jeremy goecks <jeremy.goecks@emory.edu> date: Sat Jan 23 15:28:25 2010 -0500 description: Integrate 'sharing via email' functionality into sharing framework so that all item sharing/viewing/publishing/security/tagging functionality uses the same code base and the same UI. diffstat: lib/galaxy/web/controllers/history.py | 41 +++++--- lib/galaxy/web/controllers/workflow.py | 15 ++- static/scripts/galaxy.base.js | 4 + static/scripts/packed/galaxy.base.js | 2 +- templates/display_base.mako | 38 ++++++- templates/grid_base.mako | 32 +------ templates/history/display.mako | 6 +- templates/sharing_base.mako | 151 ++++++++++++++++---------------- templates/workflow/list.mako | 5 +- 9 files changed, 152 insertions(+), 142 deletions(-) diffs (479 lines): diff -r 8775859ad3f3 -r 5f8672ff2ae9 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Fri Jan 22 15:21:51 2010 -0500 +++ b/lib/galaxy/web/controllers/history.py Sat Jan 23 15:28:25 2010 -0500 @@ -105,18 +105,18 @@ return history.user.email # Grid definition title = "Histories shared with you by others" - template='/history/grid.mako' model_class = model.History default_sort_key = "-update_time" default_filter = {} columns = [ - grids.GridColumn( "Name", key="name" ), + grids.GridColumn( "Name", key="name", attach_popup=True ), # link=( lambda item: dict( operation="View", id=item.id ) ), attach_popup=True ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), SharedByColumn( "Shared by", key="user_id" ) ] operations = [ + grids.GridOperation( "View", allow_multiple=False, target="_top" ), grids.GridOperation( "Clone" ), grids.GridOperation( "Unshare" ) ] @@ -312,7 +312,11 @@ if 'operation' in kwargs: ids = util.listify( kwargs.get( 'id', [] ) ) operation = kwargs['operation'].lower() - if operation == "clone": + if operation == "view": + # Display history. + history = self.get_history( trans, ids[0], False) + return self.display_by_username_and_slug( trans, history.user.username, history.slug ) + elif operation == "clone": if not ids: message = "Select a history to clone" return self.shared_list_grid( trans, status='error', message=message, **kwargs ) @@ -487,26 +491,33 @@ datasets = query.all(), user_owns_history = user_owns_history, show_deleted = False ) - + @web.expose def display_by_username_and_slug( self, trans, username, slug ): - """ Display history based on a username and slug. """ - session = trans.sa_session - user = session.query( model.User ).filter_by( username=username ).first() - if user is None: - raise web.httpexceptions.HTTPNotFound() - history = trans.sa_session.query( model.History ).filter_by( user=user, slug=slug, deleted=False, importable=True ).first() - if history is None: + """ Display history based on a username and slug. """ + + # Get history. + session = trans.sa_session + user = session.query( model.User ).filter_by( username=username ).first() + history_query_base = trans.sa_session.query( model.History ).filter_by( user=user, slug=slug, deleted=False ) + if user is not None: + # User can view history if it's importable or if it's shared with him/her. + history = history_query_base.filter( or_( model.History.importable==True, model.History.users_shared_with.any( model.HistoryUserShareAssociation.user==trans.get_user() ) ) ).first() + else: + # User not logged in, so only way to view history is if it's importable. + history = history_query_base.filter_by( importable=True ).first() + if history is None: raise web.httpexceptions.HTTPNotFound() - query = trans.sa_session.query( model.HistoryDatasetAssociation ) \ + # Get datasets. + query = trans.sa_session.query( model.HistoryDatasetAssociation ) \ .filter( model.HistoryDatasetAssociation.history == history ) \ .options( eagerload( "children" ) ) \ .join( "dataset" ).filter( model.Dataset.purged == False ) \ .options( eagerload_all( "dataset.actions" ) ) - # Do not show deleted datasets. - query = query.filter( model.HistoryDatasetAssociation.deleted == False ) - return trans.stream_template_mako( "history/display.mako", + # Do not show deleted datasets. + query = query.filter( model.HistoryDatasetAssociation.deleted == False ) + return trans.stream_template_mako( "history/display.mako", item = history, item_data = query.all() ) @web.expose diff -r 8775859ad3f3 -r 5f8672ff2ae9 lib/galaxy/web/controllers/workflow.py --- a/lib/galaxy/web/controllers/workflow.py Fri Jan 22 15:21:51 2010 -0500 +++ b/lib/galaxy/web/controllers/workflow.py Sat Jan 23 15:28:25 2010 -0500 @@ -158,13 +158,18 @@ """ View workflow based on a username and slug. """ session = trans.sa_session - # Get and verify user and stored workflow. + # Get history. + session = trans.sa_session user = session.query( model.User ).filter_by( username=username ).first() - if user is None: - raise web.httpexceptions.HTTPNotFound() - stored_workflow = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, slug=slug, deleted=False, importable=True ).first() + workflow_query_base = trans.sa_session.query( model.StoredWorkflow ).filter_by( user=user, slug=slug, deleted=False ) + if user is not None: + # User can view workflow if it's importable or if it's shared with him/her. + stored_workflow = workflow_query_base.filter( or_( model.StoredWorkflow.importable==True, model.StoredWorkflow.users_shared_with.any( model.StoredWorkflowUserShareAssociation.user==trans.get_user() ) ) ).first() + else: + # User not logged in, so only way to view workflow is if it's importable. + stored_workflow = workflow_query_base.filter_by( importable=True ).first() if stored_workflow is None: - raise web.httpexceptions.HTTPNotFound() + raise web.httpexceptions.HTTPNotFound() # Get data for workflow's steps. for step in stored_workflow.latest_workflow.steps: diff -r 8775859ad3f3 -r 5f8672ff2ae9 static/scripts/galaxy.base.js --- a/static/scripts/galaxy.base.js Fri Jan 22 15:21:51 2010 -0500 +++ b/static/scripts/galaxy.base.js Sat Jan 23 15:28:25 2010 -0500 @@ -37,6 +37,10 @@ if ( target == "_parent" ) { f = window.parent; } + else if + ( target == "_top" ) { + f = window.top; + } f.location = href; } }; diff -r 8775859ad3f3 -r 5f8672ff2ae9 static/scripts/packed/galaxy.base.js --- a/static/scripts/packed/galaxy.base.js Fri Jan 22 15:21:51 2010 -0500 +++ b/static/scripts/packed/galaxy.base.js Sat Jan 23 15:28:25 2010 -0500 @@ -1,1 +1,1 @@ -$.fn.makeAbsolute=function(a){return this.each(function(){var b=$(this);var c=b.position();b.css({position:"absolute",marginLeft:0,marginTop:0,top:c.top,left:c.left,right:$(window).width()-(c.left+b.width())});if(a){b.remove().appendTo("body")}})};jQuery(document).ready(function(){jQuery("a[confirm]").click(function(){return confirm(jQuery(this).attr("confirm"))});make_popup_menus()});function make_popup_menus(){jQuery("div[popupmenu]").each(function(){var c={};$(this).find("a").each(function(){var b=$(this).attr("confirm"),d=$(this).attr("href"),e=$(this).attr("target");c[$(this).text()]=function(){if(!b||confirm(b)){var g=window;if(e=="_parent"){g=window.parent}g.location=d}}});var a=$("#"+$(this).attr("popupmenu"));make_popupmenu(a,c);$(this).remove();a.addClass("popup").show()})}function ensure_popup_helper(){if($("#popup-helper").length==0){$("<div id='popup-helper'/>").css({background:"white",opacity:0,zIndex:15000,position:"absolute",top:0,left:0,width:"100%",height:" 100%"}).appendTo("body").hide()}}function make_popupmenu(d,c){ensure_popup_helper();var a=$(d);var b=$("<ul id='"+d.attr("id")+"-menu'></ul>");$.each(c,function(g,f){if(f){$("<li/>").html(g).click(f).appendTo(b)}else{$("<li class='head'/>").html(g).appendTo(b)}});var e=$("<div class='popmenu-wrapper'>");e.append(b).append("<div class='overlay-border'>").css("position","absolute").appendTo("body").hide();attach_popupmenu(d,e)}function attach_popupmenu(b,d){var a=function(){d.unbind().hide();$("#popup-helper").unbind("click.popupmenu").hide()};var c=function(g){var h=$(b).offset();$("#popup-helper").bind("click.popupmenu",a).show();d.click(a).css({left:0,top:-1000}).show();var f=g.pageX-d.width()/2;f=Math.min(f,$(document).scrollLeft()+$(window).width()-$(d).width()-20);f=Math.max(f,$(document).scrollLeft()+20);d.css({top:g.pageY-5,left:f});return false};$(b).click(c)}var array_length=function(a){if(a.length){return a.length}var b=0;for(element in a){b++}return b};var replace_ dbkey_select=function(){var c=$("select[name=dbkey]");var d=c.attr("value");if(c.length!=0){var e=$("<input id='dbkey-input' type='text'></input>");e.attr("size",40);e.attr("name",c.attr("name"));e.click(function(){var g=$(this).attr("value");$(this).attr("value","Loading...");$(this).showAllInCache();$(this).attr("value",g);$(this).select()});var b=new Array();var a=new Object();c.children("option").each(function(){var h=$(this).text();var g=$(this).attr("value");if(g=="?"){return}b.push(h);a[h]=g;if(g==d){e.attr("value",h)}});if(e.attr("value")==""){e.attr("value","Click to Search or Select Build")}var f={selectFirst:false,autoFill:false,mustMatch:false,matchContains:true,max:1000,minChars:0,hideForLessThanMinChars:false};e.autocomplete(b,f);c.replaceWith(e);$("form").submit(function(){var i=$("#dbkey-input");if(i.length!=0){var h=i.attr("value");var g=a[h];if(g!=null&&g!=undefined){i.attr("value",g)}else{if(d!=""){i.attr("value",d)}else{i.attr("value","?")}}}})}}; \ No newline at end of file +$.fn.makeAbsolute=function(a){return this.each(function(){var b=$(this);var c=b.position();b.css({position:"absolute",marginLeft:0,marginTop:0,top:c.top,left:c.left,right:$(window).width()-(c.left+b.width())});if(a){b.remove().appendTo("body")}})};jQuery(document).ready(function(){jQuery("a[confirm]").click(function(){return confirm(jQuery(this).attr("confirm"))});make_popup_menus()});function make_popup_menus(){jQuery("div[popupmenu]").each(function(){var c={};$(this).find("a").each(function(){var b=$(this).attr("confirm"),d=$(this).attr("href"),e=$(this).attr("target");c[$(this).text()]=function(){if(!b||confirm(b)){var g=window;if(e=="_parent"){g=window.parent}else{if(e=="_top"){g=window.top}}g.location=d}}});var a=$("#"+$(this).attr("popupmenu"));make_popupmenu(a,c);$(this).remove();a.addClass("popup").show()})}function ensure_popup_helper(){if($("#popup-helper").length==0){$("<div id='popup-helper'/>").css({background:"white",opacity:0,zIndex:15000,position:"absolute",t op:0,left:0,width:"100%",height:"100%"}).appendTo("body").hide()}}function make_popupmenu(d,c){ensure_popup_helper();var a=$(d);var b=$("<ul id='"+d.attr("id")+"-menu'></ul>");$.each(c,function(g,f){if(f){$("<li/>").html(g).click(f).appendTo(b)}else{$("<li class='head'/>").html(g).appendTo(b)}});var e=$("<div class='popmenu-wrapper'>");e.append(b).append("<div class='overlay-border'>").css("position","absolute").appendTo("body").hide();attach_popupmenu(d,e)}function attach_popupmenu(b,d){var a=function(){d.unbind().hide();$("#popup-helper").unbind("click.popupmenu").hide()};var c=function(g){var h=$(b).offset();$("#popup-helper").bind("click.popupmenu",a).show();d.click(a).css({left:0,top:-1000}).show();var f=g.pageX-d.width()/2;f=Math.min(f,$(document).scrollLeft()+$(window).width()-$(d).width()-20);f=Math.max(f,$(document).scrollLeft()+20);d.css({top:g.pageY-5,left:f});return false};$(b).click(c)}var array_length=function(a){if(a.length){return a.length}var b=0;for(element in a){b++}return b};var replace_dbkey_select=function(){var c=$("select[name=dbkey]");var d=c.attr("value");if(c.length!=0){var e=$("<input id='dbkey-input' type='text'></input>");e.attr("size",40);e.attr("name",c.attr("name"));e.click(function(){var g=$(this).attr("value");$(this).attr("value","Loading...");$(this).showAllInCache();$(this).attr("value",g);$(this).select()});var b=new Array();var a=new Object();c.children("option").each(function(){var h=$(this).text();var g=$(this).attr("value");if(g=="?"){return}b.push(h);a[h]=g;if(g==d){e.attr("value",h)}});if(e.attr("value")==""){e.attr("value","Click to Search or Select Build")}var f={selectFirst:false,autoFill:false,mustMatch:false,matchContains:true,max:1000,minChars:0,hideForLessThanMinChars:false};e.autocomplete(b,f);c.replaceWith(e);$("form").submit(function(){var i=$("#dbkey-input");if(i.length!=0){var h=i.attr("value");var g=a[h];if(g!=null&&g!=undefined){i.attr("value",g)}else{if(d!=""){i.attr("value",d)}else{i. attr("value","?")}}}})}}; \ No newline at end of file diff -r 8775859ad3f3 -r 5f8672ff2ae9 templates/display_base.mako --- a/templates/display_base.mako Fri Jan 22 15:21:51 2010 -0500 +++ b/templates/display_base.mako Sat Jan 23 15:28:25 2010 -0500 @@ -1,4 +1,12 @@ -<%inherit file="/base_panels.mako"/> +<%! + def inherit( context ): + if context.get('no_panels'): + return '/base.mako' + else: + return '/base_panels.mako' + from galaxy import model +%> +<%inherit file="${inherit( context )}"/> <%namespace file="./tagging_common.mako" import="render_individual_tagging_element, render_community_tagging_element" /> <%! @@ -10,7 +18,7 @@ <%def name="get_class_display_name( a_class )"> <% ## Start with exceptions, end with default. - if a_class is StoredWorkflow: + if a_class is model.StoredWorkflow: return "Workflow" else: return a_class.__name__ @@ -18,7 +26,7 @@ </%def> <%def name="title()"> - Galaxy :: ${iff( item.published, "Published ", "Accessible " ) + self.get_class_display_name( item.__class__ )} : ${item.name} + Galaxy | ${iff( item.published, "Published ", iff( item.importable , "Accessible ", "Shared " ) ) + self.get_class_display_name( item.__class__ )} | ${item.name} </%def> <%def name="init()"> @@ -84,6 +92,12 @@ <% return item.name %> </%def> +## +## When page has no panels, center panel is body. +## +<%def name="body()"> + ${self.center_panel()} +</%def> ## ## Page content. Pages that inherit this page should override render_item_links() and render_item() @@ -101,12 +115,18 @@ %> <div class="unified-panel-header" unselectable="on"> - %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 class="unified-panel-header-inner"> + %if item.published: + <a href="${href_to_all_items}">Published ${item_plural}</a> | + <a href="${href_to_user_items}">${item.user.username}</a> + %elif item.importable: + Accessible ${self.get_class_display_name( item.__class__ )} + %else: + ## Item shared with user. + Shared ${self.get_class_display_name( item.__class__ )} + %endif + | ${self.get_item_name( item )} + </div> </div> <div class="unified-panel-body"> diff -r 8775859ad3f3 -r 5f8672ff2ae9 templates/grid_base.mako --- a/templates/grid_base.mako Fri Jan 22 15:21:51 2010 -0500 +++ b/templates/grid_base.mako Sat Jan 23 15:28:25 2010 -0500 @@ -197,37 +197,7 @@ }); }); } - - // Overrides function in galaxy.base.js so that click does operation. - function make_popup_menus() - { - jQuery( "div[popupmenu]" ).each( function() { - var options = {}; - $(this).find( "a" ).each( function() { - var confirmtext = $(this).attr( "confirm" ), - href = $(this).attr( "href" ), - target = $(this).attr( "target" ); - options[ $(this).text() ] = function() { - if ( !confirmtext || confirm( confirmtext ) ) { - if ( href.indexOf( "operation" ) > -1 ) { - do_operation_from_href(href); - } else { - var f = window; - if ( target == "_parent" ) { - f = window.parent; - } - f.location = href; - } - } - }; - }); - var b = $( "#" + $(this).attr( 'popupmenu' ) ); - make_popupmenu( b, options ); - $(this).remove(); - b.addClass( "popup" ).show(); - }); - } - + // Initialize grid elements. function init_grid_elements() { diff -r 8775859ad3f3 -r 5f8672ff2ae9 templates/history/display.mako --- a/templates/history/display.mako Fri Jan 22 15:21:51 2010 -0500 +++ b/templates/history/display.mako Sat Jan 23 15:28:25 2010 -0500 @@ -229,11 +229,11 @@ <%def name="render_item_links( history )"> %if history.user != trans.get_user(): - <a href="${h.url_for( controller='/history', action='imp', id=trans.security.encode_id(history.id) )}">import and start using history</a> | + <a href="${h.url_for( controller='/history', action='imp', id=trans.security.encode_id(history.id) )}">import and start using history</a> %else: - your history | + your history %endif - <a href="${self.get_history_link( history )}">${_('refresh')}</a> + ##<a href="${self.get_history_link( history )}">${_('refresh')}</a> %if show_deleted: | <a href="${h.url_for('history', show_deleted=False)}">${_('hide deleted')}</a> %endif diff -r 8775859ad3f3 -r 5f8672ff2ae9 templates/sharing_base.mako --- a/templates/sharing_base.mako Fri Jan 22 15:21:51 2010 -0500 +++ b/templates/sharing_base.mako Sat Jan 23 15:28:25 2010 -0500 @@ -67,27 +67,29 @@ <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> +## Require that user have a public username before sharing or publishing an item. +%if trans.get_user().username is None or trans.get_user().username is "": + To make a ${item_class_name_lc} accessible via link or publish it, you must create a public username: + <p> + <form action="${h.url_for( action='set_public_username', id=trans.security.encode_id( item.id ) )}" + method="POST"> + <div class="form-row"> + <label>Public Username:</label> + <div class="form-row-input"> + <input type="text" name="username" size="40"/> + </div> + </div> + <div style="clear: both"></div> + <div class="form-row"> + <input class="action-button" type="submit" name="Set Username" value="Set Username"/> + </div> + </form> +%else: + ## User has a public username, so private sharing and publishing options. + <div class="indent" style="margin-top: 2em"> + <h3>Making ${item_class_name} Accessible via Link and Publishing It</h3> - <div class="indent"> - %if trans.get_user().username is None or trans.get_user().username is "": - To make a ${item_class_name_lc} accessible via link or publish it, you must create a public username: - <p> - <form action="${h.url_for( action='set_public_username', id=trans.security.encode_id( item.id ) )}" - method="POST"> - <div class="form-row"> - <label>Public Username:</label> - <div class="form-row-input"> - <input type="text" name="username" size="40"/> - </div> - </div> - <div style="clear: both"></div> - <div class="form-row"> - <input class="action-button" type="submit" name="Set Username" value="Set Username"/> - </div> - </form> - %else: + <div class="indent"> %if item.importable: <% item_status = "accessible via link" @@ -101,12 +103,12 @@ <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 ) )}" @@ -128,78 +130,75 @@ <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 - %endif - </div> + </div> -<h3>Sharing ${item_class_name} with Specific Users</h3> + <h3>Sharing ${item_class_name} with Specific Users</h3> - <div class="indent"> - %if item.users_shared_with: + <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> + <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> + + <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> + + <p> + <a class="action-button" href="${h.url_for( action='share', id=trans.security.encode_id(item.id) )}"> + <span>Share with another user</span> + </a> + + %else: + + <p>You have not shared this ${item_class_name_lc} with any users.</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> - <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> + %endif + </div> + </div> +%endif <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 8775859ad3f3 -r 5f8672ff2ae9 templates/workflow/list.mako --- a/templates/workflow/list.mako Fri Jan 22 15:21:51 2010 -0500 +++ b/templates/workflow/list.mako Sat Jan 23 15:28:25 2010 -0500 @@ -82,8 +82,9 @@ <td>${len(workflow.latest_workflow.steps)}</td> <td> <div popupmenu="shared-${i}-popup"> - <a class="action-button" href="${h.url_for( action='run', id=trans.security.encode_id(workflow.id) )}">Run</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='display_by_username_and_slug', username=workflow.user.username, slug=workflow.slug)}" target="_top">View</a> + <a class="action-button" href="${h.url_for( action='run', id=trans.security.encode_id(workflow.id) )}">Run</a> + <a class="action-button" href="${h.url_for( action='clone', id=trans.security.encode_id(workflow.id) )}">Clone</a> </div> </td> </tr>