diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 013a16484d4..fc60017c299 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -246,7 +246,7 @@ class History( object ): # This needs to be a list return [ hda for hda in self.datasets if not hda.dataset.deleted ] def get_display_name( self ): - ## History name can be either a string or a unicode object. If string, convert to unicode object assuming 'utf-8' format. + """ History name can be either a string or a unicode object. If string, convert to unicode object assuming 'utf-8' format. """ history_name = self.name if isinstance(history_name, str): history_name = unicode(history_name, 'utf-8') diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index a472ba8e969..49276fc4fdd 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -1,7 +1,6 @@ import logging, os, string, shutil, re, socket, mimetypes, smtplib, urllib from galaxy.web.base.controller import * -from galaxy.tags.tag_handler import TagHandler from galaxy.web.framework.helpers import time_ago, iff, grids from galaxy import util, datatypes, jobs, web, model from cgi import escape, FieldStorage @@ -49,55 +48,7 @@ class HistoryDatasetAssociationListGrid( grids.Grid ): class HistoryColumn( grids.GridColumn ): def get_value( self, trans, grid, hda): return hda.history.name - - class StatusColumn( grids.GridColumn ): - def get_value( self, trans, grid, hda ): - if hda.deleted: - return "deleted" - return "" - def get_link( self, trans, grid, hda ): - return None - class TagsColumn( grids.GridColumn ): - 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, hda ): - self.tag_elt_id_gen += 1 - elt_id="tagging-elt" + str( self.tag_elt_id_gen ) - div_elt = "
" % elt_id - return div_elt + trans.fill_template( "/tagging_common.mako", trans=trans, tagged_item=hda, - 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 hdas 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( model.HistoryDatasetAssociation.tags.any( tag_id=tag.id ) ) - if value: - query = query.filter( model.HistoryDatasetAssociation.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( model.HistoryDatasetAssociation.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 StatusColumn( grids.GridColumn ): def get_value( self, trans, grid, hda ): if hda.deleted: @@ -118,19 +69,25 @@ class HistoryDatasetAssociationListGrid( grids.Grid ): template='/dataset/grid.mako' default_sort_key = "-create_time" columns = [ - grids.GridColumn( "Name", key="name", + grids.TextColumn( "Name", key="name", model_class=model.HistoryDatasetAssociation, # Link name to dataset's history. - link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ) ), + link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ), filterable="advanced" ), HistoryColumn( "History", key="history", link=( lambda item: iff( item.history.deleted, None, dict( operation="switch_history", id=item.id ) ) ) ), - TagsColumn( "Tags", key="tags", filterable=True ), + grids.TagsColumn( "Tags", "tags", model.HistoryDatasetAssociation, model.HistoryDatasetAssociationTagAssociation, filterable="advanced" ), StatusColumn( "Status", key="deleted", attach_popup=False ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), ] + columns.append( + grids.MulticolFilterColumn( + "Search", + cols_to_filter=[ columns[0], columns[2] ], + key="free-text-search", visible=False, filterable="default" ) + ) operations = [] standard_filters = [] - default_filter = dict( deleted="False", tags="All" ) + default_filter = dict( name="All", deleted="False", tags="All" ) preserve_state = False use_paging = True num_rows_per_page = 50 diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py index a4f4e944bcb..b3cd3cdc8eb 100644 --- a/lib/galaxy/web/controllers/history.py +++ b/lib/galaxy/web/controllers/history.py @@ -2,7 +2,6 @@ from galaxy.web.base.controller import * 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.util.odict import odict @@ -19,30 +18,10 @@ SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" class HistoryListGrid( grids.Grid ): # Custom column types - class NameColumn( grids.GridColumn ): - def __init( self, key, link, attach_popup, filterable ): - grids.GridColumn.__init__(self, key, link, attach_popup) - - def get_value( self, trans, grid, history ): + class NameColumn( grids.TextColumn ): + 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 = [] @@ -53,6 +32,7 @@ class HistoryListGrid( grids.Grid ): else: rval.append( '' ) return rval + class StatusColumn( grids.GridColumn ): def get_value( self, trans, grid, history ): if history.deleted: @@ -66,44 +46,6 @@ class HistoryListGrid( grids.Grid ): 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) - # 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 = "" % 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", tag_click_fn="add_tag_to_grid_filter" ) - def filter( self, db_session, query, column_filter ): - """ Modify query to filter histories by tag. """ - 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(): - if name: - # Search for tag names. - query = query.filter( History.tags.any( func.lower( model.HistoryTagAssociation.user_tname ).like( "%" + name.lower() + "%" ) ) ) - if value: - # 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 = 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 ): @@ -122,12 +64,12 @@ class HistoryListGrid( grids.Grid ): pass elif column_filter: if column_filter == "private": - query = query.filter( History.users_shared_with == None ) - query = query.filter( History.importable == False ) + query = query.filter( model.History.users_shared_with == None ) + query = query.filter( model.History.importable == False ) elif column_filter == "shared": - query = query.filter( History.users_shared_with != None ) + query = query.filter( model.History.users_shared_with != None ) elif column_filter == "importable": - query = query.filter( History.importable == True ) + query = query.filter( model.History.importable == True ) return query def get_accepted_filters( self ): """ Returns a list of accepted filters for this column. """ @@ -141,43 +83,6 @@ class HistoryListGrid( grids.Grid ): 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 = "Saved Histories" @@ -185,19 +90,25 @@ class HistoryListGrid( grids.Grid ): template='/history/grid.mako' default_sort_key = "-create_time" columns = [ - NameColumn( "Name", key="name", + NameColumn( "Name", key="name", model_class=model.History, link=( lambda history: iff( history.deleted, None, dict( operation="switch", id=history.id ) ) ), - attach_popup=True, filterable=True ), + attach_popup=True, filterable="advanced" ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), - TagsColumn( "Tags", key="tags", filterable=True), + grids.TagsColumn( "Tags", "tags", model.History, model.HistoryTagAssociation, filterable="advanced"), StatusColumn( "Status", attach_popup=False ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), # 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. + DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ), + SharingColumn( "Shared", key="shared", visible=False, filterable="advanced" ), ] + columns.append( + grids.MulticolFilterColumn( + "Search", + cols_to_filter=[ columns[0], columns[2] ], + key="free-text-search", visible=False, filterable="default" ) + ) + operations = [ grids.GridOperation( "Switch", allow_multiple=False, condition=( lambda item: not item.deleted ) ), grids.GridOperation( "Share", condition=( lambda item: not item.deleted ) ), @@ -464,7 +375,7 @@ class HistoryController( BaseController ): return ac_data = "" - for history in trans.sa_session.query( History ).filter_by( user=user ).filter( func.lower( History.name ) .like(q.lower() + "%") ): + for history in trans.sa_session.query( model.History ).filter_by( user=user ).filter( func.lower( model.History.name ) .like(q.lower() + "%") ): ac_data = ac_data + history.name + "\n" return ac_data diff --git a/lib/galaxy/web/controllers/tag.py b/lib/galaxy/web/controllers/tag.py index 164e2ffdca0..f8cd6df499f 100644 --- a/lib/galaxy/web/controllers/tag.py +++ b/lib/galaxy/web/controllers/tag.py @@ -105,7 +105,7 @@ class TagsController ( BaseController ): # 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) + from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table) 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() ] @@ -154,7 +154,7 @@ class TagsController ( BaseController ): # Build select statement. cols_to_select = [ item_tag_assoc_class.table.c.value, func.count('*') ] - from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag) + from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table) 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 + "%")) diff --git a/lib/galaxy/web/framework/helpers/grids.py b/lib/galaxy/web/framework/helpers/grids.py index 39f47286958..d8503ce7945 100644 --- a/lib/galaxy/web/framework/helpers/grids.py +++ b/lib/galaxy/web/framework/helpers/grids.py @@ -1,6 +1,7 @@ from galaxy.model import * from galaxy.model.orm import * +from galaxy.tags.tag_handler import TagHandler from galaxy.web import url_for from galaxy.util.json import from_json_string, to_json_string @@ -87,18 +88,51 @@ class Grid( object ): column_filter = kwargs.get( "f-" + column.key ) elif column.key in base_filter: column_filter = base_filter.get( column.key ) - + + # Method (1) combines a mix of strings and lists of strings into a single string and (2) attempts to de-jsonify all strings. + def from_json_string_recurse(item): + decoded_list = [] + if isinstance( item, basestring): + try: + # Not clear what we're decoding, so recurse to ensure that we catch everything. + decoded_item = from_json_string( item ) + if isinstance( decoded_item, list): + decoded_list = from_json_string_recurse( decoded_item ) + else: + decoded_list = [ str( decoded_item ) ] + except ValueError: + decoded_list = [ str( item ) ] + elif isinstance( item, list): + return_val = [] + for element in item: + a_list = from_json_string_recurse( element ) + decoded_list = decoded_list + a_list + return decoded_list + # If column filter found, apply it. if column_filter is not None: + # TextColumns may have a mix of json and strings. + if isinstance( column, TextColumn ): + column_filter = from_json_string_recurse( column_filter ) + if len( column_filter ) == 1: + column_filter = column_filter[0] # 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; make sure filter is a string so + # Carry filter along to newly generated urls; make sure filter is a string so # that we can encode to UTF-8 and thus handle user input to filters. - if not isinstance( column_filter, basestring ): - column_filter = unicode(column_filter) - extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8") + if isinstance( column_filter, list ): + # Filter is a list; process each item. + for filter in column_filter: + if not isinstance( filter, basestring ): + filter = unicode( filter ).encode("utf-8") + extra_url_args[ "f-" + column.key ] = to_json_string( column_filter ) + else: + # Process singleton filter. + if not isinstance( column_filter, basestring ): + column_filter = unicode(column_filter) + extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8") # Process sort arguments. sort_key = sort_order = None @@ -218,9 +252,12 @@ class Grid( object ): return query class GridColumn( object ): - def __init__( self, label, key=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1, filterable=False ): + def __init__( self, label, key=None, model_class=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1, + # Valid values for filterable are ['default', 'advanced', None] + filterable=None ): self.label = label self.key = key + self.model_class = model_class self.method = method self.format = format self.link = link @@ -265,6 +302,88 @@ class GridColumn( object ): args = { self.key: val } accepted_filters.append( GridColumnFilter( val, args) ) return accepted_filters + +# Generic column that employs freetext and, hence, supports freetext, case-independent filtering. +class TextColumn( GridColumn ): + def filter( self, db_session, query, column_filter ): + """ Modify query to filter using free text, case independence. """ + if column_filter == "All": + pass + elif column_filter: + query = query.filter( self.get_filter( column_filter ) ) + return query + def get_filter( self, column_filter ): + """ Returns a SQLAlchemy criterion derived from column_filter. """ + # This is a pretty ugly way to get the key attribute of model_class. TODO: Can this be fixed? + model_class_key_field = eval( "self.model_class." + self.key ) + + if isinstance( column_filter, basestring ): + return func.lower( model_class_key_field ).like( "%" + column_filter.lower() + "%" ) + elif isinstance( column_filter, list ): + composite_filter = True + for filter in column_filter: + composite_filter = and_( composite_filter, func.lower( model_class_key_field ).like( "%" + filter.lower() + "%" ) ) + return composite_filter + +# Generic column that supports tagging. +class TagsColumn( TextColumn ): + def __init__( self, col_name, key, model_class, model_tag_association_class, filterable ): + GridColumn.__init__(self, col_name, key=key, model_class=model_class, filterable=filterable) + self.model_tag_association_class = model_tag_association_class + # Tags cannot be sorted. + self.sortable = False + self.tag_elt_id_gen = 0 + def get_value( self, trans, grid, item ): + self.tag_elt_id_gen += 1 + elt_id="tagging-elt" + str( self.tag_elt_id_gen ) + div_elt = "" % elt_id + return div_elt + trans.fill_template( "/tagging_common.mako", trans=trans, tagged_item=item, + 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 filter model_class by tag. Multiple filters are ANDed. """ + if column_filter == "All": + pass + elif column_filter: + query = query.filter( self.get_filter( column_filter ) ) + return query + def get_filter( self, column_filter ): + # Parse filter to extract multiple tags. + tag_handler = TagHandler() + if isinstance( column_filter, list ): + # Collapse list of tags into a single string; this is redundant but effective. TODO: fix this by iterating over tags. + column_filter = ",".join( column_filter ) + raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") ) + filter = True + for name, value in raw_tags.items(): + if name: + # Search for tag names. + filter = and_( filter, self.model_class.tags.any( func.lower( self.model_tag_association_class.user_tname ).like( "%" + name.lower() + "%" ) ) ) + if value: + # Search for tag values. + filter = and_( filter, self.model_class.tags.any( func.lower( self.model_tag_association_class.user_value ).like( "%" + value.lower() + "%" ) ) ) + return filter + +# Column that performs multicolumn filtering. +class MulticolFilterColumn( TextColumn ): + def __init__( self, col_name, cols_to_filter, key, visible, filterable="default" ): + GridColumn.__init__( self, col_name, key=key, visible=visible, filterable=filterable) + self.cols_to_filter = cols_to_filter + def filter( self, db_session, query, column_filter ): + """ Modify query to filter model_class by tag. Multiple filters are ANDed. """ + if column_filter == "All": + return query + if isinstance( column_filter, list): + composite_filter = True + for filter in column_filter: + part_composite_filter = False + for column in self.cols_to_filter: + part_composite_filter = or_( part_composite_filter, column.get_filter( filter ) ) + composite_filter = and_( composite_filter, part_composite_filter ) + else: + composite_filter = False + for column in self.cols_to_filter: + composite_filter = or_( composite_filter, column.get_filter( column_filter ) ) + return query.filter( composite_filter ) class GridOperation( object ): def __init__( self, label, key=None, condition=None, allow_multiple=True, allow_popup=True, target=None, url_args=None ): diff --git a/static/scripts/jquery.wymeditor.js b/static/scripts/jquery.wymeditor.js index b13e2d02f66..34c42342cf0 100644 --- a/static/scripts/jquery.wymeditor.js +++ b/static/scripts/jquery.wymeditor.js @@ -295,6 +295,66 @@ jQuery.extend(WYMeditor, { }); +/* + Galaxy code that integrates into the WYM Editor. + */ +var Galaxy = +{ + /* + Galaxy constants for WYM Editor: + TOOLS - A string replaced by the galaxy toolbar's HTML. + TOOLS_ITEMS - A string replaced by the galaxy toolbar items. + INSERT_HISTORY - Command: open the insert history dialog. + INSERT_DATASET - Command: open the insert dataset dialog. + DIALOG_HISTORY - A dialog to insert a history. + DIALOG_DATASET - A dialog to insert a dataset. + */ + TOOLS : "{Galaxy_Tools}", + TOOLS_ITEMS : "{Galaxy_Tools_Items}", + INSERT_HISTORY : "InsertHistory", + INSERT_DATASET : "InsertDataset", + DIALOG_HISTORY : "DialogHistory", + DIALOG_DATASET : "DialogDataset", + + // Tool items overview. + toolsItems: [ + {'name': "InsertHistory", 'title': 'History', 'css': 'galaxy_tools_insert_history_link'}, + {'name': "InsertDataset", 'title': 'Dataset', 'css': 'galaxy_dataset'} + ], + + // Tools HTML. + toolsHtml: "