[hg] galaxy 2769: Made history grid filterable by tags (and valu...
details: http://www.bx.psu.edu/hg/galaxy/rev/40f8f713cbd8 changeset: 2769:40f8f713cbd8 user: jeremy goecks <jeremy.goecks@emory.edu> date: Thu Sep 24 19:00:44 2009 -0400 description: Made history grid filterable by tags (and values) and by status. 9 file(s) affected in this change: lib/galaxy/tags/tag_handler.py lib/galaxy/web/controllers/history.py lib/galaxy/web/controllers/tag.py lib/galaxy/web/framework/helpers/grids.py static/june_2007_style/autocomplete_tagging.css.tmpl static/june_2007_style/blue/autocomplete_tagging.css templates/history/grid.mako templates/root/history.mako templates/tagging_common.mako diffs (837 lines): diff -r 35dd55a7898e -r 40f8f713cbd8 lib/galaxy/tags/tag_handler.py --- a/lib/galaxy/tags/tag_handler.py Thu Sep 24 16:52:15 2009 -0400 +++ b/lib/galaxy/tags/tag_handler.py Thu Sep 24 19:00:44 2009 -0400 @@ -21,8 +21,8 @@ def get_tag_assoc_class(self, entity_class): return self.tag_assoc_classes[entity_class] - # Remove a tag from an item. def remove_item_tag(self, item, tag_name): + """Remove a tag from an item.""" # Get item tag association. item_tag_assoc = self._get_item_tag_assoc(item, tag_name) @@ -35,8 +35,8 @@ return False - # Delete tags from an item. def delete_item_tags(self, item): + """Delete tags from an item.""" # Delete item-tag associations. for tag in item.tags: tag.delete() @@ -44,8 +44,8 @@ # Delete tags from item. del item.tags[:] - # Returns true if item is has a given tag. def item_has_tag(self, item, tag): + """Returns true if item is has a given tag.""" # Get tag name. if isinstance(tag, basestring): tag_name = tag @@ -59,22 +59,25 @@ return False - # Apply tags to an item. def apply_item_tags(self, db_session, item, tags_str): + """Apply tags to an item.""" # Parse tags. - parsed_tags = self._parse_tags(tags_str) + parsed_tags = self.parse_tags(tags_str) # Apply each tag. for name, value in parsed_tags.items(): + # Use lowercase name for searching/creating tag. + lc_name = name.lower() + # Get or create item-tag association. - item_tag_assoc = self._get_item_tag_assoc(item, name) + item_tag_assoc = self._get_item_tag_assoc(item, lc_name) if not item_tag_assoc: # # Create item-tag association. # # Create tag; if None, skip the tag (and log error). - tag = self._get_or_create_tag(db_session, name) + tag = self._get_or_create_tag(db_session, lc_name) if not tag: # Log error? continue @@ -88,16 +91,15 @@ item_tag_assoc.tag = tag # Apply attributes to item-tag association. Strip whitespace from user name and tag. + lc_value = None if value: - trimmed_value = value.strip() - else: - trimmed_value = value - item_tag_assoc.user_tname = name.strip() - item_tag_assoc.user_value = trimmed_value - item_tag_assoc.value = self._scrub_tag_value(value) + lc_value = value.lower() + item_tag_assoc.user_tname = name + item_tag_assoc.user_value = value + item_tag_assoc.value = lc_value - # Build a string from an item's tags. def get_tags_str(self, tags): + """Build a string from an item's tags.""" # Return empty string if there are no tags. if not tags: return "" @@ -111,16 +113,18 @@ tags_str_list.append(tag_str) return ", ".join(tags_str_list) - # Get a Tag object from a tag id. def get_tag_by_id(self, db_session, tag_id): + """Get a Tag object from a tag id.""" return db_session.query(Tag).filter(Tag.id==tag_id).first() - # Get a Tag object from a tag name (string). def get_tag_by_name(self, db_session, tag_name): - return db_session.query(Tag).filter(Tag.name==tag_name).first() + """Get a Tag object from a tag name (string).""" + if tag_name: + return db_session.query( Tag ).filter( Tag.name==tag_name.lower() ).first() + return None - # Create a Tag object from a tag string. def _create_tag(self, db_session, tag_str): + """Create a Tag object from a tag string.""" tag_hierarchy = tag_str.split(self.__class__.hierarchy_separator) tag_prefix = "" parent_tag = None @@ -139,8 +143,8 @@ tag_prefix = tag.name + self.__class__.hierarchy_separator return tag - # Get or create a Tag object from a tag string. def _get_or_create_tag(self, db_session, tag_str): + """Get or create a Tag object from a tag string.""" # Scrub tag; if tag is None after being scrubbed, return None. scrubbed_tag_str = self._scrub_tag_name(tag_str) if not scrubbed_tag_str: @@ -155,18 +159,18 @@ return tag - # Return ItemTagAssociation object for an item and a tag string; returns None if there is - # no such tag. def _get_item_tag_assoc(self, item, tag_name): + """Return ItemTagAssociation object for an item and a tag string; returns None if there is + no such tag.""" scrubbed_tag_name = self._scrub_tag_name(tag_name) for item_tag_assoc in item.tags: if item_tag_assoc.tag.name == scrubbed_tag_name: return item_tag_assoc return None - # Returns a list of raw (tag-name, value) pairs derived from a string; method does not scrub tags. - # Return value is a dictionary where tag-names are keys. - def _parse_tags(self, tag_str): + def parse_tags(self, tag_str): + """Returns a list of raw (tag-name, value) pairs derived from a string; method scrubs tag names and values as well. + Return value is a dictionary where tag-names are keys.""" # Gracefully handle None. if not tag_str: return dict() @@ -179,11 +183,13 @@ name_value_pairs = dict() for raw_tag in raw_tags: nv_pair = self._get_name_value_pair(raw_tag) - name_value_pairs[nv_pair[0]] = nv_pair[1] + scrubbed_name = self._scrub_tag_name( nv_pair[0] ) + scrubbed_value = self._scrub_tag_value( nv_pair[1] ) + name_value_pairs[scrubbed_name] = scrubbed_value return name_value_pairs - # Scrub a tag value. def _scrub_tag_value(self, value): + """Scrub a tag value.""" # Gracefully handle None: if not value: return None @@ -192,11 +198,10 @@ reg_exp = re.compile('\s') scrubbed_value = re.sub(reg_exp, "", value) - # Lowercase and return. - return scrubbed_value.lower() + return scrubbed_value - # Scrub a tag name. def _scrub_tag_name(self, name): + """Scrub a tag name.""" # Gracefully handle None: if not name: return None @@ -213,21 +218,20 @@ if len(scrubbed_name) < 3 or len(scrubbed_name) > 255: return None - # Lowercase and return. - return scrubbed_name.lower() + return scrubbed_name - # Scrub a tag name list. def _scrub_tag_name_list(self, tag_name_list): + """Scrub a tag name list.""" scrubbed_tag_list = list() for tag in tag_name_list: - scrubbed_tag_list.append(self._scrub_tag_name(tag)) + scrubbed_tag_list.append( self._scrub_tag_name(tag) ) return scrubbed_tag_list - # Get name, value pair from a tag string. def _get_name_value_pair(self, tag_str): + """Get name, value pair from a tag string.""" # Use regular expression to parse name, value. - reg_exp = re.compile("[" + self.__class__.key_value_separators + "]") - name_value_pair = reg_exp.split(tag_str) + reg_exp = re.compile( "[" + self.__class__.key_value_separators + "]" ) + name_value_pair = reg_exp.split( tag_str ) # Add empty slot if tag does not have value. if len(name_value_pair) < 2: diff -r 35dd55a7898e -r 40f8f713cbd8 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Thu Sep 24 16:52:15 2009 -0400 +++ b/lib/galaxy/web/controllers/history.py Thu Sep 24 19:00:44 2009 -0400 @@ -2,8 +2,11 @@ from galaxy.web.framework.helpers import time_ago, iff, grids from galaxy import util from galaxy.model.mapping import desc +from galaxy.model import History from galaxy.model.orm import * from galaxy.util.json import * +from galaxy.tags.tag_handler import TagHandler +from sqlalchemy.sql.expression import ClauseElement import webhelpers, logging, operator from datetime import datetime from cgi import escape @@ -39,16 +42,55 @@ return dict( operation="sharing" ) return None class TagsColumn( grids.GridColumn ): - def __init__(self, col_name): - grids.GridColumn.__init__(self, col_name) + def __init__(self, col_name, key, filterable): + grids.GridColumn.__init__(self, col_name, key=key, filterable=filterable) + # Tags cannot be sorted. + self.sortable = False self.tag_elt_id_gen = 0 - def get_value( self, trans, grid, history ): self.tag_elt_id_gen += 1 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=history, elt_id = elt_id, in_form="true", input_size="20" ) + return div_elt + trans.fill_template( "/tagging_common.mako", trans=trans, tagged_item=history, + 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 include only histories with tags in column_filter. """ + if column_filter == "All": + pass + elif column_filter: + # Parse filter to extract multiple tags. + tag_handler = TagHandler() + raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") ) + for name, value in raw_tags.items(): + tag = tag_handler.get_tag_by_name( db_session, name ) + if tag: + query = query.filter( History.tags.any( tag_id=tag.id ) ) + if value: + query = query.filter( History.tags.any( value=value.lower() ) ) + else: + # Tag doesn't exist; unclear what to do here, but the literal thing to do is add the criterion, which + # will then yield a query that returns no results. + query = query.filter( History.tags.any( user_tname=name ) ) + return query + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = { "All": "All" } + accepted_filters = [] + for label, val in accepted_filter_labels_and_vals.items(): + args = { self.key: val } + accepted_filters.append( grids.GridColumnFilter( label, args) ) + return accepted_filters + + + class DeletedColumn( grids.GridColumn ): + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = { "Active" : "False", "Deleted" : "True", "All": "All" } + accepted_filters = [] + for label, val in accepted_filter_labels_and_vals.items(): + args = { self.key: val } + accepted_filters.append( grids.GridColumnFilter( label, args) ) + return accepted_filters # Grid definition title = "Stored histories" @@ -60,12 +102,12 @@ link=( lambda item: iff( item.deleted, None, dict( operation="switch", id=item.id ) ) ), attach_popup=True ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), - TagsColumn( "Tags"), + TagsColumn( "Tags", key="tags", filterable=True), StatusColumn( "Status", attach_popup=False ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), # Valid for filtering but invisible - grids.GridColumn( "Deleted", key="deleted", visible=False ) + DeletedColumn( "Status", key="deleted", visible=False, filterable=True ) ] operations = [ grids.GridOperation( "Switch", allow_multiple=False, condition=( lambda item: not item.deleted ) ), @@ -80,9 +122,9 @@ standard_filters = [ grids.GridColumnFilter( "Active", args=dict( deleted=False ) ), grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ), - grids.GridColumnFilter( "All", args=dict( deleted='All' ) ) + grids.GridColumnFilter( "All", args=dict( deleted='All' ) ), ] - default_filter = dict( deleted=False ) + default_filter = dict( deleted="False", tags="All" ) def get_current_item( self, trans ): return trans.get_history() def apply_default_filter( self, trans, query, **kwargs ): diff -r 35dd55a7898e -r 40f8f713cbd8 lib/galaxy/web/controllers/tag.py --- a/lib/galaxy/web/controllers/tag.py Thu Sep 24 16:52:15 2009 -0400 +++ b/lib/galaxy/web/controllers/tag.py Thu Sep 24 19:00:44 2009 -0400 @@ -34,7 +34,7 @@ self._do_security_check(trans, item) - self.tag_handler.apply_item_tags( trans.sa_session, item, unicode(new_tag).encode('utf-8') ) + self.tag_handler.apply_item_tags( trans.sa_session, item, new_tag.encode('utf-8') ) trans.sa_session.flush() @web.expose @@ -45,7 +45,7 @@ self._do_security_check(trans, item) - self.tag_handler.remove_item_tag( item, unicode(tag_name).encode('utf-8') ) + self.tag_handler.remove_item_tag( item, tag_name.encode('utf-8') ) #print tag_name #print unicode(tag_name) trans.sa_session.flush() @@ -60,41 +60,53 @@ self._do_security_check(trans, item) tag_handler.delete_item_tags(item) - self.tag_handler.apply_item_tags( trans.sa_session, item, unicode(new_tags).encode('utf-8') ) + self.tag_handler.apply_item_tags( trans.sa_session, item, new_tags.encode('utf-8') ) trans.sa_session.flush() @web.expose @web.require_login( "get autocomplete data for an item's tags" ) - def tag_autocomplete_data(self, trans, id=None, item_class=None, q=None, limit=None, timestamp=None): + def tag_autocomplete_data( self, trans, q=None, limit=None, timestamp=None, id=None, item_class=None ): """ Get autocomplete data for an item's tags. """ - + # # Get item, do security check, and get autocomplete data. # - item = self._get_item(trans, item_class, trans.security.decode_id(id)) + item = None + if id is not None: + item = self._get_item(trans, item_class, trans.security.decode_id(id)) + self._do_security_check(trans, item) + + # Get item class. TODO: we should have a mapper that goes from class_name to class object. + if item_class == 'History': + item_class = History + elif item_class == 'HistoryDatasetAssociation': + item_class = HistoryDatasetAssociation - self._do_security_check(trans, item) - - q = unicode(q).encode('utf-8') + q = q.encode('utf-8') if q.find(":") == -1: - return self._get_tag_autocomplete_names(trans, item, q, limit, timestamp) + return self._get_tag_autocomplete_names(trans, q, limit, timestamp, item, item_class) else: - return self._get_tag_autocomplete_values(trans, item, q, limit, timestamp) + return self._get_tag_autocomplete_values(trans, q, limit, timestamp, item, item_class) - def _get_tag_autocomplete_names(self, trans, item, q, limit, timestamp): + def _get_tag_autocomplete_names( self, trans, q, limit, timestamp, item=None, item_class=None ): """Returns autocomplete data for tag names ordered from most frequently used to least frequently used.""" # # Get user's item tags and usage counts. # - # Get item-tag association class. - item_tag_assoc_class = self.tag_handler.get_tag_assoc_class(item.__class__) + # Get item's class object and item-tag association class. + if item is None and item_class is None: + raise RuntimeError("Both item and item_class cannot be None") + elif item is not None: + item_class = item.__class__ + + item_tag_assoc_class = self.tag_handler.get_tag_assoc_class(item_class) # 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.table).join(Tag) - where_clause = and_(self._get_column_for_filtering_item_by_user_id(item.__class__)==trans.get_user().id, + from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag) + where_clause = and_(self._get_column_for_filtering_item_by_user_id(item_class)==trans.get_user().id, Tag.table.c.name.like(q + "%")) order_by = [ func.count("*").desc() ] group_by = item_tag_assoc_class.table.c.tag_id @@ -109,18 +121,18 @@ for row in result_set: tag = self.tag_handler.get_tag_by_id(trans.sa_session, row[0]) - # Exclude tags that are already applied to the history. - if self.tag_handler.item_has_tag(item, tag): + # Exclude tags that are already applied to the item. + if ( item is not None ) and ( self.tag_handler.item_has_tag(item, tag) ): continue # Add tag to autocomplete data. Use the most frequent name that user # has employed for the tag. tag_names = self._get_usernames_for_tag(trans.sa_session, trans.get_user(), - tag, item.__class__, item_tag_assoc_class) + tag, item_class, item_tag_assoc_class) ac_data += tag_names[0] + "|" + tag_names[0] + "\n" return ac_data - def _get_tag_autocomplete_values(self, trans, item, q, limit, timestamp): + def _get_tag_autocomplete_values(self, trans, q, limit, timestamp, item=None, item_class=None): """Returns autocomplete data for tag values ordered from most frequently used to least frequently used.""" @@ -132,13 +144,18 @@ if tag is None: return "" - # Get item-tag association class. - item_tag_assoc_class = self.tag_handler.get_tag_assoc_class(item.__class__) + # Get item's class object and item-tag association class. + if item is None and item_class is None: + raise RuntimeError("Both item and item_class cannot be None") + elif item is not None: + item_class = item.__class__ + + item_tag_assoc_class = self.tag_handler.get_tag_assoc_class(item_class) # Build select statement. cols_to_select = [ item_tag_assoc_class.table.c.value, func.count('*') ] - from_obj = item_tag_assoc_class.table.join(item.table).join(Tag) - where_clause = and_(self._get_column_for_filtering_item_by_user_id(item.__class__)==trans.get_user().id, + from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag) + where_clause = and_(self._get_column_for_filtering_item_by_user_id(item_class)==trans.get_user().id, Tag.table.c.id==tag.id, item_tag_assoc_class.table.c.value.like(tag_value + "%")) order_by = [ func.count("*").desc(), item_tag_assoc_class.table.c.value ] diff -r 35dd55a7898e -r 40f8f713cbd8 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Thu Sep 24 16:52:15 2009 -0400 +++ b/lib/galaxy/web/framework/helpers/grids.py Thu Sep 24 19:00:44 2009 -0400 @@ -38,19 +38,27 @@ query = self.apply_default_filter( trans, query, **kwargs ) # Maintain sort state in generated urls extra_url_args = {} - # Process filtering arguments - filter_args = {} - if self.default_filter: - filter_args.update( self.default_filter ) + # Process filtering arguments to (a) build a query that actuates the filter and (b) builds a + # dictionary that denotes the current filter. + cur_filter_dict = {} for column in self.columns: if column.key: + # Look for filter criterion in kwargs; if not found, look in default filter. + column_filter = None if "f-" + column.key in kwargs: column_filter = kwargs.get( "f-" + column.key ) - query = column.filter( query, column_filter, filter_args ) - # Carry filter along to newly generated urls - extra_url_args[ "f-" + column.key ] = column_filter - if filter_args: - query = query.filter_by( **filter_args ) + elif ( self.default_filter ) and ( column.key in self.default_filter ): + column_filter = self.default_filter.get( column.key ) + + # If column filter found, apply it. + if column_filter is not None: + # Update query. + query = column.filter( trans.sa_session, query, column_filter ) + # Upate current filter dict. + cur_filter_dict[ column.key ] = column_filter + # Carry filter along to newly generated urls. + extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8") + # Process sort arguments sort_key = sort_order = None if 'sort' in kwargs: @@ -92,6 +100,7 @@ return trans.fill_template( self.template, grid=self, query=query, + cur_filter_dict=cur_filter_dict, sort_key=sort_key, encoded_sort_key=encoded_sort_key, sort_order=sort_order, @@ -125,7 +134,7 @@ return query class GridColumn( object ): - def __init__( self, label, key=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1 ): + def __init__( self, label, key=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1, filterable=False ): self.label = label self.key = key self.method = method @@ -134,6 +143,7 @@ self.attach_popup = attach_popup self.visible = visible self.ncells = ncells + self.filterable = filterable # Currently can only sort of columns that have a database # representation, not purely derived. if self.key: @@ -154,20 +164,23 @@ if self.link and self.link( item ): return self.link( item ) return None - def filter( self, query, column_filter, filter_args ): - """ - Must modify filter_args for carrying forward, and return query - (possibly filtered). - """ + def filter( self, db_session, query, column_filter ): + """ Modify query to reflect the column filter. """ + if column_filter == "All": + pass if column_filter == "True": - filter_args[self.key] = True query = query.filter_by( **{ self.key: True } ) elif column_filter == "False": - filter_args[self.key] = False query = query.filter_by( **{ self.key: False } ) - elif column_filter == "All": - del filter_args[self.key] return query + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filters_vals = [ "False", "True", "All" ] + accepted_filters = [] + for val in accepted_filters_vals: + args = { self.key: val } + accepted_filters.append( GridColumnFilter( val, args) ) + return accepted_filters class GridOperation( object ): def __init__( self, label, key=None, condition=None, allow_multiple=True, target=None, url_args=None ): diff -r 35dd55a7898e -r 40f8f713cbd8 static/june_2007_style/autocomplete_tagging.css.tmpl --- a/static/june_2007_style/autocomplete_tagging.css.tmpl Thu Sep 24 16:52:15 2009 -0400 +++ b/static/june_2007_style/autocomplete_tagging.css.tmpl Thu Sep 24 19:00:44 2009 -0400 @@ -76,7 +76,7 @@ .toggle-link { - font-weight: bold; + font-weight: normal; padding: 0.3em; margin-bottom: 1em; width: 100%; diff -r 35dd55a7898e -r 40f8f713cbd8 static/june_2007_style/blue/autocomplete_tagging.css --- a/static/june_2007_style/blue/autocomplete_tagging.css Thu Sep 24 16:52:15 2009 -0400 +++ b/static/june_2007_style/blue/autocomplete_tagging.css Thu Sep 24 19:00:44 2009 -0400 @@ -76,7 +76,7 @@ .toggle-link { - font-weight: bold; + font-weight: normal; padding: 0.3em; margin-bottom: 1em; width: 100%; diff -r 35dd55a7898e -r 40f8f713cbd8 templates/history/grid.mako --- a/templates/history/grid.mako Thu Sep 24 16:52:15 2009 -0400 +++ b/templates/history/grid.mako Thu Sep 24 19:00:44 2009 -0400 @@ -1,3 +1,5 @@ +<%! from galaxy.web.framework.helpers.grids import GridColumnFilter %> + <%inherit file="/base.mako"/> <%def name="title()">${grid.title}</%def> @@ -25,6 +27,74 @@ }); }) }); + + // Set up autocomplete for tag filter input. + var t = $("#input-tag-filter"); + 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 < 3) + return false; + + // + // New tag OK. + // + } + }); + + // 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 autocomplete_options = + { selectFirst: false, formatItem : format_item_func, autoFill: false, highlight: false, mustMatch: true }; + + t.autocomplete("${h.url_for( controller='tag', action='tag_autocomplete_data', item_class='History' )}", autocomplete_options); + + //t.addClass("tag-input"); + + return t; }); ## Can this be moved into base.mako? %if refresh_frames: @@ -55,6 +125,25 @@ } %endif %endif + + // + // Add a tag to the current grid filter; this adds the tag to the filter and then issues a request to refresh the grid. + // + function add_tag_to_grid_filter(tag_name, tag_value) + { + // Use tag as a filter: replace TAGNAME with tag_name and issue query. + <% + url_args = {} + if "tags" in cur_filter_dict and cur_filter_dict["tags"] != "All": + url_args["f-tags"] = cur_filter_dict["tags"].encode("utf-8") + ", TAGNAME" + else: + url_args["f-tags"] = "TAGNAME" + %> + var url_base = "${url( url_args )}"; + var url = url_base.replace("TAGNAME", tag_name); + self.location = url; + } + </script> </%def> @@ -73,19 +162,50 @@ </style> </%def> -%if grid.standard_filters: - <div class="grid-header"> - <h2>${grid.title}</h2> - <span class="title">Filter:</span> - %for i, filter in enumerate( grid.standard_filters ): - %if i > 0: - <span>|</span> +<div class="grid-header"> + <h2>${grid.title}</h2> + + ## Print grid filter. + <form name="history_actions" action="javascript:add_tag_to_grid_filter($('#input-tag-filter').attr('value'))" method="get" > + <strong>Filter: </strong> + %for column in grid.columns: + %if column.filterable: + <span> by ${column.label.lower()}:</span> + ## For now, include special case to handle tags. + %if column.key == "tags": + %if cur_filter_dict[column.key] != "All": + <span class="filter" "style='font-style: italic'"> + ${cur_filter_dict[column.key]} + </span> + <span>|</span> + %endif + <input id="input-tag-filter" name="f-tags" type="text" value="" size="15"/> + <span>|</span> + %endif + + ## Handle other columns. + %for i, filter in enumerate( column.get_accepted_filters() ): + %if i > 0: + <span>|</span> + %endif + %if cur_filter_dict[column.key] == filter.args[column.key]: + <span class="filter" "style='font-style: italic'">${filter.label}</span> + %else: + <span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span> + %endif + %endfor + <span> </span> %endif - <span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span> %endfor - </div> -%endif - + + ## Link to clear all filters. + <% + args = { "deleted" : "False", "tags" : "All" } + no_filter = GridColumnFilter("Clear", args) + %> + <span><a href="${url( no_filter.get_url_args() )}">${no_filter.label}</a></span> + </form> +</div> <form name="history_actions" action="${url()}" method="post" > <table class="grid"> <thead> diff -r 35dd55a7898e -r 40f8f713cbd8 templates/root/history.mako --- a/templates/root/history.mako Thu Sep 24 16:52:15 2009 -0400 +++ b/templates/root/history.mako Thu Sep 24 19:00:44 2009 -0400 @@ -242,6 +242,25 @@ } }); }; + + // + // 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> <style> @@ -289,7 +308,7 @@ %if trans.get_user() is not None: <div id='history-tag-area' class="tag-element"></div> - ${render_tagging_element(history, "history-tag-area")} + ${render_tagging_element(history, "history-tag-area", get_toggle_link_text_fn='get_toggle_link_text')} %endif %if not datasets: diff -r 35dd55a7898e -r 40f8f713cbd8 templates/tagging_common.mako --- a/templates/tagging_common.mako Thu Sep 24 16:52:15 2009 -0400 +++ b/templates/tagging_common.mako Thu Sep 24 19:00:44 2009 -0400 @@ -1,12 +1,11 @@ ## 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, elt_id=elt_id, in_form=in_form, input_size=input_size)} + ${render_tagging_element(tagged_item, elt_id=elt_id, in_form=in_form, input_size=input_size, tag_click_fn=tag_click_fn)} %endif ## Render the tags 'tags' as an autocomplete element. -<%def name="render_tagging_element(tagged_item, elt_id, use_toggle_link='true', in_form='false', input_size='15')"> +<%def name="render_tagging_element(tagged_item, elt_id, 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')"> <script type="text/javascript"> - // // Set up autocomplete tagger. // @@ -39,9 +38,9 @@ }; // - // Function get text to display on the toggle link. + // Default function get text to display on the toggle link. // - var get_toggle_link_text = function(tags) + var default_get_toggle_link_text_fn = function(tags) { var text = ""; var num_tags = array_length(tags); @@ -73,30 +72,19 @@ else { // No tags. - text = "Add tags to history"; + text = "Add tags"; } return text; }; - // - // Function to handle a tag click. - // - var tag_click_fn = function(tag_name, tag_value) - { - /* - alert(tag_name); - - // Do URL request to get histories tag. - self.location = "http://www.yahoo.com"; - */ - }; + // Default function to handle a tag click. + var default_tag_click_fn = function(tag_name, tag_value) {}; var options = { tags : ${h.to_json_string(tag_names_and_values)}, - get_toggle_link_text_fn: get_toggle_link_text, - tag_click_fn: tag_click_fn, - ##tag_click_fn: function(name, value) { /* Do nothing. */ }, + 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__ )}",
participants (1)
-
Nate Coraor