From f100d00ce64e286cd6a1ddfc6a19368fd6160369 Mon Sep 17 00:00:00 2001 From: Jeremy Goecks Date: Mon, 1 Feb 2010 16:30:18 -0500 Subject: [PATCH] Added UI support for annotating histories, HDAs, workflows, and workflow steps. Numerous small changes as well: (a) added an 'edit workflow attributes' to workflow editor; (b) added 'Edit tags and Annotation' area to history panel; (c) added generic javascript method to ajax text updates. Also fixed public URL column bug. --- lib/galaxy/web/base/controller.py | 57 ++++++++++- lib/galaxy/web/controllers/history.py | 13 ++- lib/galaxy/web/controllers/root.py | 4 + lib/galaxy/web/controllers/workflow.py | 67 +++++++++++-- static/june_2007_style/base.css.tmpl | 42 ++++++++ static/june_2007_style/blue/base.css | 7 ++ static/june_2007_style/blue/history.css | 7 -- static/june_2007_style/history.css.tmpl | 40 -------- static/scripts/galaxy.base.js | 44 +++++++++ .../scripts/galaxy.workflow_editor.canvas.js | 5 +- static/scripts/packed/galaxy.base.js | 2 +- .../packed/galaxy.workflow_editor.canvas.js | 2 +- templates/dataset/edit_attributes.mako | 39 +++++--- templates/grid_base.mako | 2 +- templates/root/history.mako | 80 +++++++++------- templates/sharing_base.mako | 8 +- templates/tagging_common.mako | 10 +- templates/workflow/edit_attributes.mako | 66 +++++++++++++ templates/workflow/editor.mako | 96 ++++++++++++++++--- 19 files changed, 461 insertions(+), 130 deletions(-) create mode 100644 templates/workflow/edit_attributes.mako diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 605cb0675ee..54ec35999ca 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -25,9 +25,13 @@ class OwnerColumn( grids.TextColumn ): # Item's public URL based on username and slug. class PublicURLColumn( grids.TextColumn ): def get_link( self, trans, grid, item ): - if item.user.username: + if item.user.username and item.slug: return dict( action='display_by_username_and_slug', username=item.user.username, slug=item.slug ) - else: + elif not item.user.username: + # TODO: provide link to set username. + return None + elif not item.user.slug: + # TODO: provide link to set slg return None class BaseController( object ): @@ -73,6 +77,55 @@ class BaseController( object ): item_class = None return item_class + def get_item_annotation_str( self, db_session, user, item ): + """ Returns a user's annotation string for an item. """ + annotation_obj = self.get_item_annotation_obj( db_session, user, item ) + if annotation_obj: + return annotation_obj.annotation + return None + + def get_item_annotation_obj( self, db_session, user, item ): + """ Returns a user's annotation object for an item. """ + # Get annotation association. TODO: we could replace this eval() with a long if/else stmt, but this is more general without sacrificing + try: + annotation_assoc_class = eval( "model.%sAnnotationAssociation" % item.__class__.__name__ ) + except: + # Item doesn't have an annotation association class and cannot be annotated. + return False + + # Get annotation association object. + annotation_assoc = db_session.query( annotation_assoc_class ).filter_by( user=user ) + if item.__class__ == model.History: + annotation_assoc = annotation_assoc.filter_by( history=item ) + elif item.__class__ == model.HistoryDatasetAssociation: + annotation_assoc = annotation_assoc.filter_by( hda=item ) + elif item.__class__ == model.StoredWorkflow: + annotation_assoc = annotation_assoc.filter_by( stored_workflow=item ) + elif item.__class__ == model.WorkflowStep: + annotation_assoc = annotation_assoc.filter_by( workflow_step=item ) + return annotation_assoc.first() + + def add_item_annotation( self, trans, item, annotation ): + """ Add or update an item's annotation; a user can only have a single annotation for an item. """ + + # Get/create annotation association object. + annotation_assoc = self.get_item_annotation_obj( trans.sa_session, trans.get_user(), item ) + if not annotation_assoc: + # Create association. + # TODO: we could replace this eval() with a long if/else stmt, but this is more general without sacrificing + try: + annotation_assoc_class = eval( "model.%sAnnotationAssociation" % item.__class__.__name__ ) + except: + # Item doesn't have an annotation association class and cannot be annotated. + return False + annotation_assoc = annotation_assoc_class() + item.annotations.append( annotation_assoc ) + annotation_assoc.user = trans.get_user() + + # Set annotation. + annotation_assoc.annotation = annotation + return True + Root = BaseController class SharingStatusColumn( grids.GridColumn ): diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py index 94831bb13f9..f7abd25791b 100644 --- a/lib/galaxy/web/controllers/history.py +++ b/lib/galaxy/web/controllers/history.py @@ -353,7 +353,7 @@ class HistoryController( BaseController, Sharable ): return trans.show_ok_message( "History deleted, a new history is active", refresh_frames=['history'] ) @web.expose def rename_async( self, trans, id=None, new_name=None ): - history = trans.sa_session.query( model.History ).get( id ) + history = self.get_history( trans, id ) # Check that the history exists, and is either owned by the current # user (if logged in) or the current history assert history is not None @@ -365,6 +365,17 @@ class HistoryController( BaseController, Sharable ): history.name = new_name trans.sa_session.add( history ) trans.sa_session.flush() + + @web.expose + @web.require_login( "use Galaxy histories" ) + def annotate_async( self, trans, id, new_annotation=None, **kwargs ): + history = self.get_history( trans, id ) + if new_annotation: + self.add_item_annotation( trans, history, new_annotation ) + trans.sa_session.flush() + return + else: + return "failed" @web.expose @web.json diff --git a/lib/galaxy/web/controllers/root.py b/lib/galaxy/web/controllers/root.py index 3e14922c2c6..2e274726be5 100644 --- a/lib/galaxy/web/controllers/root.py +++ b/lib/galaxy/web/controllers/root.py @@ -78,6 +78,7 @@ class RootController( BaseController ): query = query.filter( model.HistoryDatasetAssociation.deleted == False ) return trans.stream_template_mako( "root/history.mako", history = history, + annotation = self.get_item_annotation_str( trans.sa_session, trans.get_user(), history ), datasets = query.all(), hda_id = hda_id, show_deleted = show_deleted ) @@ -304,6 +305,8 @@ class RootController( BaseController ): else: setattr( data.metadata, name, spec.unwrap( params.get (name, None) ) ) data.datatype.after_setting_metadata( data ) + self.add_item_annotation( trans, data, params.annotation ) + else: msg = ' (Metadata could not be changed because this dataset is currently being used as input or output. You must cancel or wait for these jobs to complete before changing metadata.)' trans.sa_session.flush() @@ -383,6 +386,7 @@ class RootController( BaseController ): messagetype = 'done' return trans.fill_template( "/dataset/edit_attributes.mako", data=data, + data_annotation=self.get_item_annotation_str( trans.sa_session, trans.get_user(), data ), datatypes=ldatatypes, current_user_roles=current_user_roles, all_roles=all_roles, diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index 89af74c9855..68766c1b702 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -303,6 +303,28 @@ class WorkflowController( BaseController, Sharable ): session.flush() # Redirect to load galaxy frames. return trans.response.send_redirect( url_for( controller='workflow' ) ) + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def edit_attributes( self, trans, id, **kwargs ): + # Get workflow and do error checking. + stored = get_stored_workflow( trans, id ) + if not stored: + error( "You do not own this workflow or workflow ID is invalid." ) + + # Update workflow attributes if new values submitted. + if 'name' in kwargs: + # Rename workflow. + stored.name = kwargs[ 'name' ] + if 'annotation' in kwargs: + # Set workflow annotation. + self.add_item_annotation( trans, stored, kwargs[ 'annotation' ] ) + trans.sa_session.flush() + + return trans.fill_template( 'workflow/edit_attributes.mako', + stored=stored, + annotation=self.get_item_annotation_str( trans.sa_session, trans.get_user(), stored ) + ) @web.expose @web.require_login( "use Galaxy workflows" ) @@ -320,7 +342,29 @@ class WorkflowController( BaseController, Sharable ): else: return form( url_for( action='rename', id=trans.security.encode_id(stored.id) ), "Rename workflow", submit_text="Rename" ) \ .add_text( "new_name", "Workflow Name", value=stored.name ) - + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def rename_async( self, trans, id, new_name=None, **kwargs ): + stored = get_stored_workflow( trans, id ) + if new_name: + stored.name = new_name + trans.sa_session.flush() + return + else: + return "failed" + + @web.expose + @web.require_login( "use Galaxy workflows" ) + def annotate_async( self, trans, id, new_annotation=None, **kwargs ): + stored = get_stored_workflow( trans, id ) + if new_annotation: + self.add_item_annotation( trans, stored, new_annotation ) + trans.sa_session.flush() + return + else: + return "failed" + @web.expose @web.require_login( "use Galaxy workflows" ) def clone( self, trans, id ): @@ -400,11 +444,11 @@ class WorkflowController( BaseController, Sharable ): """ if not id: error( "Invalid workflow id" ) - id = trans.security.decode_id( id ) - return trans.fill_template( "workflow/editor.mako", workflow_id=id ) + stored = get_stored_workflow( trans, id ) + return trans.fill_template( "workflow/editor.mako", stored=stored, annotation=self.get_item_annotation_str( trans.sa_session, trans.get_user(), stored ) ) @web.json - def editor_form_post( self, trans, type='tool', tool_id=None, **incoming ): + def editor_form_post( self, trans, type='tool', tool_id=None, annotation=None, **incoming ): """ Accepts a tool state and incoming values, and generates a new tool form and some additional information, packed into a json dictionary. @@ -423,7 +467,8 @@ class WorkflowController( BaseController, Sharable ): 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), 'tool_errors': module.get_errors(), - 'form_html': module.get_config_form() + 'form_html': module.get_config_form(), + 'annotation': annotation } @web.json @@ -445,7 +490,8 @@ class WorkflowController( BaseController, Sharable ): 'tooltip': module.get_tooltip(), 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), - 'form_html': module.get_config_form() + 'form_html': module.get_config_form(), + 'annotation': "" } @web.json @@ -477,6 +523,11 @@ class WorkflowController( BaseController, Sharable ): # FIXME: Frontend should be able to handle workflow messages # as a dictionary not just the values data['upgrade_messages'][step.order_index] = upgrade_message.values() + # Get user annotation. + step_annotation = self.get_item_annotation_obj ( trans.sa_session, trans.get_user(), step ) + annotation_str = "" + if step_annotation: + annotation_str = step_annotation.annotation # Pack attributes into plain dictionary step_dict = { 'id': step.order_index, @@ -489,6 +540,7 @@ class WorkflowController( BaseController, Sharable ): 'data_inputs': module.get_data_inputs(), 'data_outputs': module.get_data_outputs(), 'form_html': module.get_config_form(), + 'annotation' : annotation_str } # Connections input_connections = step.input_connections @@ -551,6 +603,9 @@ class WorkflowController( BaseController, Sharable ): workflow.has_errors = True # Stick this in the step temporarily step.temp_input_connections = step_dict['input_connections'] + + # Save step annotation. + self.add_item_annotation( trans, step, step_dict[ 'annotation' ] ) # Second pass to deal with connections between steps for step in steps: # Input connections diff --git a/static/june_2007_style/base.css.tmpl b/static/june_2007_style/base.css.tmpl index 1f672765084..29af3bc55af 100644 --- a/static/june_2007_style/base.css.tmpl +++ b/static/june_2007_style/base.css.tmpl @@ -656,3 +656,45 @@ div.permissionContainer { } } + +## Icon buttons. + +.icon-button { + width: 16px; + height: 16px; + display: block; + float: left; + margin-left: 2px; + ## Allow alt text for screen readers + text-indent: 20px; +} + +.icon-button.display { + -sprite-group: history-buttons; + -sprite-image: eye_icon.png; +} + +.icon-button.display:hover { + -sprite-group: history-buttons; + -sprite-image: eye_icon_dark.png; +} + +.icon-button.delete { + -sprite-group: history-buttons; + -sprite-image: delete_icon.png; +} + +.icon-button.delete:hover { + -sprite-group: history-buttons; + -sprite-image: delete_icon_dark.png; +} + +.icon-button.edit { + -sprite-group: history-buttons; + -sprite-image: pencil_icon.png; +} + +.icon-button.edit:hover { + -sprite-group: history-buttons; + -sprite-image: pencil_icon_dark.png; +} \ No newline at end of file diff --git a/static/june_2007_style/blue/base.css b/static/june_2007_style/blue/base.css index 2ef720ae485..c0b1591518b 100644 --- a/static/june_2007_style/blue/base.css +++ b/static/june_2007_style/blue/base.css @@ -110,3 +110,10 @@ div.permissionContainer{padding-left:20px;} .text-content fieldset{border-color:#ccc;border:1px solid #ccc;} .text-content th,.text-content td{border-bottom:1px solid #ddd;border-right:1px solid #ccc;} .text-content th,.text-content td{padding:.8em;} +.icon-button{width:16px;height:16px;display:block;float:left;margin-left:2px;text-indent:20px;} +.icon-button.edit{background:url(history-buttons.png) no-repeat 0px -52px;} +.icon-button.edit:hover{background:url(history-buttons.png) no-repeat 0px -78px;} +.icon-button.display{background:url(history-buttons.png) no-repeat 0px -0px;} +.icon-button.display:hover{background:url(history-buttons.png) no-repeat 0px -26px;} +.icon-button.delete{background:url(history-buttons.png) no-repeat 0px -104px;} +.icon-button.delete:hover{background:url(history-buttons.png) no-repeat 0px -130px;} \ No newline at end of file diff --git a/static/june_2007_style/blue/history.css b/static/june_2007_style/blue/history.css index 425e14be24f..ca3e0ba952c 100644 --- a/static/june_2007_style/blue/history.css +++ b/static/june_2007_style/blue/history.css @@ -22,13 +22,6 @@ div.historyItem-queued .state-icon{background:url(history-states.png) no-repeat div.historyItem-noPermission{filter:alpha(opacity=60);-moz-opacity:.60;opacity:.60;} div.historyItemTitleBar.spinner .state-icon{background:url(data_running.gif) 0 1px no-repeat !important;} div.historyItemButtons{float:right;} -.icon-button{width:16px;height:16px;display:block;float:left;margin-left:2px;text-indent:20px;} -.icon-button.display{background:url(history-buttons.png) no-repeat 0px -0px;} -.icon-button.display:hover{background:url(history-buttons.png) no-repeat 0px -26px;} -.icon-button.edit{background:url(history-buttons.png) no-repeat 0px -52px;} -.icon-button.edit:hover{background:url(history-buttons.png) no-repeat 0px -78px;} -.icon-button.delete{background:url(history-buttons.png) no-repeat 0px -104px;} -.icon-button.delete:hover{background:url(history-buttons.png) no-repeat 0px -130px;} div.historyItemBody div{padding-top:2px;} pre.peek{background:white;color:black;width:100%;overflow:auto;} pre.peek th{color:white;background:#023858;} diff --git a/static/june_2007_style/history.css.tmpl b/static/june_2007_style/history.css.tmpl index b2e48bc9bfe..02058be374f 100644 --- a/static/june_2007_style/history.css.tmpl +++ b/static/june_2007_style/history.css.tmpl @@ -120,46 +120,6 @@ div.historyItemButtons { float: right; } -.icon-button { - width: 16px; - height: 16px; - display: block; - float: left; - margin-left: 2px; - ## Allow alt text for screen readers - text-indent: 20px; -} - -.icon-button.display { - -sprite-group: history-buttons; - -sprite-image: eye_icon.png; -} - -.icon-button.display:hover { - -sprite-group: history-buttons; - -sprite-image: eye_icon_dark.png; -} - -.icon-button.edit { - -sprite-group: history-buttons; - -sprite-image: pencil_icon.png; -} - -.icon-button.edit:hover { - -sprite-group: history-buttons; - -sprite-image: pencil_icon_dark.png; -} - -.icon-button.delete { - -sprite-group: history-buttons; - -sprite-image: delete_icon.png; -} - -.icon-button.delete:hover { - -sprite-group: history-buttons; - -sprite-image: delete_icon_dark.png; -} - div.historyItemBody div { padding-top: 2px; } diff --git a/static/scripts/galaxy.base.js b/static/scripts/galaxy.base.js index cfdb1a816c1..5ab9f6ad6e1 100644 --- a/static/scripts/galaxy.base.js +++ b/static/scripts/galaxy.base.js @@ -207,3 +207,47 @@ var replace_dbkey_select = function() }); } } // end replace_dbkey_select() + +// +// Edit and save text asynchronously. +// +function async_save_text(click_to_edit_elt, text_elt_id, save_url, text_parm_name, use_textarea, num_rows) +{ + $("#" + click_to_edit_elt).click( function() { + var old_text = $("#" + text_elt_id).text() + if (use_textarea) + var t = $("" ); + else + var t = $("" ); + t.blur( function() { + $(this).remove(); + $("#" + text_elt_id).show(); + }); + t.keyup( function( e ) { + if ( e.keyCode == 27 ) { + // Escape key + $(this).trigger( "blur" ); + } else if ( e.keyCode == 13 ) { + // Enter key + new_text = this.value; + $(this).trigger( "blur" ); + var ajax_data = new Object(); + ajax_data[text_parm_name] = new_text; + $.ajax({ + url: save_url, + data: ajax_data, + error: function() { alert( "Text editing for elt " + text_elt_id + " failed" ) }, + success: function() { + $("#" + text_elt_id).text( new_text ); + } + }); + } + }); + $("#" + text_elt_id).hide(); + t.insertAfter( $("#" + text_elt_id) ); + t.focus(); + t.select(); + return false; + }); + +} diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index 68cf330658c..a72e5f2bdc0 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -272,6 +272,7 @@ $.extend( Node.prototype, { this.tool_state = data.tool_state; this.tool_errors = data.tool_errors; this.tooltip = data.tooltip ? data.tooltip : "" + this.annotation = data.annotation; if ( this.tool_errors ) { f.addClass( "tool-node-error" ); @@ -306,6 +307,7 @@ $.extend( Node.prototype, { node = this; this.tool_state = data.tool_state; this.form_html = data.form_html; + this.annotation = data.annotation; this.tool_errors = data.tool_errors; if ( this.tool_errors ) { el.addClass( "tool-node-error" ); @@ -405,7 +407,8 @@ $.extend( Workflow.prototype, { tool_state : node.tool_state, tool_errors : node.tool_errors, input_connections : input_connections, - position : $(node.element).position() + position : $(node.element).position(), + annotation: node.annotation }; nodes[ node.id ] = node_data; }); diff --git a/static/scripts/packed/galaxy.base.js b/static/scripts/packed/galaxy.base.js index 07efface083..f92e85b85a8 100644 --- a/static/scripts/packed/galaxy.base.js +++ b/static/scripts/packed/galaxy.base.js @@ -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}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){$("