details: http://www.bx.psu.edu/hg/galaxy/rev/e7b9d15e8e93 changeset: 2797:e7b9d15e8e93 user: jeremy goecks <jeremy.goecks at emory.edu> date: Tue Sep 29 17:57:45 2009 -0400 description: Added grid to view/filter datasets. To view datasets grid: User (tab)-->My Data-->Datasets 7 file(s) affected in this change: lib/galaxy/web/controllers/dataset.py lib/galaxy/web/controllers/root.py lib/galaxy/web/framework/helpers/grids.py templates/base_panels.mako templates/dataset/grid.mako templates/history/grid.mako templates/my_data.mako diffs (590 lines): diff -r b819249af24d -r e7b9d15e8e93 lib/galaxy/web/controllers/dataset.py --- a/lib/galaxy/web/controllers/dataset.py Tue Sep 29 17:14:20 2009 -0400 +++ b/lib/galaxy/web/controllers/dataset.py Tue Sep 29 17:57:45 2009 -0400 @@ -1,6 +1,8 @@ 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 @@ -42,7 +44,97 @@ (This is an automated message). """ +class HistoryDatasetAssociationListGrid( grids.Grid ): + 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 = "<div id=%s></div>" % 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: + return "deleted" + return "" + 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 datasets" + model_class = model.HistoryDatasetAssociation + template='/dataset/grid.mako' + default_sort_key = "-create_time" + columns = [ + grids.GridColumn( "Name", key="name", + # Link name to dataset's history. + link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ) ), + TagsColumn( "Tags", key="tags", filterable=True ), + 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 ), + ] + operations = [] + standard_filters = [] + default_filter = dict( deleted="False", tags="All" ) + preserve_state = False + use_paging = True + num_rows_per_page = 10 + def apply_default_filter( self, trans, query, **kwargs ): + # This is a somewhat obtuse way to join the History and HDA tables. However, it's necessary + # because the initial query in build_initial_query is specificied on the HDA table (this is reasonable) + # and there's no simple property in the HDA to do the join. + return query.select_from( model.HistoryDatasetAssociation.table.join( model.History.table ) ).filter( model.History.user == trans.user ) + class DatasetInterface( BaseController ): + + stored_list_grid = HistoryDatasetAssociationListGrid() @web.expose def errors( self, trans, id ): @@ -136,7 +228,50 @@ raise paste.httpexceptions.HTTPNotFound( "File Not Found (%s)." % ( filename ) ) else: return trans.show_error_message( "You are not allowed to access this dataset" ) - + + @web.expose + @web.require_login( "see all available datasets" ) + def list( self, trans, **kwargs ): + """List all available datasets""" + status = message = None + + if 'operation' in kwargs: + operation = kwargs['operation'].lower() + hda_ids = util.listify( kwargs.get( 'id', [] ) ) + + # Display no message by default + status, message = None, None + + # Load the hdas and ensure they all belong to the current user + hdas = [] + for encoded_hda_id in hda_ids: + hda_id = trans.security.decode_id( encoded_hda_id ) + hda = trans.sa_session.query( model.HistoryDatasetAssociation ).filter_by( id=hda_id ).first() + if hda: + # Ensure history is owned by current user + if hda.history.user_id != None and trans.user: + assert trans.user.id == hda.history.user_id, "HistoryDatasetAssocation does not belong to current user" + hdas.append( hda ) + else: + log.warn( "Invalid history_dataset_association id '%r' passed to list", hda_id ) + + if hdas: + + if operation == "switch": + # Convert hda to histories. + histories = [] + for hda in hdas: + histories.append( hda.history ) + + # Use history controller to switch the history. TODO: is this reasonable? + status, message = trans.webapp.controllers['history']._list_switch( trans, histories ) + + # Current history changed, refresh history frame + trans.template_context['refresh_frames'] = ['history'] + + # Render the list view + return self.stored_list_grid( trans, status=status, message=message, **kwargs ) + @web.expose def display_at( self, trans, dataset_id, filename=None, **kwd ): """Sets up a dataset permissions so it is viewable at an external site""" @@ -222,7 +357,7 @@ new_history = trans.app.model.History() if new_history_name: new_history.name = new_history_name - new_history.user = user + new_history_name = user new_history.flush() target_history_ids.append( new_history.id ) if user: diff -r b819249af24d -r e7b9d15e8e93 lib/galaxy/web/controllers/root.py --- a/lib/galaxy/web/controllers/root.py Tue Sep 29 17:14:20 2009 -0400 +++ b/lib/galaxy/web/controllers/root.py Tue Sep 29 17:57:45 2009 -0400 @@ -46,6 +46,13 @@ yield "</body></html>" ## ---- Root history display --------------------------------------------- + + @web.expose + def my_data( self, trans ): + """ + Display user's data. + """ + return trans.fill_template_mako( "/my_data.mako" ) @web.expose def history( self, trans, as_xml=False, show_deleted=False ): diff -r b819249af24d -r e7b9d15e8e93 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Tue Sep 29 17:14:20 2009 -0400 +++ b/lib/galaxy/web/framework/helpers/grids.py Tue Sep 29 17:57:45 2009 -0400 @@ -70,18 +70,19 @@ use_default_filter_str = kwargs.get( 'use_default_filter' ) use_default_filter = False if use_default_filter_str: - use_default_filter = use_default_filter_str.lower() == 'true' + use_default_filter = ( use_default_filter_str.lower() == 'true' ) # Process filtering arguments to (a) build a query that represents the filter and (b) builds a # dictionary that denotes the current filter. cur_filter_dict = {} for column in self.columns: if column.key: - # Get the filter criterion for the column. Precedence is (a) if using default filter, look there; (b) look in kwargs; and (c) look in - # base filter. + # Get the filter criterion for the column. Precedence is (a) if using default filter, only look there; otherwise, + # (b) look in kwargs; and (c) look in base filter. column_filter = None - if use_default_filter and self.default_filter: - column_filter = self.default_filter.get( column.key ) + if use_default_filter: + if self.default_filter: + column_filter = self.default_filter.get( column.key ) elif "f-" + column.key in kwargs: column_filter = kwargs.get( "f-" + column.key ) elif column.key in base_filter: @@ -130,7 +131,6 @@ # be computed. total_num_rows = query.count() query = query.limit( self.num_rows_per_page ).offset( ( page_num-1 ) * self.num_rows_per_page ) - num_pages = int ( math.ceil( float( total_num_rows ) / self.num_rows_per_page ) ) else: # Defaults. diff -r b819249af24d -r e7b9d15e8e93 templates/base_panels.mako --- a/templates/base_panels.mako Tue Sep 29 17:14:20 2009 -0400 +++ b/templates/base_panels.mako Tue Sep 29 17:57:45 2009 -0400 @@ -217,6 +217,7 @@ </ul> <ul class="loggedin-only" style="${style2}"> <li>Logged in as <span id="user-email">${user_email}</span></li> + <li><a target="galaxy_main" href="${h.url_for( controller='root', action='my_data' )}">My Data</a></li> %if app.config.use_remote_user: %if app.config.remote_user_logout_href: <li><a href="${app.config.remote_user_logout_href}" target="_top">Logout</a></li> diff -r b819249af24d -r e7b9d15e8e93 templates/dataset/grid.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/dataset/grid.mako Tue Sep 29 17:57:45 2009 -0400 @@ -0,0 +1,322 @@ +<%! from galaxy.web.framework.helpers.grids import GridColumnFilter %> + +<%inherit file="/base.mako"/> +<%def name="title()">${grid.title}</%def> + +%if message: + <p> + <div class="${message_type}message transient-message">${message}</div> + <div style="clear: both"></div> + </p> +%endif + +<%def name="javascripts()"> + ${parent.javascripts()} + ${h.js("jquery.autocomplete", "autocomplete_tagging" )} + <script type="text/javascript"> + ## TODO: generalize and move into galaxy.base.js + $(document).ready(function() { + $(".grid").each( function() { + var grid = this; + var checkboxes = $(this).find("input.grid-row-select-checkbox"); + var update = $(this).find( "span.grid-selected-count" ); + $(checkboxes).each( function() { + $(this).change( function() { + var n = $(checkboxes).filter("[checked]").size(); + update.text( n ); + }); + }) + }); + + // 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='HistoryDatasetAssociation' )}", autocomplete_options); + + //t.addClass("tag-input"); + + return t; + }); + ## Can this be moved into base.mako? + %if refresh_frames: + %if 'masthead' in refresh_frames: + ## Refresh masthead == user changes (backward compatibility) + if ( parent.user_changed ) { + %if trans.user: + parent.user_changed( "${trans.user.email}", ${int( app.config.is_admin_user( trans.user ) )} ); + %else: + parent.user_changed( null, false ); + %endif + } + %endif + %if 'history' in refresh_frames: + if ( parent.frames && parent.frames.galaxy_history ) { + parent.frames.galaxy_history.location.href="${h.url_for( controller='root', action='history')}"; + if ( parent.force_right_panel ) { + parent.force_right_panel( 'show' ); + } + } + %endif + %if 'tools' in refresh_frames: + if ( parent.frames && parent.frames.galaxy_tools ) { + parent.frames.galaxy_tools.location.href="${h.url_for( controller='root', action='tool_menu')}"; + if ( parent.force_left_panel ) { + parent.force_left_panel( 'show' ); + } + } + %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> + +<%def name="stylesheets()"> + ${h.css( "base", "autocomplete_tagging" )} + <style> + ## Not generic to all grids -- move to base? + .count-box { + min-width: 1.1em; + padding: 5px; + border-width: 1px; + border-style: solid; + text-align: center; + display: inline-block; + } + </style> +</%def> + +<div class="grid-header"> + <h2>${grid.title}</h2> + + ## Print grid filter. + <form name="dataset_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 + %endfor + </form> +</div> +<form name="dataset_actions" action="${url()}" method="post" > + <table class="grid"> + <thead> + <tr> + %for column in grid.columns: + %if column.visible: + <% + href = "" + extra = "" + if column.sortable: + if sort_key == column.key: + if sort_order == "asc": + href = url( sort=( "-" + column.key ) ) + extra = "↓" + else: + href = url( sort=( column.key ) ) + extra = "↑" + else: + href = url( sort=column.key ) + %> + <th\ + %if column.ncells > 1: + colspan="${column.ncells}" + %endif + > + %if href: + <a href="${href}">${column.label}</a> + %else: + ${column.label} + %endif + <span>${extra}</span> + </th> + %endif + %endfor + <th></th> + </tr> + </thead> + <tbody> + %for i, item in enumerate( query ): + <tr \ + %if current_item == item: + class="current" \ + %endif + > + ## Data columns + %for column in grid.columns: + %if column.visible: + <% + # Link + link = column.get_link( trans, grid, item ) + if link: + href = url( **link ) + else: + href = None + # Value (coerced to list so we can loop) + value = column.get_value( trans, grid, item ) + if column.ncells == 1: + value = [ value ] + %> + %for cellnum, v in enumerate( value ): + <% + # Attach popup menu? + if column.attach_popup and cellnum == 0: + extra = '<a id="grid-%d-popup" class="arrow" style="display: none;"><span>▼</span></a>' % i + else: + extra = "" + %> + %if href: + <td><div class="menubutton split" style="float: left;"><a class="label" href="${href}">${v}${extra}</a> </td> + %else: + <td >${v}${extra}</td> + %endif + %endfor + %endif + %endfor + ## Actions column + <td> + <div popupmenu="grid-${i}-popup"> + %for operation in grid.operations: + %if operation.allowed( item ): + <a class="action-button" href="${url( operation=operation.label, id=item.id )}">${operation.label}</a> + %endif + %endfor + </div> + </td> + </tr> + %endfor + </tbody> + <tfoot> + %if num_pages > 1: + <tr> + <td></td> + <td colspan="100" style="text-align: right"> + Page: + %for page_index in range(1, num_pages + 1): + %if page_index == cur_page_num: + <span style="font-style: italic">${page_index}</span> + %else: + <% args = { "page" : page_index } %> + <span><a href="${url( args )}">${page_index}</a></span> + %endif + %endfor + </td> + </tr> + %endif + %if grid.operations: + <tr> + <td></td> + <td colspan="100"> + For <span class="grid-selected-count"></span> selected histories: + %for operation in grid.operations: + %if operation.allow_multiple: + <input type="submit" name="operation" value="${operation.label}" class="action-button"> + %endif + %endfor + </td> + </tr> + %endif + </tfoot> + </table> +</form> diff -r b819249af24d -r e7b9d15e8e93 templates/history/grid.mako --- a/templates/history/grid.mako Tue Sep 29 17:14:20 2009 -0400 +++ b/templates/history/grid.mako Tue Sep 29 17:57:45 2009 -0400 @@ -303,7 +303,7 @@ %if num_pages > 1: <tr> <td></td> - <td colspan="100" style="font-size: 90%; text-align: right"> + <td colspan="100" style="text-align: right"> Page: %for page_index in range(1, num_pages + 1): %if page_index == cur_page_num: diff -r b819249af24d -r e7b9d15e8e93 templates/my_data.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/templates/my_data.mako Tue Sep 29 17:57:45 2009 -0400 @@ -0,0 +1,13 @@ +<%inherit file="/base.mako"/> + +<%def name="stylesheets()"> + ${h.css( "base" )} +</%def> + +<h2>Available Galaxy Data</h2> + +<ul> + <li><a href='${h.url_for( controller='history', action='list' )}'>Histories</li> + <li><a href='${h.url_for( controller='dataset', action='list' )}'>Datasets</li> +</ul> +