details: http://www.bx.psu.edu/hg/galaxy/rev/bf01713ce302 changeset: 3179:bf01713ce302 user: jeremy goecks <jeremy.goecks at emory.edu> date: Mon Dec 14 22:12:03 2009 -0500 description: Added individual and community tags to pages. More refactoring of tagging code as well. diffstat: lib/galaxy/tags/tag_handler.py | 62 ++++++++++++++++++-- lib/galaxy/web/controllers/tag.py | 28 ++++----- static/scripts/autocomplete_tagging.js | 46 ++++++++------- templates/page/display.mako | 61 ++++++++++++++------ templates/tagging_common.mako | 81 +++++++++++++++++++------- 5 files changed, 192 insertions(+), 86 deletions(-) diffs (487 lines): diff -r 86cbda1bcf67 -r bf01713ce302 lib/galaxy/tags/tag_handler.py --- a/lib/galaxy/tags/tag_handler.py Mon Dec 14 21:14:53 2009 -0500 +++ b/lib/galaxy/tags/tag_handler.py Mon Dec 14 22:12:03 2009 -0500 @@ -1,5 +1,9 @@ from galaxy.model import Tag import re +from sqlalchemy.sql.expression import func, and_ +from sqlalchemy.sql import select +from galaxy.model import History, HistoryTagAssociation, Dataset, DatasetTagAssociation, \ + HistoryDatasetAssociation, HistoryDatasetAssociationTagAssociation, Page, PageTagAssociation class TagHandler( object ): @@ -18,14 +22,56 @@ # Key-value separator. key_value_separators = "=:" - def __init__(self): - self.tag_assoc_classes = dict() + # Item-specific information needed to perform tagging. + class ItemTagAssocInfo( object ): + def __init__( self, item_class, tag_assoc_class, item_id_col ): + self.item_class = item_class + self.tag_assoc_class = tag_assoc_class + self.item_id_col = item_id_col + + # Initialize with known classes. + item_tag_assoc_info = {} + item_tag_assoc_info["History"] = ItemTagAssocInfo( History, HistoryTagAssociation, HistoryTagAssociation.table.c.history_id ) + item_tag_assoc_info["HistoryDatasetAssociation"] = \ + ItemTagAssocInfo( HistoryDatasetAssociation, HistoryDatasetAssociationTagAssociation, HistoryDatasetAssociationTagAssociation.table.c.history_dataset_association_id ) + item_tag_assoc_info["Page"] = ItemTagAssocInfo( Page, PageTagAssociation, PageTagAssociation.table.c.page_id ) - def add_tag_assoc_class(self, entity_class, tag_assoc_class): - self.tag_assoc_classes[entity_class] = tag_assoc_class + def get_tag_assoc_class(self, item_class): + """ Returns tag association class for item class. """ + return self.item_tag_assoc_info[item_class.__name__].tag_assoc_class - def get_tag_assoc_class(self, entity_class): - return self.tag_assoc_classes[entity_class] + def get_id_col_in_item_tag_assoc_table( self, item_class): + """ Returns item id column in class' item-tag association table. """ + return self.item_tag_assoc_info[item_class.__name__].item_id_col + + def get_community_tags(self, sa_session, item=None, limit=None): + """ Returns community tags for an item. """ + + # Get item-tag association class. + item_class = item.__class__ + item_tag_assoc_class = self.get_tag_assoc_class( item_class ) + if not item_tag_assoc_class: + return [] + + # Build select statement. + cols_to_select = [ item_tag_assoc_class.table.c.tag_id, func.count('*') ] + from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table) + where_clause = ( self.get_id_col_in_item_tag_assoc_table(item_class) == item.id ) + order_by = [ func.count("*").desc() ] + group_by = item_tag_assoc_class.table.c.tag_id + + # Do query and get result set. + query = select(columns=cols_to_select, from_obj=from_obj, + whereclause=where_clause, group_by=group_by, order_by=order_by, limit=limit) + result_set = sa_session.execute(query) + + # Return community tags. + community_tags = [] + for row in result_set: + tag_id = row[0] + community_tags.append( self.get_tag_by_id( sa_session, tag_id ) ) + + return community_tags def remove_item_tag( self, trans, item, tag_name ): """Remove a tag from an item.""" @@ -45,7 +91,7 @@ """Delete tags from an item.""" # Delete item-tag associations. for tag in item.tags: - trans.sa_ession.delete( tag ) + trans.sa_session.delete( tag ) # Delete tags from item. del item.tags[:] @@ -89,7 +135,7 @@ continue # Create tag association based on item class. - item_tag_assoc_class = self.tag_assoc_classes[item.__class__] + item_tag_assoc_class = self.get_tag_assoc_class( item.__class__ ) item_tag_assoc = item_tag_assoc_class() # Add tag to association. diff -r 86cbda1bcf67 -r bf01713ce302 lib/galaxy/web/controllers/tag.py --- a/lib/galaxy/web/controllers/tag.py Mon Dec 14 21:14:53 2009 -0500 +++ b/lib/galaxy/web/controllers/tag.py Mon Dec 14 22:12:03 2009 -0500 @@ -13,19 +13,10 @@ def __init__(self, app): BaseController.__init__(self, app) - - # Keep a list of taggable classes. - self.taggable_classes = dict() - self.taggable_classes[History.__name__] = History - self.taggable_classes[HistoryDatasetAssociation.__name__] = HistoryDatasetAssociation - self.taggable_classes[Page.__name__] = Page - + # Set up tag handler to recognize the following items: History, HistoryDatasetAssociation, Page, ... self.tag_handler = TagHandler() - self.tag_handler.add_tag_assoc_class(History, HistoryTagAssociation) - self.tag_handler.add_tag_assoc_class(HistoryDatasetAssociation, HistoryDatasetAssociationTagAssociation) - self.tag_handler.add_tag_assoc_class(Page, PageTagAssociation) - + @web.expose @web.require_login( "Add tag to an item." ) def add_tag_async( self, trans, id=None, item_class=None, new_tag=None, context=None ): @@ -91,6 +82,8 @@ item_class = History elif item_class == 'HistoryDatasetAssociation': item_class = HistoryDatasetAssociation + elif item_class == 'Page': + item_class = Page q = q.encode('utf-8') if q.find(":") == -1: @@ -207,16 +200,16 @@ def _get_column_for_filtering_item_by_user_id(self, item_class): """ Returns the column to use when filtering by user id. """ - # TODO: make this generic by using a dict() to map from item class to a "user id" column - if item_class is History: - return History.table.c.user_id - elif item_class is HistoryDatasetAssociation: + if item_class is HistoryDatasetAssociation: # Use the user_id associated with the HDA's history. return History.table.c.user_id + else: + # Generically, just use the user_id column of the tagged item's table. + return item_class.table.c.user_id def _get_item(self, trans, item_class_name, id): """ Get an item based on type and id. """ - item_class = self.taggable_classes[item_class_name] + item_class = self.tag_handler.item_tag_assoc_info[item_class_name].item_class item = trans.sa_session.query(item_class).filter("id=" + str(id))[0] return item; @@ -234,3 +227,6 @@ elif isinstance(item, HistoryDatasetAssociation): # TODO. pass + elif isinstance(item, Page): + # TODO. + pass diff -r 86cbda1bcf67 -r bf01713ce302 static/scripts/autocomplete_tagging.js --- a/static/scripts/autocomplete_tagging.js Mon Dec 14 21:14:53 2009 -0500 +++ b/static/scripts/autocomplete_tagging.js Mon Dec 14 22:12:03 2009 -0500 @@ -3,7 +3,22 @@ * @author: Jeremy Goecks * @require: jquery.autocomplete plugin */ -jQuery.fn.autocomplete_tagging = function(elt_id, options) +// +// Initialize "tag click functions" for tags. +// +function init_tag_click_function(tag_elt, click_func) +{ + $(tag_elt).find('.tag-name').each( function() { + $(this).click( function() { + var tag_str = $(this).text(); + var tag_name_and_value = tag_str.split(":") + click_func(tag_name_and_value[0], tag_name_and_value[1]); + return true; + }); + }); +} + +jQuery.fn.autocomplete_tagging = function(options) { // @@ -61,21 +76,17 @@ // Initalize object's elements. // - // 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'); + // Get elements for this object. For this_obj, assume the last element with the id is the "this"; this is somewhat of a hack to address the problem + // that there may be two tagging elements for a single item if there are both community and individual tags for an element. + var this_obj = $(this); + var tag_area = this_obj.find('.tag-area'); + var toggle_link = this_obj.find('.toggle-link'); + 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) @@ -210,7 +221,7 @@ } var autocomplete_options = { selectFirst: false, formatItem : format_item_func, autoFill: false, highlight: false }; - tag_input_field.autocomplete(settings.ajax_autocomplete_tag_url, autocomplete_options); + tag_input_field.autocomplete(settings.ajax_autocomplete_tag_url, autocomplete_options); // Initialize delete tag images for current tags. @@ -218,16 +229,9 @@ init_delete_tag_image( $(this) ); }); - 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; - }); - }); - + // Initialize tag click function. + init_tag_click_function($(this), settings.tag_click_fn); // Initialize "add tag" button. add_tag_button.click( function() diff -r 86cbda1bcf67 -r bf01713ce302 templates/page/display.mako --- a/templates/page/display.mako Mon Dec 14 21:14:53 2009 -0500 +++ b/templates/page/display.mako Mon Dec 14 22:12:03 2009 -0500 @@ -146,29 +146,35 @@ }; // - // Function provides text for tagging toggle link. + // Handle click on community tag. // - var get_toggle_link_text = function(tags) + function community_tag_click(tag_name, tag_value) { - 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; - }; + alert("community tag click: " + tag_name); + } </script> </%def> <%def name="stylesheets()"> ${parent.stylesheets()} ${h.css( "base", "history", "autocomplete_tagging" )} + <style> + .page-body + { + padding: 10px; + float: left; + width: 65%; + } + .page-meta + { + float: right; + width: 27%; + padding: 0.5em; + margin: 0.25em; + vertical-align: text-top; + border: 2px solid #DDDDDD; + } + </style> </%def> <%def name="init()"> @@ -180,6 +186,7 @@ %> </%def> +<%namespace file="../tagging_common.mako" import="render_tagging_element, render_community_tagging_element" /> <%def name="center_panel()"> @@ -191,9 +198,27 @@ <div class="unified-panel-body"> <div style="overflow: auto; height: 100%;"> - <div class="page text-content" style="padding: 10px;"> - ${page.latest_revision.content.decode( "utf-8" )} - </div> + <div class="page text-content page-body"> + ${page.latest_revision.content.decode( "utf-8" )} + </div> + <div class="page-meta"> + <div><strong>Tags</strong></div> + <p> + ## Community tags. + <div> + Community: + ${render_community_tagging_element( tagged_item=page, tag_click_fn='community_tag_click', use_toggle_link=False )} + %if len ( page.tags ) == 0: + none + %endif + </div> + ## User tags. + <p> + <div> + Yours: + ${render_tagging_element( tagged_item=page, elt_context='display.mako', use_toggle_link=False )} + </div> + </div> </div> </div> diff -r 86cbda1bcf67 -r bf01713ce302 templates/tagging_common.mako --- a/templates/tagging_common.mako Mon Dec 14 21:14:53 2009 -0500 +++ b/templates/tagging_common.mako Mon Dec 14 22:12:03 2009 -0500 @@ -1,40 +1,59 @@ <%! from cgi import escape from galaxy.web.framework.helpers import iff + from random import random + from sys import maxint + from math import floor + from galaxy.tags.tag_handler import TagHandler + from galaxy.model import Tag, ItemTagAssociation + + tag_handler = TagHandler() %> + ## Render a tagging element if there is a tagged_item. %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)"> +## Render HTML for a list of tags. +<%def name="render_tagging_element_html(elt_id=None, tags=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 + num_tags = len( tags ) %> - <div id="${elt_id}" class="tag-element"> + <div class="tag-element" + %if elt_id: + id="${elt_id}" + %endif + ## Do not display element if there are no tags and it is not editable. + %if num_tags == 0 and not editable: + style="display: none" + %endif + > %if use_toggle_link: - <a id="toggle-link-${tagged_item_id}" class="toggle-link" href="#">${len(tagged_item.tags)} Tags</a> + <a class="toggle-link" href="#">${num_tags} Tags</a> %endif - <div id="tag-area-${tagged_item_id}" class="tag-area"> + <div class="tag-area"> ## Build buttons for current tags. - %for tag in tagged_item.tags: + %for tag in tags: <% - tag_name = tag.user_tname - tag_value = None - if tag.value is not None: + ## Handle both Tag and ItemTagAssociation objects. + if isinstance( tag, Tag ): + tag_name = tag.name + tag_value = None + elif isinstance( tag, ItemTagAssociation ): + tag_name = tag.user_tname 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 + if tag_value: + tag_str = tag_name + ":" + tag_value + else: + tag_str = tag_name %> <span class="tag-button"> <span class="tag-name">${tag_str}</span> @@ -47,9 +66,9 @@ ## 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> + <textarea class="tag-input" rows='1' cols='${input_size}'></textarea> %else: - <input id='tag-input' class="tag-input" type='text' size='${input_size}'></input> + <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"/> @@ -58,17 +77,32 @@ </div> </%def> +## Render community tagging element. +<%def name="render_community_tagging_element(tagged_item=None, use_toggle_link=False, tag_click_fn='default_tag_click_fn')"> + ## Build HTML. + <% + elt_id = int ( floor ( random()*maxint ) ) + community_tags = tag_handler.get_community_tags(trans.sa_session, tagged_item, 10) + %> + ${self.render_tagging_element_html(elt_id=elt_id, tags=community_tags, use_toggle_link=use_toggle_link, editable=False)} + + ## Set up tag click function. + <script type="text/javascript"> + init_tag_click_function($('#${elt_id}'), ${tag_click_fn}); + </script> +</%def> + ## Render the tags 'tags' as an autocomplete element. <%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 + elt_id = int ( floor ( random()*maxint ) ) %> ## Build HTML. - ${self.render_tagging_element_html(tagged_item, editable, use_toggle_link, input_size, in_form)} + ${self.render_tagging_element_html(elt_id, tagged_item.tags, editable, use_toggle_link, input_size, in_form)} ## Build script that augments tags using progressive javascript. <script type="text/javascript"> @@ -131,7 +165,7 @@ }; // Default function to handle a tag click. - var default_tag_click_fn = function(tag_name, tag_value) {}; + var default_tag_click_fn = function(tag_name, tag_value) { }; var options = { @@ -139,15 +173,16 @@ editable : ${iff( editable, 'true', 'false' )}, get_toggle_link_text_fn: ${get_toggle_link_text_fn}, tag_click_fn: ${tag_click_fn}, - 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 )}", + ## Use forward slash in controller to suppress route memory. + 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')}", use_toggle_link: ${iff( use_toggle_link, 'true', 'false' )}, }; - $("#${elt_id}").autocomplete_tagging('${elt_id}', options); + $('#${elt_id}').autocomplete_tagging(options); </script> ## Use style to hide/display the tag area.