details: http://www.bx.psu.edu/hg/galaxy/rev/f776fa6045ba changeset: 2947:f776fa6045ba user: jeremy goecks <jeremy.goecks@emory.edu> date: Tue Nov 03 12:58:13 2009 -0500 description: Improved search functionality for history grid: (a) generic free text search box and (b) advanced search using name, tag, deleted status, and shared status. diffstat: lib/galaxy/tags/tag_handler.py | 8 +- lib/galaxy/web/controllers/history.py | 145 +++++++++++++++++--- lib/galaxy/web/framework/helpers/grids.py | 1 + static/scripts/autocomplete_tagging.js | 2 +- templates/history/grid.mako | 250 +++++++++++++++++++--------------- test/base/twilltestcase.py | 18 +- test/functional/test_history_functions.py | 2 +- 7 files changed, 278 insertions(+), 148 deletions(-) diffs (649 lines): diff -r 80915982fdb2 -r f776fa6045ba lib/galaxy/tags/tag_handler.py --- a/lib/galaxy/tags/tag_handler.py Tue Nov 03 11:28:34 2009 -0500 +++ b/lib/galaxy/tags/tag_handler.py Tue Nov 03 12:58:13 2009 -0500 @@ -3,6 +3,12 @@ class TagHandler( object ): + # Minimum tag length. + min_tag_len = 2 + + # Maximum tag length. + max_tag_len = 255 + # Tag separator. tag_separators = ',;' @@ -215,7 +221,7 @@ scrubbed_name = scrubbed_name[1:] # If name is too short or too long, return None. - if len(scrubbed_name) < 3 or len(scrubbed_name) > 255: + if len(scrubbed_name) < self.min_tag_len or len(scrubbed_name) > self.max_tag_len: return None return scrubbed_name diff -r 80915982fdb2 -r f776fa6045ba lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Tue Nov 03 11:28:34 2009 -0500 +++ b/lib/galaxy/web/controllers/history.py Tue Nov 03 12:58:13 2009 -0500 @@ -5,6 +5,7 @@ from galaxy.model import History from galaxy.model.orm import * from galaxy.util.json import * +from galaxy.util.odict import odict from galaxy.tags.tag_handler import TagHandler from sqlalchemy.sql.expression import ClauseElement import webhelpers, logging, operator @@ -19,12 +20,29 @@ class HistoryListGrid( grids.Grid ): # Custom column types class NameColumn( grids.GridColumn ): - def __init( self, key, link, attach_popup ): + def __init( self, key, link, attach_popup, filterable ): grids.GridColumn.__init__(self, key, link, attach_popup) def get_value( self, trans, grid, history ): return history.get_display_name() + def filter( self, db_session, query, column_filter ): + """ Modify query to filter histories by name. """ + if column_filter == "All": + pass + elif column_filter: + query = query.filter( func.lower( History.name ).like( "%" + column_filter.lower() + "%" ) ) + return query + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = odict() + accepted_filter_labels_and_vals["FREETEXT"] = "FREETEXT" + accepted_filters = [] + for label, val in accepted_filter_labels_and_vals.iteritems(): + args = { self.key: val } + accepted_filters.append( grids.GridColumnFilter( label, args) ) + return accepted_filters + class DatasetsByStateColumn( grids.GridColumn ): def get_value( self, trans, grid, history ): rval = [] @@ -48,6 +66,7 @@ if item.users_shared_with or item.importable: return dict( operation="sharing" ) return None + class TagsColumn( grids.GridColumn ): def __init__( self, col_name, key, filterable ): grids.GridColumn.__init__(self, col_name, key=key, filterable=filterable) @@ -61,7 +80,7 @@ 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. """ + """ Modify query to filter histories by tag. """ if column_filter == "All": pass elif column_filter: @@ -69,52 +88,115 @@ 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 name: + # Search for tag names. + query = query.filter( History.tags.any( func.lower( model.HistoryTagAssociation.user_tname ).like( "%" + name.lower() + "%" ) ) ) 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 ) ) + # Search for tag values. + query = query.filter( History.tags.any( func.lower( model.HistoryTagAssociation.user_value ).like( "%" + value.lower() + "%" ) ) ) 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 - - + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = odict() + accepted_filter_labels_and_vals["FREETEXT"] = "FREETEXT" + accepted_filters = [] + for label, val in accepted_filter_labels_and_vals.iteritems(): + 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_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 + + class SharingColumn( grids.GridColumn ): + def filter( self, db_session, query, column_filter ): + """ Modify query to filter histories by sharing status. """ + if column_filter == "All": + pass + elif column_filter: + if column_filter == "private": + query = query.filter( History.users_shared_with == None ) + query = query.filter( History.importable == False ) + elif column_filter == "shared": + query = query.filter( History.users_shared_with != None ) + elif column_filter == "importable": + query = query.filter( History.importable == True ) + return query + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = odict() + accepted_filter_labels_and_vals["private"] = "private" + accepted_filter_labels_and_vals["shared"] = "shared" + accepted_filter_labels_and_vals["importable"] = "importable" + 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 FreeTextSearchColumn( grids.GridColumn ): + def filter( self, db_session, query, column_filter ): + """ Modify query to search tags and history names. """ + if column_filter == "All": + pass + elif column_filter: + # Build tags filter. + tag_handler = TagHandler() + raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") ) + tags_filter = None + for name, value in raw_tags.items(): + if name: + # Search for tag names. + tags_filter = History.tags.any( func.lower( model.HistoryTagAssociation.user_tname ).like( "%" + name.lower() + "%" ) ) + if value: + # Search for tag values. + tags_filter = and_( tags_filter, func.lower( History.tags.any( model.HistoryTagAssociation.user_value ).like( "%" + value.lower() + "%" ) ) ) + + # Build history name filter. + history_name_filter = func.lower( History.name ).like( "%" + column_filter.lower() + "%" ) + + # Apply filters to query. + if tags_filter: + query = query.filter( or_( tags_filter, history_name_filter ) ) + else: + query = query.filter( history_name_filter ) + return query + def get_accepted_filters( self ): + """ Returns a list of accepted filters for this column. """ + accepted_filter_labels_and_vals = odict() + accepted_filter_labels_and_vals["FREETEXT"] = "FREETEXT" + accepted_filters = [] + for label, val in accepted_filter_labels_and_vals.iteritems(): + args = { self.key: val } + accepted_filters.append( grids.GridColumnFilter( label, args) ) + return accepted_filters # Grid definition - title = "Stored histories" + title = "Saved Histories" model_class = model.History template='/history/grid.mako' default_sort_key = "-create_time" columns = [ NameColumn( "Name", key="name", link=( lambda history: iff( history.deleted, None, dict( operation="switch", id=history.id ) ) ), - attach_popup=True ), + attach_popup=True, filterable=True ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), 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 - DeletedColumn( "Status", key="deleted", visible=False, filterable=True ) + # Columns that are valid for filtering but are not visible. + DeletedColumn( "Deleted", key="deleted", visible=False, filterable=True ), + SharingColumn( "Shared", key="shared", visible=False, filterable=True ), + FreeTextSearchColumn( "Search", key="free-text-search", visible=False ) # Not filterable because it's the default search. ] operations = [ grids.GridOperation( "Switch", allow_multiple=False, condition=( lambda item: not item.deleted ) ), @@ -131,7 +213,7 @@ grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ), grids.GridColumnFilter( "All", args=dict( deleted='All' ) ), ] - default_filter = dict( deleted="False", tags="All" ) + default_filter = dict( name="All", deleted="False", tags="All", shared="All" ) num_rows_per_page = 50 preserve_state = False use_paging = True @@ -160,6 +242,7 @@ template='/history/grid.mako' model_class = model.History default_sort_key = "-update_time" + default_filter = {} columns = [ grids.GridColumn( "Name", key="name" ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), @@ -374,6 +457,18 @@ trans.sa_session.flush() @web.expose + def name_autocomplete_data( self, trans, q=None, limit=None, timestamp=None ): + """Return autocomplete data for history names""" + user = trans.get_user() + if not user: + return + + ac_data = "" + for history in trans.sa_session.query( History ).filter_by( user=user ).filter( func.lower( History.name ) .like(q.lower() + "%") ): + ac_data = ac_data + history.name + "\n" + return ac_data + + @web.expose def imp( self, trans, id=None, confirm=False, **kwd ): """Import another user's history via a shared URL""" msg = "" diff -r 80915982fdb2 -r f776fa6045ba lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Tue Nov 03 11:28:34 2009 -0500 +++ b/lib/galaxy/web/framework/helpers/grids.py Tue Nov 03 12:58:13 2009 -0500 @@ -183,6 +183,7 @@ query=query, cur_page_num = page_num, num_pages = num_pages, + default_filter_dict=self.default_filter, cur_filter_dict=cur_filter_dict, sort_key=sort_key, encoded_sort_key=encoded_sort_key, diff -r 80915982fdb2 -r f776fa6045ba static/scripts/autocomplete_tagging.js --- a/static/scripts/autocomplete_tagging.js Tue Nov 03 11:28:34 2009 -0500 +++ b/static/scripts/autocomplete_tagging.js Tue Nov 03 12:58:13 2009 -0500 @@ -309,7 +309,7 @@ new_value = new_value.replace(/^\s+|\s+$/g,""); // Too short? - if (new_value.length < 3) + if (new_value.length < 2) return false; // diff -r 80915982fdb2 -r f776fa6045ba templates/history/grid.mako --- a/templates/history/grid.mako Tue Nov 03 11:28:34 2009 -0500 +++ b/templates/history/grid.mako Tue Nov 03 12:58:13 2009 -0500 @@ -29,71 +29,20 @@ }); // 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. - // - } - }); + var t = $("#input-tags-filter"); - // 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 }; + { selectFirst: false, autoFill: false, highlight: false, mustMatch: false }; t.autocomplete("${h.url_for( controller='tag', action='tag_autocomplete_data', item_class='History' )}", autocomplete_options); - - $("#page-select").change(navigate_to_page); + // Set up autocomplete for name filter input. + var t2 = $("#input-name-filter"); + + var autocomplete_options = + { selectFirst: false, autoFill: false, highlight: false, mustMatch: false }; + + t2.autocomplete("${h.url_for( controller='history', action='name_autocomplete_data' )}", autocomplete_options); }); ## Can this be moved into base.mako? %if refresh_frames: @@ -125,21 +74,52 @@ %endif %endif + // Filter and sort args for grid. + var filter_args = ${h.to_json_string(cur_filter_dict)}; + var sort_key = "${sort_key}"; + // - // Add a tag to the current grid filter; this adds the tag to the filter and then issues a request to refresh the grid. + // Add tag to grid filter. // 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); + // Put tag name and value together. + var tag = tag_name + (tag_value != null && tag_value != "" ? ":" + tag_value : ""); + add_condition_to_grid_filter("tags", tag, true); + } + + // + // Add a filter to the current grid filter; this adds the filter and then issues a request to refresh the grid. + // + function add_condition_to_grid_filter(name, value, append) + { + // Update filter arg with new condition. + if (append) + { + // Append value. + var cur_val = filter_args[name]; + if (cur_val != "All") + cur_val = cur_val + ", " + value; + else + cur_val = value; + filter_args[name] = cur_val; + } + else + { + // Replace value. + filter_args[name] = value; + } + + // Build URL with filter args, sort key. + var filter_arg_value_strs = new Array(); + var i = 0; + for (arg in filter_args) + { + filter_arg_value_strs[i++] = "f-" + arg + "=" + filter_args[arg]; + } + var filter_str = filter_arg_value_strs.join("&"); + var url_base = "${h.url_for( controller='history', action='list')}"; + var url = url_base + "?" + filter_str + "&sort=" + sort_key; self.location = url; } @@ -154,7 +134,7 @@ var url = url_base.replace("PAGE", page_num); self.location = url; } - + </script> </%def> @@ -175,47 +155,95 @@ <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> + + ## Search box and more options filter at top of grid. + <div> + ## Grid search. TODO: use more elegant way to get free text search column. + <% column = grid.columns[-1] %> + <% use_form = False %> + %for i, filter in enumerate( column.get_accepted_filters() ): + %if i > 0: + <span>|</span> + %endif + %if column.key in cur_filter_dict and cur_filter_dict[column.key] == filter.args[column.key]: + <span class="filter" "style='font-style: italic'">${filter.label}</span> + %elif filter.label == "FREETEXT": + <form name="history_actions" + action="javascript:add_condition_to_grid_filter($('#input-${column.key}-filter').attr('name'),$('#input-${column.key}-filter').attr('value'),false)" + method="get" > + ${column.label}: + %if column.key in cur_filter_dict and cur_filter_dict[column.key] != "All": + <span style="font-style: italic">${cur_filter_dict[column.key]}</span> + <% filter_all = GridColumnFilter( "", { column.key : "All" } ) %> + <a href="${url( filter_all.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a> + | %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> + <span><input id="input-${column.key}-filter" name="${column.key}" type="text" value="" size="15"/></span> + <% use_form = True %> + %else: + <span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span> %endif %endfor - - ## Link to clear all filters. TODO: this should be the default filter or an empty filter. - <% - args = { "deleted" : "False", "tags" : "All" } - no_filter = GridColumnFilter("Clear Filter", args) - %> - <span><a href="${url( no_filter.get_url_args() )}">${no_filter.label}</a></span> - </form> + | <a href="" onclick="javascript:$('#more-search-options').slideToggle('fast');return false;">Advanced Search</a> + %if use_form: + </form> + %endif + </div> + + ## Advanced Search + <div id="more-search-options" style="display: none; padding-top: 5px"> + <table style="border: 1px solid gray;"> + <tr><td style="text-align: left" colspan="100"> + Advanced Search | + <a href=""# onclick="javascript:$('#more-search-options').slideToggle('fast');return false;">Close</a> | + ## Link to clear all filters. + <% + no_filter = GridColumnFilter("Clear All", default_filter_dict) + %> + <a href="${url( no_filter.get_url_args() )}">${no_filter.label}</a> + </td></tr> + %for column in grid.columns: + %if column.filterable: + <tr> + ## Show div if current filter has value that is different from the default filter. + %if cur_filter_dict[column.key] != default_filter_dict[column.key]: + <script type="text/javascript"> + $('#more-search-options').css("display", "block"); + </script> + %endif + <td style="padding-left: 10px">${column.label.lower()}:</td> + <td> + <% use_form = False %> + %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> + %elif filter.label == "FREETEXT": + <form name="history_actions" action="javascript:add_condition_to_grid_filter($('#input-${column.key}-filter').attr('name'),$('#input-${column.key}-filter').attr('value'),true)" + method="get" > + %if column.key in cur_filter_dict and cur_filter_dict[column.key] != "All": + <span style="font-style: italic">${cur_filter_dict[column.key]}</span> + <% filter_all = GridColumnFilter( "", { column.key : "All" } ) %> + <a href="${url( filter_all.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a> + | + %endif + <span><input id="input-${column.key}-filter" name="${column.key}" type="text" value="" size="15"/></span> + <% use_form = True %> + %else: + <span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span> + %endif + %endfor + %if use_form: + </form> + %endif + </td> + </tr> + %endif + %endfor + </table> + </div> </div> <form name="history_actions" action="${url()}" method="post" > <input type="hidden" name="page" value="${cur_page_num}"> @@ -291,7 +319,7 @@ extra = "" %> %if href: - <td><div class="menubutton split" style="float: left;"><a class="label" href="${href}">${v}${extra}</a> </td> + <td><div class="menubutton split" style="float: left;"><a class="label" href="${href}">${v}</a>${extra}</td> %else: <td >${v}${extra}</td> %endif diff -r 80915982fdb2 -r f776fa6045ba test/base/twilltestcase.py --- a/test/base/twilltestcase.py Tue Nov 03 11:28:34 2009 -0500 +++ b/test/base/twilltestcase.py Tue Nov 03 12:58:13 2009 -0500 @@ -1,7 +1,7 @@ import pkg_resources pkg_resources.require( "twill==0.9" ) -import StringIO, os, sys, random, filecmp, time, unittest, urllib, logging, difflib, zipfile, tempfile +import StringIO, os, sys, random, filecmp, time, unittest, urllib, logging, difflib, zipfile, tempfile, re from itertools import * import twill @@ -311,20 +311,20 @@ def view_stored_active_histories( self, check_str='' ): self.home() self.visit_page( "history/list" ) - self.check_page_for_string( 'Stored histories' ) + self.check_page_for_string( 'Saved Histories' ) self.check_page_for_string( '<input type="checkbox" name="id" value=' ) - self.check_page_for_string( 'operation=Rename&id' ) - self.check_page_for_string( 'operation=Switch&id' ) - self.check_page_for_string( 'operation=Delete&id' ) + self.check_page_for_string( 'operation=Rename' ) + self.check_page_for_string( 'operation=Switch' ) + self.check_page_for_string( 'operation=Delete' ) if check_str: self.check_page_for_string( check_str ) self.home() def view_stored_deleted_histories( self, check_str='' ): self.home() self.visit_page( "history/list?f-deleted=True" ) - self.check_page_for_string( 'Stored histories' ) + self.check_page_for_string( 'Saved Histories' ) self.check_page_for_string( '<input type="checkbox" name="id" value=' ) - self.check_page_for_string( 'operation=Undelete&id' ) + self.check_page_for_string( 'operation=Undelete' ) if check_str: self.check_page_for_string( check_str ) self.home() @@ -723,14 +723,14 @@ # Functions associated with browsers, cookies, HTML forms and page visits def check_page_for_string( self, patt ): - """Looks for 'patt' in the current browser page""" + """Looks for 'patt' in the current browser page""" page = self.last_page() for subpatt in patt.split(): if page.find( patt ) == -1: fname = self.write_temp_file( page ) errmsg = "no match to '%s'\npage content written to '%s'" % ( patt, fname ) raise AssertionError( errmsg ) - + def write_temp_file( self, content ): fd, fname = tempfile.mkstemp( suffix='.html', prefix='twilltestcase-' ) f = os.fdopen( fd, "w" ) diff -r 80915982fdb2 -r f776fa6045ba test/functional/test_history_functions.py --- a/test/functional/test_history_functions.py Tue Nov 03 11:28:34 2009 -0500 +++ b/test/functional/test_history_functions.py Tue Nov 03 12:58:13 2009 -0500 @@ -179,7 +179,7 @@ self.share_current_history( regular_user1.email, check_str=history3.name ) # Check out list of histories to make sure history3 was shared - self.view_stored_active_histories( check_str='operation=sharing">shared' ) + self.view_stored_active_histories( check_str='operation=sharing' ) # Enable importing history3 via a URL self.enable_import_via_link( self.security.encode_id( history3.id ), check_str='Unshare',