mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Added individual and community tags to pages. More refactoring of tagging code as well.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user