Added individual and community tags to pages. More refactoring of tagging code as well.

This commit is contained in:
Jeremy Goecks
2009-12-14 22:12:03 -05:00
parent f7fdf28381
commit 2271da0025
5 changed files with 193 additions and 87 deletions
+54 -8
View File
@@ -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 @@ class TagHandler( object ):
# 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 @@ class TagHandler( object ):
"""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 @@ class TagHandler( object ):
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.
+12 -16
View File
@@ -13,19 +13,10 @@ class TagsController ( BaseController ):
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 @@ class TagsController ( BaseController ):
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 @@ class TagsController ( BaseController ):
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 @@ class TagsController ( BaseController ):
elif isinstance(item, HistoryDatasetAssociation):
# TODO.
pass
elif isinstance(item, Page):
# TODO.
pass
+25 -21
View File
@@ -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 @@ jQuery.fn.autocomplete_tagging = function(elt_id, options)
// 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 @@ jQuery.fn.autocomplete_tagging = function(elt_id, options)
}
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 @@ jQuery.fn.autocomplete_tagging = function(elt_id, options)
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()
+43 -18
View File
@@ -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>
+59 -24
View File
@@ -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">
%if use_toggle_link:
<a id="toggle-link-${tagged_item_id}" class="toggle-link" href="#">${len(tagged_item.tags)} Tags</a>
<div class="tag-element"
%if elt_id:
id="${elt_id}"
%endif
<div id="tag-area-${tagged_item_id}" class="tag-area">
## 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 class="toggle-link" href="#">${num_tags} Tags</a>
%endif
<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.