details: http://www.bx.psu.edu/hg/galaxy/rev/47b702c583a3 changeset: 3160:47b702c583a3 user: jeremy goecks <jeremy.goecks@emory.edu> date: Tue Dec 08 18:49:06 2009 -0500 description: Added 'annotate history' functionality to pages. To accomplish this, refactored tagging from pure javascript to progressive javascript. Also fixed unicode and grid filtering bugs. diffstat: lib/galaxy/web/base/controller.py | 17 + lib/galaxy/web/controllers/history.py | 50 +- lib/galaxy/web/controllers/page.py | 33 +- lib/galaxy/web/controllers/tag.py | 3 +- lib/galaxy/web/framework/helpers/grids.py | 2 +- static/scripts/autocomplete_tagging.js | 868 +++++++++++++++---------------- static/scripts/jquery.wymeditor.js | 5 +- static/scripts/packed/autocomplete_tagging.js | 2 +- static/wymeditor/lang/en.js | 1 + templates/dataset/edit_attributes.mako | 10 +- templates/history/sharing.mako | 2 +- templates/history/view.mako | 7 +- templates/page/display.mako | 169 ++++++ templates/page/editor.mako | 105 ++- templates/page/history_annotation_table.mako | 59 ++ templates/page/wymiframe.mako | 27 + templates/root/history.mako | 12 +- templates/tagging_common.mako | 98 +++- 18 files changed, 935 insertions(+), 535 deletions(-) diffs (1867 lines): diff -r d95a9c843c53 -r 47b702c583a3 lib/galaxy/web/base/controller.py --- a/lib/galaxy/web/base/controller.py Tue Dec 08 17:05:35 2009 -0500 +++ b/lib/galaxy/web/base/controller.py Tue Dec 08 18:49:06 2009 -0500 @@ -7,6 +7,7 @@ # Pieces of Galaxy to make global in every controller from galaxy import config, tools, web, model, util from galaxy.web import error, form, url_for +from galaxy.model.orm import * from Cheetah.Template import Template @@ -25,6 +26,22 @@ """Returns the application toolbox""" return self.app.toolbox + def get_history( self, trans, id, check_ownership=True ): + """Get a History from the database by id, verifying ownership.""" + # Load history from database + id = trans.security.decode_id( id ) + history = trans.sa_session.query( model.History ).get( id ) + if not history: + err+msg( "History not found" ) + if check_ownership: + # Verify ownership + user = trans.get_user() + if not user: + error( "Must be logged in to manage histories" ) + if history.user != user: + error( "History is not owned by current user" ) + return history + Root = BaseController """ Deprecated: `BaseController` used to be available under the name `Root` diff -r d95a9c843c53 -r 47b702c583a3 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Tue Dec 08 17:05:35 2009 -0500 +++ b/lib/galaxy/web/controllers/history.py Tue Dec 08 18:49:06 2009 -0500 @@ -44,7 +44,7 @@ return "" def get_link( self, trans, grid, item ): if item.users_shared_with or item.importable: - return dict( operation="sharing" ) + return dict( operation="sharing", id=item.id ) return None class DeletedColumn( grids.GridColumn ): @@ -205,7 +205,7 @@ # Load the histories and ensure they all belong to the current user histories = [] for history_id in history_ids: - history = get_history( trans, history_id ) + history = self.get_history( trans, history_id ) if history: # Ensure history is owned by current user if history.user_id != None and trans.user: @@ -237,7 +237,7 @@ history.importable = True elif operation == "disable import via link": if history_ids: - histories = [ get_history( trans, history_id ) for history_id in history_ids ] + histories = [ self.get_history( trans, history_id ) for history_id in history_ids ] for history in histories: if history.importable: history.importable = False @@ -332,7 +332,7 @@ if not ids: message = "Select a history to unshare" return self.shared_list_grid( trans, status='error', message=message, **kwargs ) - histories = [ get_history( trans, history_id ) for history_id in ids ] + histories = [ self.get_history( trans, history_id ) for history_id in ids ] for history in histories: # Current user is the user with which the histories were shared association = trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ).filter_by( user=trans.user, history=history ).one() @@ -375,7 +375,7 @@ @web.require_login( "get history name" ) def get_name_async( self, trans, id=None ): """ Returns the name for a given history. """ - history = get_history( trans, id, False ) + history = self.get_history( trans, id, False ) # To get name: user must own history, history must be importable. if history.user == trans.get_user() or history.importable or trans.get_user() in history.users_shared_with: @@ -386,15 +386,15 @@ @web.require_login( "set history's importable flag" ) def set_importable_async( self, trans, id=None, importable=False ): """ Set history's importable attribute. """ - history = get_history( trans, id, True ) + history = self.get_history( trans, id, True ) # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed. - importable = importable in ['True', 'true', 't']; + importable = importable in ['True', 'true', 't', 'T']; if history and history.importable != importable: history.importable = importable trans.sa_session.flush() - return result + return @web.expose def name_autocomplete_data( self, trans, q=None, limit=None, timestamp=None ): @@ -416,7 +416,7 @@ user_history = trans.get_history() if not id: return trans.show_error_message( "You must specify a history you want to import." ) - import_history = get_history( trans, id, check_ownership=False ) + import_history = self.get_history( trans, id, check_ownership=False ) if not import_history: return trans.show_error_message( "The specified history does not exist.") if not import_history.importable: @@ -470,7 +470,7 @@ # Get history to view. if not id: return trans.show_error_message( "You must specify a history you want to view." ) - history_to_view = get_history( trans, id, False) + history_to_view = self.get_history( trans, id, False) # Integrity checks. if not history_to_view: return trans.show_error_message( "The specified history does not exist." ) @@ -512,7 +512,7 @@ send_to_err = err_msg histories = [] for history_id in id: - histories.append( get_history( trans, history_id ) ) + histories.append( self.get_history( trans, history_id ) ) return trans.fill_template( "/history/share.mako", histories=histories, email=email, @@ -618,7 +618,7 @@ send_to_err = "" histories = [] for history_id in id: - histories.append( get_history( trans, history_id ) ) + histories.append( self.get_history( trans, history_id ) ) send_to_users = [] for email_address in util.listify( email ): email_address = email_address.strip() @@ -776,7 +776,7 @@ if id: ids = util.listify( id ) if ids: - histories = [ get_history( trans, history_id ) for history_id in 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 ): @@ -831,7 +831,7 @@ histories = [] cur_names = [] for history_id in id: - history = get_history( trans, history_id ) + history = self.get_history( trans, history_id ) if history and history.user_id == user.id: histories.append( history ) cur_names.append( history.get_display_name() ) @@ -872,7 +872,7 @@ ids = util.listify( id ) histories = [] for history_id in ids: - history = get_history( trans, history_id, check_ownership=False ) + history = self.get_history( trans, history_id, check_ownership=False ) histories.append( history ) user = trans.get_user() for history in histories: @@ -896,22 +896,4 @@ msg = 'Clone with name "%s" is now included in your previously stored histories.' % new_history.name else: msg = '%d cloned histories are now included in your previously stored histories.' % len( histories ) - return trans.show_ok_message( msg ) - -## ---- Utility methods ------------------------------------------------------- - -def get_history( trans, id, check_ownership=True ): - """Get a History from the database by id, verifying ownership.""" - # Load history from database - id = trans.security.decode_id( id ) - history = trans.sa_session.query( model.History ).get( id ) - if not history: - err+msg( "History not found" ) - if check_ownership: - # Verify ownership - user = trans.get_user() - if not user: - error( "Must be logged in to manage histories" ) - if history.user != user: - error( "History is not owned by current user" ) - return history + return trans.show_ok_message( msg ) \ No newline at end of file diff -r d95a9c843c53 -r 47b702c583a3 lib/galaxy/web/controllers/page.py --- a/lib/galaxy/web/controllers/page.py Tue Dec 08 17:05:35 2009 -0500 +++ b/lib/galaxy/web/controllers/page.py Tue Dec 08 18:49:06 2009 -0500 @@ -315,5 +315,36 @@ @web.expose @web.require_login("select a history from saved histories") def list_histories_for_selection( self, trans, **kwargs ): + """ Returns HTML that enables a user to select one or more histories. """ # Render the list view - return self._history_selection_grid( trans, **kwargs ) \ No newline at end of file + return self._history_selection_grid( trans, **kwargs ) + + @web.expose + @web.require_login("get annotation table for history") + def get_history_annotation_table( self, trans, id ): + """ Returns HTML for an annotation table for a history. """ + + # TODO: users should be able to annotate a history if they own it, it is importable, or it is shared with them. This only + # returns a history if a user owns it. + history = self.get_history( trans, id, True ) + + if history: + # TODO: Query taken from root/history; it should be moved either into history or trans object + # so that it can reused. + 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" ) ) \ + .order_by( model.HistoryDatasetAssociation.hid ) + # For now, do not show deleted datasets. + show_deleted = False + if not show_deleted: + query = query.filter( model.HistoryDatasetAssociation.deleted == False ) + return trans.fill_template( "page/history_annotation_table.mako", history=history, datasets=query.all(), show_deleted=False ) + + @web.expose + def get_editor_iframe( self, trans ): + """ Returns the document for the page editor's iframe. """ + return trans.fill_template( "page/wymiframe.mako" ) + \ No newline at end of file diff -r d95a9c843c53 -r 47b702c583a3 lib/galaxy/web/controllers/tag.py --- a/lib/galaxy/web/controllers/tag.py Tue Dec 08 17:05:35 2009 -0500 +++ b/lib/galaxy/web/controllers/tag.py Tue Dec 08 18:49:06 2009 -0500 @@ -178,8 +178,9 @@ # Create and return autocomplete data. ac_data = "#Header|Your Values for '%s'\n" % (tag_name) + tag_uname = self._get_usernames_for_tag(trans.sa_session, trans.get_user(), tag, item_class, item_tag_assoc_class)[0] for row in result_set: - ac_data += tag.name + ":" + row[0] + "|" + row[0] + "\n" + ac_data += tag_uname + ":" + row[0] + "|" + row[0] + "\n" return ac_data def _get_usernames_for_tag(self, db_session, user, tag, item_class, item_tag_assoc_class): diff -r d95a9c843c53 -r 47b702c583a3 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Tue Dec 08 17:05:35 2009 -0500 +++ b/lib/galaxy/web/framework/helpers/grids.py Tue Dec 08 18:49:06 2009 -0500 @@ -360,7 +360,7 @@ elt_id="tagging-elt" + str( self.tag_elt_id_gen ) div_elt = "<div id=%s></div>" % elt_id return div_elt + trans.fill_template( "/tagging_common.mako", trans=trans, tagged_item=item, elt_context=self.grid_name, - elt_id = elt_id, in_form="true", input_size="20", tag_click_fn="add_tag_to_grid_filter" ) + elt_id = elt_id, in_form=True, input_size="20", tag_click_fn="add_tag_to_grid_filter" ) def filter( self, db_session, query, column_filter ): """ Modify query to filter model_class by tag. Multiple filters are ANDed. """ if column_filter == "All": diff -r d95a9c843c53 -r 47b702c583a3 static/scripts/autocomplete_tagging.js --- a/static/scripts/autocomplete_tagging.js Tue Dec 08 17:05:35 2009 -0500 +++ b/static/scripts/autocomplete_tagging.js Tue Dec 08 18:49:06 2009 -0500 @@ -1,486 +1,458 @@ /** - * JQuery extension for tagging with autocomplete. - * @author: Jeremy Goecks - * @require: jquery.autocomplete plugin - */ -var ac_tag_area_id_gen = 1; +* JQuery extension for tagging with autocomplete. +* @author: Jeremy Goecks +* @require: jquery.autocomplete plugin +*/ +jQuery.fn.autocomplete_tagging = function(elt_id, options) +{ -jQuery.fn.autocomplete_tagging = function(options) { + // + // Set up function defaults. + // + var defaults = + { + get_toggle_link_text_fn: function(tags) + { + var text = ""; + var num_tags = array_length(tags); + if (num_tags != 0) + text = num_tags + (num_tags != 0 ? " Tags" : " Tag"); + else + // No tags. + text = "Add tags"; + return text; + }, + tag_click_fn : function (name, value) { }, + editable: true, + input_size: 20, + in_form: false, + tags : {}, + use_toggle_link: true, + item_id: "", + add_tag_img: "", + add_tag_img_rollover: "", + delete_tag_img: "", + ajax_autocomplete_tag_url: "", + ajax_retag_url: "", + ajax_delete_tag_url: "", + ajax_add_tag_url: "" + }; - // - // Set up function defaults. - // - var defaults = - { - get_toggle_link_text_fn: function(tags) - { - var text = ""; - var num_tags = array_length(tags); - if (num_tags != 0) - text = num_tags + (num_tags != 0 ? " Tags" : " Tag"); - else - // No tags. - text = "Add tags"; - return text; - }, - tag_click_fn : function (name, value) { }, - editable: true, - input_size: 20, - in_form: false, - tags : {}, - use_toggle_link: true, - item_id: "", - add_tag_img: "", - add_tag_img_rollover: "", - delete_tag_img: "", - ajax_autocomplete_tag_url: "", - ajax_retag_url: "", - ajax_delete_tag_url: "", - ajax_add_tag_url: "" - }; + // + // Extend object. + // + var settings = jQuery.extend(defaults, options); - // - // Extend object. - // - var settings = jQuery.extend(defaults, options); - - // - // Create core elements: tag area and TODO. - // - - // Tag area. - var area_id = "tag-area-" + (ac_tag_area_id_gen)++; - var tag_area = $("<div>").attr("id", area_id).addClass("tag-area"); - this.append(tag_area); - - // - // Returns the number of keys (elements) in an array/dictionary. - // - var array_length = function(an_array) - { - if (an_array.length) - return an_array.length; + // + // Returns the number of keys (elements) in an array/dictionary. + // + var array_length = function(an_array) + { + if (an_array.length) + return an_array.length; - var count = 0; - for (element in an_array) - count++; - return count; - }; - - // - // Function to build toggle link. - // - var build_toggle_link = function() - { - var link_text = settings.get_toggle_link_text_fn(settings.tags); - var toggle_link = $("<a href='/history/tags'>").text(link_text).addClass("toggle-link"); - // Link toggles the display state of the tag area. - toggle_link.click( function() - { - // Take special actions depending on whether toggle is showing or hiding link. - var showing_tag_area = (tag_area.css("display") == "none"); - var after_toggle_fn; - if (showing_tag_area) - { - after_toggle_fn = function() - { - // If there are no tags, go right to editing mode by generating a - // click on the area. - var num_tags = array_length(settings.tags); - if (num_tags == 0) - tag_area.click(); - }; - } - else // Hiding area. - { - after_toggle_fn = function() - { - tag_area.blur(); - }; - } - tag_area.slideToggle("fast", after_toggle_fn); - - return false; - }); - - return toggle_link; - }; - - // Add toggle link. - var toggle_link = build_toggle_link(); - if (settings.use_toggle_link) - { - this.prepend(toggle_link); - } - - // - // Function to build other elements. - // + var count = 0; + for (element in an_array) + count++; + return count; + }; - // - // Return a string that contains the contents of an associative array. This is - // a debugging method. - // - var assoc_array_to_str = function(an_array) - { - // Convert associative array to simple array and then join array elements. - var array_str_list = new Array(); - for (key in an_array) - array_str_list[array_str_list.length] = key + "-->" + an_array[key]; - - return "{" + array_str_list.join(",") + "}" - }; + // + // Initalize object's elements. + // - // - // Collapse tag name + value into a single string. - // - var build_tag_str = function(tag_name, tag_value) - { - return tag_name + ( (tag_value != "" && tag_value) ? ":" + tag_value : ""); - }; - - // - // Get tag name and value from a string. - // - var get_tag_name_and_value = function(tag_str) - { - return tag_str.split(":"); - }; - - // - // Add "add tag" button. - // - var build_add_tag_button = function(tag_input_field) - { - var add_tag_button = $("<img src='" + settings.add_tag_img + "' rollover='" + settings.add_tag_img_rollover + "'/>").addClass("add-tag-button"); - - add_tag_button.click( function() - { - // Hide button. - $(this).hide(); - - // Clicking on button is the same as clicking on the tag area. - tag_area.click(); - - return false; + // Get elements for this object. + var this_obj = $('#' + elt_id); + var id_parts = $(this).attr('id').split("-"); + var obj_id = id_parts[ id_parts.length-1 ]; + var tag_area = this_obj.find('#tag-area-' + obj_id); + var toggle_link = this_obj.find('#toggle-link-' + obj_id); + var tag_input_field = this_obj.find('#tag-input'); + var add_tag_button = this_obj.find('.add-tag-button'); + + // Initialize toggle link. + toggle_link.click( function() { + var id = $(this).attr('id').split('-')[2]; + + // Take special actions depending on whether toggle is showing or hiding link. + var tag_area = $('#tag-area-' + id); + var showing_tag_area = (tag_area.css("display") == "none"); + var after_toggle_fn; + if (showing_tag_area) + { + after_toggle_fn = function() + { + // If there are no tags, go right to editing mode by generating a + // click on the area. + var num_tags = $(this).find('.tag-button').length; + if (num_tags == 0) + tag_area.click(); + }; + } + else // Hiding area. + { + after_toggle_fn = function() + { + tag_area.blur(); + }; + } + tag_area.slideToggle("fast", after_toggle_fn); + + return $(this); }); - return add_tag_button; - }; + // Initialize tag input field. + if (settings.editable) + tag_input_field.hide(); + tag_input_field.keyup( function( e ) + { + if ( e.keyCode == 27 ) + { + // Escape key + $(this).trigger( "blur" ); + } + else if ( + ( e.keyCode == 13 ) || // Return Key + ( e.keyCode == 188 ) || // Comma + ( e.keyCode == 32 ) // Space + ) + { + // + // Check input. + // - // - // Function that builds a tag button. - // - var build_tag_button = function(tag_str) - { - // Build "delete tag" image and handler. - var delete_img = $("<img src='" + settings.delete_tag_img + "'/>").addClass("delete-tag-img"); - delete_img.mouseenter( function () - { - $(this).attr("src", settings.delete_tag_img_rollover); - }); - delete_img.mouseleave( function () - { - $(this).attr("src", settings.delete_tag_img); - }); - delete_img.click( function () - { - // Tag button is image's parent. - var tag_button = $(this).parent(); - - // Get tag name, value. - var tag_name_elt = tag_button.find(".tag-name").eq(0); - var tag_str = tag_name_elt.text(); - var tag_name_and_value = get_tag_name_and_value(tag_str); - var tag_name = tag_name_and_value[0]; - var tag_value = tag_name_and_value[1]; + new_value = this.value; - var prev_button = tag_button.prev(); - tag_button.remove(); + // Do nothing if return key was used to autocomplete. + if (return_key_pressed_for_autocomplete == true) + { + return_key_pressed_for_autocomplete = false; + return false; + } - // Remove tag from local list for consistency. - delete settings.tags[tag_name]; - - // Update toggle link text. - var new_text = settings.get_toggle_link_text_fn(settings.tags); - toggle_link.text(new_text); + // Suppress space after a ":" + if ( new_value.indexOf(": ", new_value.length - 2) != -1) + { + this.value = new_value.substring(0, new_value.length-1); + return false; + } - // Delete tag. - $.ajax({ - url: settings.ajax_delete_tag_url, - data: { tag_name: tag_name }, - error: function() - { - // Failed. Roll back changes and show alert. - settings.tags[tag_name] = tag_value; - if (prev_button.hasClass("tag-button")) - prev_button.after(tag_button); - else - tag_area.prepend(tag_button); - var new_text = settings.get_toggle_link_text_fn(settings.tags); - alert( "Remove tag failed" ); - - toggle_link.text(new_text); - - // TODO: no idea why it's necessary to set this up again. - delete_img.mouseenter( function () - { - $(this).attr("src", settings.delete_tag_img_rollover); - }); - delete_img.mouseleave( function () - { - $(this).attr("src", settings.delete_tag_img); - }); - }, - success: function() {} - }); + // Remove trigger keys from input. + if ( (e.keyCode == 188) || (e.keyCode == 32) ) + new_value = new_value.substring( 0 , new_value.length - 1 ); - return true; - }); + // Trim whitespace. + new_value = new_value.replace(/^\s+|\s+$/g,""); - // Build tag button. - var tag_name_elt = $("<span>").text(tag_str).addClass("tag-name"); - tag_name_elt.click( function() - { - tag_name_and_value = tag_str.split(":") - settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]); - return true; - }); + // Too short? + if (new_value.length < 2) + return false; - var tag_button = $("<span></span>").addClass("tag-button"); - tag_button.append(tag_name_elt); - // Allow delete only if element is editable. - if (settings.editable) - tag_button.append(delete_img); + // + // New tag OK - apply it. + // - return tag_button; - }; + this.value = ""; - // - // Build input + autocompete for tag. - // - var build_tag_input = function(tag_text) - { - // If element is in form, tag input is a textarea; otherwise element is a input type=text. - var t; - if (settings.in_form) - t = $( "<textarea id='history-tag-input' rows='1' cols='" + - settings.input_size + "' value='" + escape(tag_text) + "'></textarea>" ); - else // element not in form. - t = $( "<input id='history-tag-input' type='text' size='" + - settings.input_size + "' value='" + escape(tag_text) + "'></input>" ); - t.keyup( function( e ) - { - if ( e.keyCode == 27 ) - { - // Escape key - $(this).trigger( "blur" ); - } else if ( - ( e.keyCode == 13 ) || // Return Key - ( e.keyCode == 188 ) || // Comma - ( e.keyCode == 32 ) // Space - ) - { - // - // Check input. - // - - new_value = this.value; - - // Do nothing if return key was used to autocomplete. - if (return_key_pressed_for_autocomplete == true) - { - return_key_pressed_for_autocomplete = false; - return false; - } - - // Suppress space after a ":" - if ( new_value.indexOf(": ", new_value.length - 2) != -1) - { - this.value = new_value.substring(0, new_value.length-1); - return false; - } - - // Remove trigger keys from input. - if ( (e.keyCode == 188) || (e.keyCode == 32) ) - new_value = new_value.substring( 0 , new_value.length - 1 ); - - // Trim whitespace. - new_value = new_value.replace(/^\s+|\s+$/g,""); - - // Too short? - if (new_value.length < 2) - return false; - - // - // New tag OK - apply it. - // - - this.value = ""; - - // Add button for tag after all other tag buttons. - var new_tag_button = build_tag_button(new_value); - var tag_buttons = tag_area.children(".tag-button"); - if (tag_buttons.length != 0) - { - var last_tag_button = tag_buttons.slice(tag_buttons.length-1); - last_tag_button.after(new_tag_button); - } - else - tag_area.prepend(new_tag_button); + // Add button for tag after all other tag buttons. + var new_tag_button = build_tag_button(new_value); + var tag_buttons = tag_area.children(".tag-button"); + if (tag_buttons.length != 0) + { + var last_tag_button = tag_buttons.slice(tag_buttons.length-1); + last_tag_button.after(new_tag_button); + } + else + tag_area.prepend(new_tag_button); - // Add tag to internal list. - var tag_name_and_value = new_value.split(":"); - settings.tags[tag_name_and_value[0]] = tag_name_and_value[1]; - - // Update toggle link text. - var new_text = settings.get_toggle_link_text_fn(settings.tags); - toggle_link.text(new_text); + // Add tag to internal list. + var tag_name_and_value = new_value.split(":"); + settings.tags[tag_name_and_value[0]] = tag_name_and_value[1]; - // Commit tag to server. - var $this = $(this); - $.ajax({ - url: settings.ajax_add_tag_url, - data: { new_tag: new_value }, - error: function() - { - // Failed. Roll back changes and show alert. - new_tag_button.remove(); - delete settings.tags[tag_name_and_value[0]]; - var new_text = settings.get_toggle_link_text_fn(settings.tags); - toggle_link.text(new_text); - alert( "Add tag failed" ); - }, - success: function() - { - // Flush autocomplete cache because it's not out of date. - // TODO: in the future, we could remove the particular item - // that was chosen from the cache rather than flush it. - $this.flushCache(); - } - }); - - return false; - } + // Update toggle link text. + var new_text = settings.get_toggle_link_text_fn(settings.tags); + toggle_link.text(new_text); + + // Commit tag to server. + var $this = $(this); + $.ajax({ + url: settings.ajax_add_tag_url, + data: { new_tag: new_value }, + error: function() + { + // Failed. Roll back changes and show alert. + new_tag_button.remove(); + delete settings.tags[tag_name_and_value[0]]; + var new_text = settings.get_toggle_link_text_fn(settings.tags); + toggle_link.text(new_text); + alert( "Add tag failed" ); + }, + success: function() + { + // Flush autocomplete cache because it's not out of date. + // TODO: in the future, we could remove the particular item + // that was chosen from the cache rather than flush it. + $this.flushCache(); + } + }); + + return false; + } }); // Add autocomplete to input. - var format_item_func = function(key, row_position, num_rows, value, search_term) { - tag_name_and_value = value.split(":"); - return (tag_name_and_value.length == 1 ? tag_name_and_value[0] :tag_name_and_value[1]); - //var array = new Array(key, value, row_position, num_rows, - //search_term ); return "\"" + array.join("*") + "\""; + var format_item_func = function(key, row_position, num_rows, value, search_term) + { + tag_name_and_value = value.split(":"); + return (tag_name_and_value.length == 1 ? tag_name_and_value[0] :tag_name_and_value[1]); + //var array = new Array(key, value, row_position, num_rows, + //search_term ); return "\"" + array.join("*") + "\""; } var autocomplete_options = - { selectFirst: false, formatItem : format_item_func, autoFill: false, highlight: false }; - - t.autocomplete(settings.ajax_autocomplete_tag_url, autocomplete_options); + { selectFirst: false, formatItem : format_item_func, autoFill: false, highlight: false }; + tag_input_field.autocomplete(settings.ajax_autocomplete_tag_url, autocomplete_options); + + + // Initialize delete tag images for current tags. + this_obj.find('.delete-tag-img').each(function() { + init_delete_tag_image( $(this) ); + }); - t.addClass("tag-input"); + this_obj.find('.tag-name').each( function() { + $(this).click( function() { + var tag_str = $(this).text(); + var tag_name_and_value = tag_str.split(":") + settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]); + return true; + }); + }); - return t; - }; - - // - // Build tag area. - // - // Add tag buttons for each current tag to the tag area. - for (tag_name in settings.tags) + + // Initialize "add tag" button. + add_tag_button.click( function() { - var tag_value = settings.tags[tag_name]; - var tag_str = build_tag_str(tag_name, tag_value); - var tag_button = build_tag_button(tag_str, toggle_link, settings.tags); - tag_area.append(tag_button); + // Hide button. + $(this).hide(); + + // Clicking on button is the same as clicking on the tag area. + tag_area.click(); + + return false; + }); + + // + // Set up tag area interactions; these are needed only if tags are editable. + // + if (settings.editable) + { + // When the tag area blurs, go to "view tag" mode. + tag_area.blur( function(e) + { + num_tags = array_length(settings.tags); + if (num_tags != 0) + { + add_tag_button.show(); + tag_input_field.hide(); + tag_area.removeClass("active-tag-area"); + } + else + { + // No tags, so do nothing to ensure that input is still visible. + } + }); + + // On click, enable user to add tags. + tag_area.click( function(e) + { + var is_active = $(this).hasClass("active-tag-area"); + + // If a "delete image" object was pressed and area is inactive, do nothing. + if ($(e.target).hasClass("delete-tag-img") && !is_active) + return false; + + // If a "tag name" object was pressed and area is inactive, do nothing. + if ($(e.target).hasClass("tag-name") && !is_active) + return false; + + // Hide add tag button, show tag_input field. Change background to show + // area is active. + $(this).addClass("active-tag-area"); + add_tag_button.hide(); + tag_input_field.show(); + tag_input_field.focus(); + + // Add handler to document that will call blur when the tag area is blurred; + // a tag area is blurred when a user clicks on an element outside the area. + var handle_document_click = function(e) + { + var tag_area_id = tag_area.attr("id"); + // Blur the tag area if the element clicked on is not in the tag area. + if ( + ($(e.target).attr("id") != tag_area_id) && + ($(e.target).parents().filter(tag_area_id).length == 0) + ) + { + tag_area.blur(); + $(document).unbind("click", handle_document_click); + } + }; + // TODO: we should attach the click handler to all frames in order to capture + // clicks outside the frame that this element is in. + //window.parent.document.onclick = handle_document_click; + //var temp = $(window.parent.document.body).contents().find("iframe").html(); + //alert(temp); + //$(document).parent().click(handle_document_click); + $(window).click(handle_document_click); + + return false; + }); } - - // Add tag input field and "add tag" button. - var tag_input_field = build_tag_input(""); - var add_tag_button = build_add_tag_button(tag_input_field); - // When the tag area blurs, go to "view tag" mode. - tag_area.blur( function(e) - { - num_tags = array_length(settings.tags); - if (num_tags != 0) + // If using toggle link, hide the tag area. Otherwise, show the tag area. + if (settings.use_toggle_link) + tag_area.hide(); + else { - add_tag_button.show(); - tag_input_field.hide(); - tag_area.removeClass("active-tag-area"); + var num_tags = array_length(settings.tags); + if (num_tags == 0) + { + add_tag_button.hide(); + tag_input_field.show(); + } } - else + + // Initialize tag names. + //$('.tag-name'). + + // + // Helper functions. + // + + // + // Collapse tag name + value into a single string. + // + function build_tag_str(tag_name, tag_value) { - // No tags, so do nothing to ensure that input is still visible. - } - }); - - if (settings.editable) - { - tag_area.append(add_tag_button); - tag_area.append(tag_input_field); - tag_input_field.hide(); - - // On click, enable user to add tags. - tag_area.click( function(e) - { - var is_active = $(this).hasClass("active-tag-area"); + return tag_name + ( (tag_value != "" && tag_value) ? ":" + tag_value : ""); + }; - // If a "delete image" object was pressed and area is inactive, do nothing. - if ($(e.target).hasClass("delete-tag-img") && !is_active) - return false; - - // If a "tag name" object was pressed and area is inactive, do nothing. - if ($(e.target).hasClass("tag-name") && !is_active) - return false; - // Hide add tag button, show tag_input field. Change background to show - // area is active. - $(this).addClass("active-tag-area"); - add_tag_button.hide(); - tag_input_field.show(); - tag_input_field.focus(); + // Initialize a "delete tag image": when click, delete tag from UI and send delete request to server. + function init_delete_tag_image(delete_img) + { + $(delete_img).mouseenter( function () + { + $(this).attr("src", settings.delete_tag_img_rollover); + }); + $(delete_img).mouseleave( function () + { + $(this).attr("src", settings.delete_tag_img); + }); + $(delete_img).click( function () + { + // Tag button is image's parent. + var tag_button = $(this).parent(); - // Add handler to document that will call blur when the tag area is blurred; - // a tag area is blurred when a user clicks on an element outside the area. - var handle_document_click = function(e) - { - var tag_area_id = tag_area.attr("id"); - // Blur the tag area if the element clicked on is not in the tag area. - if ( - ($(e.target).attr("id") != tag_area_id) && - ($(e.target).parents().filter(tag_area_id).length == 0) - ) - { - tag_area.blur(); - $(document).unbind("click", handle_document_click); - } - }; - // TODO: we should attach the click handler to all frames in order to capture - // clicks outside the frame that this element is in. - //window.parent.document.onclick = handle_document_click; - //var temp = $(window.parent.document.body).contents().find("iframe").html(); - //alert(temp); - //$(document).parent().click(handle_document_click); - $(window).click(handle_document_click); - - return false; - }); - } - - // If using toggle link, hide the tag area. Otherwise, if there are no tags, - // hide the "add tags" button and show the input field. - if (settings.use_toggle_link) - tag_area.hide(); - else + // Get tag name, value. + var tag_name_elt = tag_button.find(".tag-name").eq(0); + var tag_str = tag_name_elt.text(); + var tag_name_and_value = get_tag_name_and_value(tag_str); + var tag_name = tag_name_and_value[0]; + var tag_value = tag_name_and_value[1]; + + var prev_button = tag_button.prev(); + tag_button.remove(); + + // Remove tag from local list for consistency. + delete settings.tags[tag_name]; + + // Update toggle link text. + var new_text = settings.get_toggle_link_text_fn(settings.tags); + toggle_link.text(new_text); + + // Delete tag. + $.ajax({ + url: settings.ajax_delete_tag_url, + data: { tag_name: tag_name }, + error: function() + { + // Failed. Roll back changes and show alert. + settings.tags[tag_name] = tag_value; + if (prev_button.hasClass("tag-button")) + prev_button.after(tag_button); + else + tag_area.prepend(tag_button); + var new_text = settings.get_toggle_link_text_fn(settings.tags); + alert( "Remove tag failed" ); + + toggle_link.text(new_text); + + // TODO: no idea why it's necessary to set this up again. + delete_img.mouseenter( function () + { + $(this).attr("src", settings.delete_tag_img_rollover); + }); + delete_img.mouseleave( function () + { + $(this).attr("src", settings.delete_tag_img); + }); + }, + success: function() {} + }); + + return true; + }); + }; + + + // + // Return a string that contains the contents of an associative array. This is + // a debugging method. + // + function assoc_array_to_str(an_array) { - var num_tags = array_length(settings.tags); - if (num_tags == 0) - { - add_tag_button.hide(); - tag_input_field.show(); - } - } - - - return this.addClass("tag-element"); -} + // Convert associative array to simple array and then join array elements. + var array_str_list = new Array(); + for (key in an_array) + array_str_list[array_str_list.length] = key + "-->" + an_array[key]; + + return "{" + array_str_list.join(",") + "}" + }; + + // + // Get tag name and value from a string. + // + function get_tag_name_and_value(tag_str) + { + return tag_str.split(":"); + }; + + // + // Function that builds a tag button. + // + function build_tag_button(tag_str) + { + // Build "delete tag" image. + var delete_img = $("<img src='" + settings.delete_tag_img + "'/>").addClass("delete-tag-img"); + init_delete_tag_image(delete_img); + + // Build tag button. + var tag_name_elt = $("<span>").text(tag_str).addClass("tag-name"); + tag_name_elt.click( function() + { + tag_name_and_value = tag_str.split(":") + settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]); + return true; + }); + + var tag_button = $("<span></span>").addClass("tag-button"); + tag_button.append(tag_name_elt); + // Allow delete only if element is editable. + if (settings.editable) + tag_button.append(delete_img); + + return tag_button; + }; + +}; diff -r d95a9c843c53 -r 47b702c583a3 static/scripts/jquery.wymeditor.js --- a/static/scripts/jquery.wymeditor.js Tue Dec 08 17:05:35 2009 -0500 +++ b/static/scripts/jquery.wymeditor.js Tue Dec 08 18:49:06 2009 -0500 @@ -370,8 +370,9 @@ iframeHtml:"<div class='wym_iframe wym_section'>" + "<iframe " + "src='" - + WYMeditor.IFRAME_BASE_PATH - + "wymiframe.html' " + + "/page/get_editor_iframe' " + //+ WYMeditor.IFRAME_BASE_PATH + //+ "wymiframe.html' " + "onload='this.contentWindow.parent.WYMeditor.INSTANCES[" + WYMeditor.INDEX + "].initIframe(this)'" + "></iframe>" diff -r d95a9c843c53 -r 47b702c583a3 static/scripts/packed/autocomplete_tagging.js --- a/static/scripts/packed/autocomplete_tagging.js Tue Dec 08 17:05:35 2009 -0500 +++ b/static/scripts/packed/autocomplete_tagging.js Tue Dec 08 18:49:06 2009 -0500 @@ -1,1 +1,1 @@ -var ac_tag_area_id_gen=1;jQuery.fn.autocomplete_tagging=function(c){var e={get_toggle_link_text_fn:function(u){var w="";var v=o(u);if(v!=0){w=v+(v!=0?" Tags":" Tag")}else{w="Add tags"}return w},tag_click_fn:function(u,v){},editable:true,input_size:20,in_form:false,tags:{},use_toggle_link:true,item_id:"",add_tag_img:"",add_tag_img_rollover:"",delete_tag_img:"",ajax_autocomplete_tag_url:"",ajax_retag_url:"",ajax_delete_tag_url:"",ajax_add_tag_url:""};var p=jQuery.extend(e,c);var k="tag-area-"+(ac_tag_area_id_gen)++;var m=$("<div>").attr("id",k).addClass("tag-area");this.append(m);var o=function(u){if(u.length){return u.length}var v=0;for(element in u){v++}return v};var b=function(){var u=p.get_toggle_link_text_fn(p.tags);var v=$("<a href='/history/tags'>").text(u).addClass("toggle-link");v.click(function(){var w=(m.css("display")=="none");var x;if(w){x=function(){var y=o(p.tags);if(y==0){m.click()}}}else{x=function(){m.blur()}}m.slideToggle("fast",x);return false});return v};v ar s=b();if(p.use_toggle_link){this.prepend(s)}var t=function(u){var v=new Array();for(key in u){v[v.length]=key+"-->"+u[key]}return"{"+v.join(",")+"}"};var a=function(v,u){return v+((u!=""&&u)?":"+u:"")};var h=function(u){return u.split(":")};var i=function(u){var v=$("<img src='"+p.add_tag_img+"' rollover='"+p.add_tag_img_rollover+"'/>").addClass("add-tag-button");v.click(function(){$(this).hide();m.click();return false});return v};var j=function(u){var v=$("<img src='"+p.delete_tag_img+"'/>").addClass("delete-tag-img");v.mouseenter(function(){$(this).attr("src",p.delete_tag_img_rollover)});v.mouseleave(function(){$(this).attr("src",p.delete_tag_img)});v.click(function(){var D=$(this).parent();var C=D.find(".tag-name").eq(0);var B=C.text();var z=h(B);var F=z[0];var y=z[1];var E=D.prev();D.remove();delete p.tags[F];var A=p.get_toggle_link_text_fn(p.tags);s.text(A);$.ajax({url:p.ajax_delete_tag_url,data:{tag_name:F},error:function(){p.tags[F]=y;if(E.hasClass("tag-button")){E .after(D)}else{m.prepend(D)}var G=p.get_toggle_link_text_fn(p.tags);alert("Remove tag failed");s.text(G);v.mouseenter(function(){$(this).attr("src",p.delete_tag_img_rollover)});v.mouseleave(function(){$(this).attr("src",p.delete_tag_img)})},success:function(){}});return true});var w=$("<span>").text(u).addClass("tag-name");w.click(function(){tag_name_and_value=u.split(":");p.tag_click_fn(tag_name_and_value[0],tag_name_and_value[1]);return true});var x=$("<span></span>").addClass("tag-button");x.append(w);if(p.editable){x.append(v)}return x};var d=function(v){var u;if(p.in_form){u=$("<textarea id='history-tag-input' rows='1' cols='"+p.input_size+"' value='"+escape(v)+"'></textarea>")}else{u=$("<input id='history-tag-input' type='text' size='"+p.input_size+"' value='"+escape(v)+"'></input>")}u.keyup(function(D){if(D.keyCode==27){$(this).trigger("blur")}else{if((D.keyCode==13)||(D.keyCode==188)||(D.keyCode==32)){new_value=this.value;if(return_key_pressed_for_autocomplete==true) {return_key_pressed_for_autocomplete=false;return false}if(new_value.indexOf(": ",new_value.length-2)!=-1){this.value=new_value.substring(0,new_value.length-1);return false}if((D.keyCode==188)||(D.keyCode==32)){new_value=new_value.substring(0,new_value.length-1)}new_value=new_value.replace(/^\s+|\s+$/g,"");if(new_value.length<2){return false}this.value="";var A=j(new_value);var z=m.children(".tag-button");if(z.length!=0){var E=z.slice(z.length-1);E.after(A)}else{m.prepend(A)}var y=new_value.split(":");p.tags[y[0]]=y[1];var B=p.get_toggle_link_text_fn(p.tags);s.text(B);var C=$(this);$.ajax({url:p.ajax_add_tag_url,data:{new_tag:new_value},error:function(){A.remove();delete p.tags[y[0]];var F=p.get_toggle_link_text_fn(p.tags);s.text(F);alert("Add tag failed")},success:function(){C.flushCache()}});return false}}});var w=function(A,z,y,C,B){tag_name_and_value=C.split(":");return(tag_name_and_value.length==1?tag_name_and_value[0]:tag_name_and_value[1])};var x={selectFirst:false,fo rmatItem:w,autoFill:false,highlight:false};u.autocomplete(p.ajax_autocomplete_tag_url,x);u.addClass("tag-input");return u};for(tag_name in p.tags){var q=p.tags[tag_name];var l=a(tag_name,q);var g=j(l,s,p.tags);m.append(g)}var n=d("");var f=i(n);m.blur(function(u){r=o(p.tags);if(r!=0){f.show();n.hide();m.removeClass("active-tag-area")}else{}});if(p.editable){m.append(f);m.append(n);n.hide();m.click(function(w){var v=$(this).hasClass("active-tag-area");if($(w.target).hasClass("delete-tag-img")&&!v){return false}if($(w.target).hasClass("tag-name")&&!v){return false}$(this).addClass("active-tag-area");f.hide();n.show();n.focus();var u=function(y){var x=m.attr("id");if(($(y.target).attr("id")!=x)&&($(y.target).parents().filter(x).length==0)){m.blur();$(document).unbind("click",u)}};$(window).click(u);return false})}if(p.use_toggle_link){m.hide()}else{var r=o(p.tags);if(r==0){f.hide();n.show()}}return this.addClass("tag-element")}; \ No newline at end of file +jQuery.fn.autocomplete_tagging=function(f,d){var g={get_toggle_link_text_fn:function(u){var w="";var v=o(u);if(v!=0){w=v+(v!=0?" Tags":" Tag")}else{w="Add tags"}return w},tag_click_fn:function(u,v){},editable:true,input_size:20,in_form:false,tags:{},use_toggle_link:true,item_id:"",add_tag_img:"",add_tag_img_rollover:"",delete_tag_img:"",ajax_autocomplete_tag_url:"",ajax_retag_url:"",ajax_delete_tag_url:"",ajax_add_tag_url:""};var q=jQuery.extend(g,d);var o=function(u){if(u.length){return u.length}var v=0;for(element in u){v++}return v};var e=$("#"+f);var m=$(this).attr("id").split("-");var a=m[m.length-1];var l=e.find("#tag-area-"+a);var s=e.find("#toggle-link-"+a);var n=e.find("#tag-input");var h=e.find(".add-tag-button");s.click(function(){var x=$(this).attr("id").split("-")[2];var v=$("#tag-area-"+x);var u=(v.css("display")=="none");var w;if(u){w=function(){var y=$(this).find(".tag-button").length;if(y==0){v.click()}}}else{w=function(){v.blur()}}v.slideToggle("fast",w);re turn $(this)});if(q.editable){n.hide()}n.keyup(function(z){if(z.keyCode==27){$(this).trigger("blur")}else{if((z.keyCode==13)||(z.keyCode==188)||(z.keyCode==32)){new_value=this.value;if(return_key_pressed_for_autocomplete==true){return_key_pressed_for_autocomplete=false;return false}if(new_value.indexOf(": ",new_value.length-2)!=-1){this.value=new_value.substring(0,new_value.length-1);return false}if((z.keyCode==188)||(z.keyCode==32)){new_value=new_value.substring(0,new_value.length-1)}new_value=new_value.replace(/^\s+|\s+$/g,"");if(new_value.length<2){return false}this.value="";var w=j(new_value);var v=l.children(".tag-button");if(v.length!=0){var A=v.slice(v.length-1);A.after(w)}else{l.prepend(w)}var u=new_value.split(":");q.tags[u[0]]=u[1];var x=q.get_toggle_link_text_fn(q.tags);s.text(x);var y=$(this);$.ajax({url:q.ajax_add_tag_url,data:{new_tag:new_value},error:function(){w.remove();delete q.tags[u[0]];var B=q.get_toggle_link_text_fn(q.tags);s.text(B);alert("Add tag fail ed")},success:function(){y.flushCache()}});return false}}});var c=function(w,v,u,y,x){tag_name_and_value=y.split(":");return(tag_name_and_value.length==1?tag_name_and_value[0]:tag_name_and_value[1])};var k={selectFirst:false,formatItem:c,autoFill:false,highlight:false};n.autocomplete(q.ajax_autocomplete_tag_url,k);e.find(".delete-tag-img").each(function(){p($(this))});e.find(".tag-name").each(function(){$(this).click(function(){var v=$(this).text();var u=v.split(":");q.tag_click_fn(u[0],u[1]);return true})});h.click(function(){$(this).hide();l.click();return false});if(q.editable){l.blur(function(u){r=o(q.tags);if(r!=0){h.show();n.hide();l.removeClass("active-tag-area")}else{}});l.click(function(w){var v=$(this).hasClass("active-tag-area");if($(w.target).hasClass("delete-tag-img")&&!v){return false}if($(w.target).hasClass("tag-name")&&!v){return false}$(this).addClass("active-tag-area");h.hide();n.show();n.focus();var u=function(y){var x=l.attr("id");if(($(y.target).attr("id ")!=x)&&($(y.target).parents().filter(x).length==0)){l.blur();$(document).unbind("click",u)}};$(window).click(u);return false})}if(q.use_toggle_link){l.hide()}else{var r=o(q.tags);if(r==0){h.hide();n.show()}}function b(v,u){return v+((u!=""&&u)?":"+u:"")}function p(u){$(u).mouseenter(function(){$(this).attr("src",q.delete_tag_img_rollover)});$(u).mouseleave(function(){$(this).attr("src",q.delete_tag_img)});$(u).click(function(){var A=$(this).parent();var z=A.find(".tag-name").eq(0);var y=z.text();var w=i(y);var C=w[0];var v=w[1];var B=A.prev();A.remove();delete q.tags[C];var x=q.get_toggle_link_text_fn(q.tags);s.text(x);$.ajax({url:q.ajax_delete_tag_url,data:{tag_name:C},error:function(){q.tags[C]=v;if(B.hasClass("tag-button")){B.after(A)}else{l.prepend(A)}var D=q.get_toggle_link_text_fn(q.tags);alert("Remove tag failed");s.text(D);u.mouseenter(function(){$(this).attr("src",q.delete_tag_img_rollover)});u.mouseleave(function(){$(this).attr("src",q.delete_tag_img)})},success:f unction(){}});return true})}function t(u){var v=new Array();for(key in u){v[v.length]=key+"-->"+u[key]}return"{"+v.join(",")+"}"}function i(u){return u.split(":")}function j(u){var v=$("<img src='"+q.delete_tag_img+"'/>").addClass("delete-tag-img");p(v);var w=$("<span>").text(u).addClass("tag-name");w.click(function(){tag_name_and_value=u.split(":");q.tag_click_fn(tag_name_and_value[0],tag_name_and_value[1]);return true});var x=$("<span></span>").addClass("tag-button");x.append(w);if(q.editable){x.append(v)}return x}}; \ No newline at end of file diff -r d95a9c843c53 -r 47b702c583a3 static/wymeditor/lang/en.js --- a/static/wymeditor/lang/en.js Tue Dec 08 17:05:35 2009 -0500 +++ b/static/wymeditor/lang/en.js Tue Dec 08 18:49:06 2009 -0500 @@ -45,5 +45,6 @@ // Galaxy replacements. Galaxy_History_Link: 'Insert Link to History', Galaxy_Dataset_Link: 'Insert Link to Dataset', + Annotate_Galaxy_History: 'Annotate History', }; diff -r d95a9c843c53 -r 47b702c583a3 templates/dataset/edit_attributes.mako --- a/templates/dataset/edit_attributes.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/dataset/edit_attributes.mako Tue Dec 08 18:49:06 2009 -0500 @@ -54,12 +54,16 @@ <label> Tags: </label> - <div id="dataset-tag-area" - style="float: left; margin-left: 1px; width: 295px; margin-right: 10px; border-style: inset; border-color: #ddd; border-width: 1px"> + <div style="float: left; width: 295px; margin-right: 10px; border-style: inset; border-width: 1px"> + <style> + .tag-area { + border: none; + } + </style> + ${render_tagging_element(data, "edit_attributes.mako", use_toggle_link=False, in_form=True, input_size="30")} </div> <div style="clear: both"></div> </div> - ${render_tagging_element(data, "dataset-tag-area", "edit_attributes.mako", use_toggle_link="false", in_form="true", input_size="30")} %endif %for name, spec in data.metadata.spec.items(): %if spec.visible: diff -r d95a9c843c53 -r 47b702c583a3 templates/history/sharing.mako --- a/templates/history/sharing.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/history/sharing.mako Tue Dec 08 18:49:06 2009 -0500 @@ -14,7 +14,7 @@ %else: %for history in histories: <div class="toolForm"> - <div class="toolFormTitle">History '${history.name}' shared with</div> + <div class="toolFormTitle">History '${history.get_display_name()}' shared with</div> <div class="toolFormBody"> <div class="form-row"> <div style="float: right;"> diff -r d95a9c843c53 -r 47b702c583a3 templates/history/view.mako --- a/templates/history/view.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/history/view.mako Tue Dec 08 18:49:06 2009 -0500 @@ -2,7 +2,7 @@ <%def name="javascripts()"> ${parent.javascripts()} - ${h.js( "jquery", "json2", "jquery.jstore-all", "jquery.autocomplete", "autocomplete_tagging" )} + ${h.js( "galaxy.base", "jquery", "json2", "jquery.jstore-all", "jquery.autocomplete", "autocomplete_tagging" )} </%def> <%def name="stylesheets()"> @@ -320,14 +320,11 @@ <p></p> %endif - <div id="history-tag-area" style="margin-bottom: 1em"> - </div> - <%namespace file="../tagging_common.mako" import="render_tagging_element" /> %if trans.get_user() is not None: <div id='history-tag-area' class="tag-element"></div> - ${render_tagging_element(history, "history-tag-area", "history/view.mako", use_toggle_link='false', get_toggle_link_text_fn='get_toggle_link_text', editable=user_owns_history)} + ${render_tagging_element(tagged_item=history, elt_context="history/view.mako", use_toggle_link=False, get_toggle_link_text_fn='get_toggle_link_text', editable=user_owns_history)} %endif %if not datasets: diff -r d95a9c843c53 -r 47b702c583a3 templates/page/display.mako --- a/templates/page/display.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/page/display.mako Tue Dec 08 18:49:06 2009 -0500 @@ -2,6 +2,175 @@ <%def name="title()">Galaxy :: ${page.user.username} :: ${page.title}</%def> +<%def name="javascripts()"> + ${parent.javascripts()} + ${h.js( "jquery", "json2", "jquery.jstore-all", "jquery.autocomplete", "autocomplete_tagging" )} + <script type="text/javascript"> + $(function() { + // Load jStore for local storage + $.extend(jQuery.jStore.defaults, { project: 'galaxy', flash: '/static/jStore.Flash.html' }) + $.jStore.load(); // Auto-select best storage + + $.jStore.ready(function(engine) { + engine.ready(function() { + // Init stuff that requires the local storage to be running + //initShowHide(); + setupHistoryItem( $("div.historyItemWrapper") ); + + // Hide all for now. + $( "div.historyItemBody:visible" ).each( function() { + if ( $.browser.mozilla ) { + $(this).find( "pre.peek" ).css( "overflow", "hidden" ); + } + $(this).slideUp( "fast" ); + }); + + }); + }); + }); + // Functionized so AJAX'd datasets can call them + function initShowHide() { + + // Load saved state and show as necessary + try { + var stored = $.jStore.store("history_expand_state"); + if (stored) { + var st = JSON.parse(stored); + for (var id in st) { + $("#" + id + " div.historyItemBody" ).show(); + } + } + } catch(err) { + // Something was wrong with values in storage, so clear storage + $.jStore.remove("history_expand_state"); + } + + // If Mozilla, hide scrollbars in hidden items since they cause animation bugs + if ( $.browser.mozilla ) { + $( "div.historyItemBody" ).each( function() { + if ( ! $(this).is( ":visible" ) ) $(this).find( "pre.peek" ).css( "overflow", "hidden" ); + }) + } + } + // Add show/hide link and delete link to a history item + function setupHistoryItem( query ) { + query.each( function() { + var id = this.id; + var body = $(this).children( "div.historyItemBody" ); + var peek = body.find( "pre.peek" ) + $(this).children( ".historyItemTitleBar" ).find( ".historyItemTitle" ).wrap( "<a href='#'></a>" ).click( function() { + if ( body.is(":visible") ) { + // Hiding stuff here + if ( $.browser.mozilla ) { peek.css( "overflow", "hidden" ) } + body.slideUp( "fast" ); + + // Save setting + var stored = $.jStore.store("history_expand_state") + var prefs = stored ? JSON.parse(stored) : null + if (prefs) { + delete prefs[id]; + $.jStore.store("history_expand_state", JSON.stringify(prefs)); + } + } else { + // Showing stuff here + body.slideDown( "fast", function() { + if ( $.browser.mozilla ) { peek.css( "overflow", "auto" ); } + }); + + // Save setting + var stored = $.jStore.store("history_expand_state") + var prefs = stored ? JSON.parse(stored) : new Object; + prefs[id] = true; + $.jStore.store("history_expand_state", JSON.stringify(prefs)); + } + return false; + }); + // Delete link + $(this).find( "div.historyItemButtons > .delete" ).each( function() { + var data_id = this.id.split( "-" )[1]; + $(this).click( function() { + $( '#historyItem-' + data_id + "> div.historyItemTitleBar" ).addClass( "spinner" ); + $.ajax({ + url: "${h.url_for( action='delete_async', id='XXX' )}".replace( 'XXX', data_id ), + error: function() { alert( "Delete failed" ) }, + success: function() { + %if show_deleted: + var to_update = {}; + to_update[data_id] = "none"; + updater( to_update ); + %else: + $( "#historyItem-" + data_id ).fadeOut( "fast", function() { + $( "#historyItemContainer-" + data_id ).remove(); + if ( $( "div.historyItemContainer" ).length < 1 ) { + $( "#emptyHistoryMessage" ).show(); + } + }); + %endif + } + }); + return false; + }); + }); + // Undelete link + $(this).find( "a.historyItemUndelete" ).each( function() { + var data_id = this.id.split( "-" )[1]; + $(this).click( function() { + $( '#historyItem-' + data_id + " > div.historyItemTitleBar" ).addClass( "spinner" ); + $.ajax({ + url: "${h.url_for( controller='dataset', action='undelete_async', id='XXX' )}".replace( 'XXX', data_id ), + error: function() { alert( "Undelete failed" ) }, + success: function() { + var to_update = {}; + to_update[data_id] = "none"; + updater( to_update ); + } + }); + return false; + }); + }); + }); + }; + + + //TODO: this function is a duplicate of array_length defined in galaxy.base.js ; not sure why it needs to be redefined here (due to streaming?). + // Returns the number of keys (elements) in an array/dictionary. + var array_length = function(an_array) + { + if (an_array.length) + return an_array.length; + + var count = 0; + for (element in an_array) + count++; + return count; + }; + + // + // Function provides text for tagging toggle link. + // + var get_toggle_link_text = function(tags) + { + var text = ""; + var num_tags = array_length(tags); + if (num_tags != 0) + { + text = num_tags + (num_tags != 1 ? " Tags" : " Tag"); + } + else + { + // No tags. + text = "Add tags to history"; + } + return text; + }; + </script> +</%def> + +<%def name="stylesheets()"> + ${parent.stylesheets()} + ${h.css( "base", "history", "autocomplete_tagging" )} +</%def> + <%def name="init()"> <% self.has_left_panel=False diff -r d95a9c843c53 -r 47b702c583a3 templates/page/editor.mako --- a/templates/page/editor.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/page/editor.mako Tue Dec 08 18:49:06 2009 -0500 @@ -21,31 +21,37 @@ </%def> <%def name="javascripts()"> - ${parent.javascripts()} - - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.js')}"> </script> - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.event.drag.js')}"> </script> - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.event.drop.js')}"> </script> - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.event.hover.js')}"> </script> - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.form.js')}"> </script> - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.jstore-all.js')}"> </script> - <script type='text/javascript' src="${h.url_for('/static/scripts/json2.js')}"> </script> - - <script type='text/javascript' src="${h.url_for('/static/scripts/galaxy.base.js')}"> </script> - - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.wymeditor.js')}"> </script> - - <script type='text/javascript' src="${h.url_for('/static/scripts/jquery.autocomplete.js')}"> </script> - + ${h.js( "jquery", "jquery.event.drag", "jquery.event.drop", "jquery.event.hover", "jquery.form", "jquery.jstore-all", "json2", + "galaxy.base", "jquery.wymeditor", "jquery.autocomplete", "autocomplete_tagging")} <script type="text/javascript"> // Useful Galaxy stuff. var Galaxy = { DIALOG_HISTORY_LINK : "history_link", + DIALOG_HISTORY_ANNOTATE : "history_annotate", }; - + + // Initialize Galaxy elements. + function init_galaxy_elts(wym) + { + // Set up events to make annotation easy. + $('.annotation', wym._doc.body).each( function() + { + $(this).click( function() { + // Works in Safari, not in Firefox. + var range = wym._doc.createRange(); + range.selectNodeContents( this ); + var selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + var t = ""; + }); + }); + + }; + ## Completely replace WYM's dialog handling WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHtml ) { @@ -193,7 +199,7 @@ ); } - // HISTORY DIALOG + // INSERT HISTORY LINK DIALOG if ( dialogType == Galaxy.DIALOG_HISTORY_LINK ) { $.ajax( { @@ -244,7 +250,7 @@ // Get history name. $.get( '${h.url_for( controller='history', action='get_name_async' )}?id=' + item_id, function( history_name ) { var href = '${h.url_for( controller='history', action='view' )}?id=' + item_id; - wym.insert("<a href='" + href + "'>History '" + history_name + "'</a>nbsp;"); + wym.insert("<a href='" + href + "'>History '" + history_name + "'</a>"); }); } else @@ -268,6 +274,54 @@ } }); } + // ANNOTATE HISTORY DIALOG + if ( dialogType == Galaxy.DIALOG_ANNOTATE_HISTORY ) { + $.ajax( + { + url: "${h.url_for( action='list_histories_for_selection' )}", + data: {}, + error: function() { alert( "Grid refresh failed" ) }, + success: function(table_html) + { + show_modal( + "Insert Link to History", + table_html, + { + "Annotate": function() + { + // Insert links to history for each checked item. + var item_ids = new Array(); + $('input[name=id]:checked').each(function() { + var item_id = $(this).val(); + + // Get annotation table for history. + $.ajax( + { + url: "${h.url_for( action='get_history_annotation_table' )}", + data: { id : item_id }, + error: function() { alert( "Grid refresh failed" ) }, + success: function(result) + { + // Insert into document. + wym.insert(result); + + init_galaxy_elts(wym); + + } + }); + }); + + hide_modal(); + }, + "Cancel": function() + { + hide_modal(); + } + } + ); + } + }); + } }; </script> @@ -313,7 +367,8 @@ {'name': 'Unlink', 'title': 'Unlink', 'css': 'wym_tools_unlink'}, {'name': 'InsertImage', 'title': 'Image', 'css': 'wym_tools_image'}, {'name': 'InsertTable', 'title': 'Table', 'css': 'wym_tools_table'}, - {'name': 'Insert Galaxy History Link', 'title' : 'Galaxy_History_Link', 'css' : 'galaxy_tools_insert_history_link'} + {'name': 'Insert Galaxy History Link', 'title' : 'Galaxy_History_Link', 'css' : 'galaxy_tools_insert_history_link'}, + {'name': 'Annonate Galaxy History', 'title' : 'Annotate_Galaxy_History', 'css' : 'galaxy_tools_annotate_history'}, ] }); ## Get the editor object @@ -367,13 +422,19 @@ $('.galaxy_tools_insert_history_link').children().click( function() { editor.dialog(Galaxy.DIALOG_HISTORY_LINK); }); + // Initialize 'Annotate history' button. + $('.galaxy_tools_annotate_history').children().click( function() { + editor.dialog(Galaxy.ANNOTATE_HISTORY); + }); + // Initialize galaxy elements. + //init_galaxy_elts(editor); }); </script> </%def> <%def name="stylesheets()"> ${parent.stylesheets()} - ${h.css( "autocomplete_tagging" )} + ${h.css( "base", "history", "autocomplete_tagging" )} </%def> <%def name="center_panel()"> @@ -384,7 +445,7 @@ <a id="close-button" class="panel-header-button">Close</a> </div> <div class="unified-panel-header-inner"> - Page editor + Page Editor <span style="font-weight: normal">| Title : ${page.title}</span> </div> </div> diff -r d95a9c843c53 -r 47b702c583a3 templates/page/history_annotation_table.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/page/history_annotation_table.mako Tue Dec 08 18:49:06 2009 -0500 @@ -0,0 +1,59 @@ +<%namespace file="../tagging_common.mako" import="render_tagging_element_html" /> +<%namespace file="../root/history_common.mako" import="render_dataset" /> + +<table> + ## Table header. + <tr> + <th colspan='2'>History '${history.get_display_name()}'</th> + </tr> + <tr> + ## Status messages and tags. + <th colspan='2'> + %if history.deleted: + <div class="warningmessagesmall"> + ${_('This is a deleted history.')} + </div> + %endif + %if trans.get_user() is not None: + Tags: ${render_tagging_element_html( tagged_item=history, editable=False, use_toggle_link=False )} + %endif + </th> + </tr> + <tr> + <th colspan="2">Description of History: + <ol> + <li>What was the motivation for this history? + <li>What is the outcome of this history? + <li>What are unresolved questions from this history? + <li>What new questions arise from this history? + </ol> + </th> + </tr> + + ## Table body. For each dataset, there is an area to annotate the dataset. + %if not datasets: + <tr> + <td> + <div class="infomessagesmall" id="emptyHistoryMessage"> + ${_("Your history is empty. Click 'Get Data' on the left pane to start")} + </div> + </td> + </tr> + %else: + ## Render requested datasets. + %for data in datasets: + %if data.visible: + <tr> + <td valign="top"><span class="annotation">Describe this step: why was it done? what data does it produce?</span></td> + ##<td valign="top" class="annotation">Describe this step: why was it done? what data does it produce?</td> + <td> + <div class="historyItemContainer" id="historyItemContainer-${data.id}"> + ${render_dataset( data, data.hid, show_deleted_on_refresh = show_deleted, user_owns_dataset = False )} + </div> + </td> + </tr> + %endif + %endfor + %endif +</table> + \ No newline at end of file diff -r d95a9c843c53 -r 47b702c583a3 templates/page/wymiframe.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/page/wymiframe.mako Tue Dec 08 18:49:06 2009 -0500 @@ -0,0 +1,27 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<!-- + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File Name: + * wymiframe.html + * Iframe used by designMode. + * See the documentation for more info. + * + * File Authors: + * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg) +--> +<html> + <head> + <title>WYMeditor iframe</title> + <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" /> + <link rel="stylesheet" type="text/css" media="screen" href="/static/wymeditor/iframe/galaxy/wymiframe.css" /> + ${h.css("base", "history", "autocomplete_tagging")} + </head> + <body class="wym_iframe text-content"></body> +</html> diff -r d95a9c843c53 -r 47b702c583a3 templates/root/history.mako --- a/templates/root/history.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/root/history.mako Tue Dec 08 18:49:06 2009 -0500 @@ -77,6 +77,7 @@ <% updateable = [data for data in reversed( datasets ) if data.visible and data.state not in [ "deleted", "empty", "error", "ok" ]] %> ${ ",".join( map(lambda data: "\"%s\" : \"%s\"" % (data.id, data.state), updateable) ) } }); + // Navigate to a dataset. %if hda_id: self.location = "#${hda_id}"; @@ -317,15 +318,16 @@ <p></p> %endif -<div id="history-tag-area" style="margin-bottom: 1em"> -</div> - <%namespace file="../tagging_common.mako" import="render_tagging_element" /> <%namespace file="history_common.mako" import="render_dataset" /> %if trans.get_user() is not None: - <div id='history-tag-area' class="tag-element"></div> - ${render_tagging_element( tagged_item=history, elt_id="history-tag-area", elt_context="history.mako", get_toggle_link_text_fn='get_toggle_link_text' )} + <style> + .tag-element { + margin-bottom: 0.5em; + } + </style> + ${render_tagging_element( tagged_item=history, elt_context='history.mako', get_toggle_link_text_fn='get_toggle_link_text' )} %endif %if not datasets: diff -r d95a9c843c53 -r 47b702c583a3 templates/tagging_common.mako --- a/templates/tagging_common.mako Tue Dec 08 17:05:35 2009 -0500 +++ b/templates/tagging_common.mako Tue Dec 08 18:49:06 2009 -0500 @@ -1,10 +1,76 @@ +<%! + from cgi import escape + from galaxy.web.framework.helpers import iff +%> ## Render a tagging element if there is a tagged_item. -%if tagged_item is not None and elt_id is not None: - ${render_tagging_element(tagged_item=tagged_item, elt_id=elt_id, elt_context=elt_context, in_form=in_form, input_size=input_size, tag_click_fn=tag_click_fn)} +%if tagged_item is not None: + ${render_tagging_element(tagged_item=tagged_item, elt_context=elt_context, in_form=in_form, input_size=input_size, tag_click_fn=tag_click_fn)} %endif +## Render HTML for a tagging element. +<%def name="render_tagging_element_html(tagged_item=None, editable=True, use_toggle_link=True, input_size='15', in_form=False)"> + ## Useful attributes. + <% + tagged_item_id = str( trans.security.encode_id (tagged_item.id) ) + elt_id = "tag-element-" + tagged_item_id + %> + <div id="${elt_id}" class="tag-element"> + %if use_toggle_link: + <a id="toggle-link-${tagged_item_id}" class="toggle-link" href="#">${len(tagged_item.tags)} Tags</a> + %endif + <div id="tag-area-${tagged_item_id}" class="tag-area"> + + ## Build buttons for current tags. + %for tag in tagged_item.tags: + <% + tag_name = tag.user_tname + tag_value = None + if tag.value is not None: + tag_value = tag.user_value + ## Convert tag name, value to unicode. + if isinstance( tag_name, str ): + tag_name = unicode( escape( tag_name ), 'utf-8' ) + if tag_value: + tag_value = unicode( escape( tag_value ), 'utf-8' ) + tag_str = tag_name + ":" + tag_value + else: + tag_str = tag_name + %> + <span class="tag-button"> + <span class="tag-name">${tag_str}</span> + %if editable: + <img class="delete-tag-img" src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/> + %endif + </span> + %endfor + + ## Add tag input field. If element is in form, tag input is a textarea; otherwise element is a input type=text. + %if editable: + %if in_form: + <textarea id='tag-input' class="tag-input" rows='1' cols='${input_size}'></textarea> + %else: + <input id='tag-input' class="tag-input" type='text' size='${input_size}'></input> + %endif + ## Add "add tag" button. + <img src='${h.url_for('/static/images/add_icon.png')}' rollover='${h.url_for('/static/images/add_icon_dark.png')}' class="add-tag-button"/> + %endif + </div> + </div> +</%def> + + ## Render the tags 'tags' as an autocomplete element. -<%def name="render_tagging_element(tagged_item, elt_id, elt_context, use_toggle_link='true', in_form='false', input_size='15', tag_click_fn='default_tag_click_fn', get_toggle_link_text_fn='default_get_toggle_link_text_fn', editable='true')"> +<%def name="render_tagging_element(tagged_item=None, elt_context=None, use_toggle_link=True, in_form=False, input_size='15', tag_click_fn='default_tag_click_fn', get_toggle_link_text_fn='default_get_toggle_link_text_fn', editable=True)"> + ## Useful attributes. + <% + tagged_item_id = str( trans.security.encode_id (tagged_item.id) ) + elt_id = "tag-element-" + tagged_item_id + %> + + ## Build HTML. + ${self.render_tagging_element_html(tagged_item, editable, use_toggle_link, input_size, in_form)} + + ## Build script that augments tags using progressive javascript. <script type="text/javascript"> // // Set up autocomplete tagger. @@ -70,22 +136,32 @@ var options = { tags : ${h.to_json_string(tag_names_and_values)}, - editable : ${str(editable).lower()}, + editable : ${iff( editable, 'true', 'false' )}, get_toggle_link_text_fn: ${get_toggle_link_text_fn}, tag_click_fn: ${tag_click_fn}, - <% tagged_item_id = trans.security.encode_id(tagged_item.id) %> ajax_autocomplete_tag_url: "${h.url_for( controller='tag', action='tag_autocomplete_data', id=tagged_item_id, item_class=tagged_item.__class__.__name__ )}", ajax_add_tag_url: "${h.url_for( controller='tag', action='add_tag_async', id=tagged_item_id, item_class=tagged_item.__class__.__name__, context=elt_context )}", ajax_delete_tag_url: "${h.url_for( controller='tag', action='remove_tag_async', id=tagged_item_id, item_class=tagged_item.__class__.__name__, context=elt_context )}", delete_tag_img: "${h.url_for('/static/images/delete_tag_icon_gray.png')}", delete_tag_img_rollover: "${h.url_for('/static/images/delete_tag_icon_white.png')}", - add_tag_img: "${h.url_for('/static/images/add_icon.png')}", - add_tag_img_rollover: "${h.url_for('/static/images/add_icon_dark.png')}", - input_size: ${input_size}, - in_form: ${in_form}, - use_toggle_link: ${use_toggle_link} + use_toggle_link: ${iff( use_toggle_link, 'true', 'false' )}, }; - $("#${elt_id}").autocomplete_tagging(options) + $("#${elt_id}").autocomplete_tagging('${elt_id}', options); </script> + + ## Use style to hide/display the tag area. + <style> + .tag-area { + display: ${iff( use_toggle_link, "none", "block" )}; + } + </style> + + <noscript> + <style> + .tag-area { + display: block; + } + </style> + </noscript> </%def> \ No newline at end of file