Commits

Ross Lazarus committed 3b3a74f Merge

merge

Comments (0)

Files changed (70)

 master_doc = 'index'
 
 # General information about the project.
-project = u'Galaxy'
+project = u'Galaxy Code'
 copyright = u'2012, Galaxy Team'
 
 # The version info for the project you're documenting, acts as replacement for
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'Galaxy.tex', u'Galaxy Documentation',
+  ('index', 'Galaxy.tex', u'Galaxy Code Documentation',
    u'Galaxy Team', 'manual'),
 ]
 
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'galaxy', u'Galaxy Documentation',
+    ('index', 'galaxy', u'Galaxy Code Documentation',
      [u'Galaxy Team'], 1)
 ]
 
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'Galaxy', u'Galaxy Documentation',
+  ('index', 'Galaxy', u'Galaxy Code Documentation',
    u'Galaxy Team', 'Galaxy', 'Data intensive biology for everyone.',
    'Miscellaneous'),
 ]
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
 #texinfo_show_urls = 'footnote'
+
+# -- ReadTheDocs.org Settings ------------------------------------------------
+
+class Mock(object):
+    def __init__(self, *args, **kwargs):
+        pass
+
+    def __call__(self, *args, **kwargs):
+        return Mock()
+
+    @classmethod
+    def __getattr__(cls, name):
+        if name in ('__file__', '__path__'):
+            return '/dev/null'
+        elif name[0] == name[0].upper():
+            mockType = type(name, (), {})
+            mockType.__module__ = __name__
+            return mockType
+        else:
+            return Mock()
+
+# adding pbs_python, DRMAA_python, markupsafe, and drmaa here had no effect.
+MOCK_MODULES = ['tables', 'decorator']
+for mod_name in MOCK_MODULES:
+    sys.modules[mod_name] = Mock()
-Galaxy Documentation
-********************
+Galaxy Code Documentation
+*************************
 
 Galaxy is an open, web-based platform for accessible, reproducible, and
 transparent computational biomedical research.

doc/source/lib/galaxy.jobs.rst

 
     galaxy.jobs.actions
     galaxy.jobs.deferred
-    galaxy.jobs.rules
     galaxy.jobs.runners
     galaxy.jobs.splitters
 

lib/galaxy/util/__init__.py

     else:
         return False
 
+def string_as_bool_or_none( string ):
+    """
+    Returns True, None or False based on the argument:
+        True if passed True, 'True', 'Yes', or 'On'
+        None if passed None or 'None'
+        False otherwise
+
+    Note: string comparison is case-insensitive so lowecase versions of those
+    function equivalently.
+    """
+    string = str( string ).lower()
+    if string in ( 'true', 'yes', 'on' ):
+        return True
+    elif string == 'none':
+        return None
+    else:
+        return False
+
 def listify( item ):
     """
     Make a single item a single item list, or return a list if passed a

lib/galaxy/util/shed_util.py

 from galaxy.web.form_builder import SelectField
 from galaxy.tools import parameters
 from galaxy.datatypes.checkers import *
+from galaxy.datatypes.sniff import is_column_based
 from galaxy.util.json import *
 from galaxy.util import inflector
 from galaxy.tools.search import ToolBoxSearch
         shutil.copy( full_source_path, os.path.join( dest_path, copied_file ) )
 def copy_sample_files( app, sample_files, tool_path=None, sample_files_copied=None, dest_path=None ):
     """
-    Copy all files to dest_path in the local Galaxy environment that have not already been copied.  Those that have been copied
-    are contained in sample_files_copied.  The default value for dest_path is ~/tool-data.
+    Copy all appropriate files to dest_path in the local Galaxy environment that have not already been copied.  Those that have been copied
+    are contained in sample_files_copied.  The default value for dest_path is ~/tool-data.  We need to be careful to copy only appropriate
+    files here because tool shed repositories can contain files ending in .sample that should not be copied to the ~/tool-data directory.
     """
+    filenames_not_to_copy = [ 'tool_data_table_conf.xml.sample' ]
     sample_files_copied = util.listify( sample_files_copied )
     for filename in sample_files:
-        if filename not in sample_files_copied:
+        filename_sans_path = os.path.split( filename )[ 1 ]
+        if filename_sans_path not in filenames_not_to_copy and filename not in sample_files_copied:
             if tool_path:
                 filename=os.path.join( tool_path, filename )
-            copy_sample_file( app, filename, dest_path=dest_path )
+            # Attempt to ensure we're copying an appropriate file.
+            if is_data_index_sample_file( filename ):
+                copy_sample_file( app, filename, dest_path=dest_path )
 def create_repo_info_dict( repository, owner, repository_clone_url, changeset_revision, ctx_rev, metadata ):
     repo_info_dict = {}
     repo_info_dict[ repository.name ] = ( repository.description,
             if shed_tool_conf == file_name:
                 return index, shed_tool_conf_dict
 def get_tool_index_sample_files( sample_files ):
+    """Try to return the list of all appropriate tool data sample files included in the repository."""
     tool_index_sample_files = []
     for s in sample_files:
-        if s.endswith( '.loc.sample' ):
+        # The problem with this is that Galaxy does not follow a standard naming convention for file names.
+        if s.endswith( '.loc.sample' ) or s.endswith( '.xml.sample' ) or s.endswith( '.txt.sample' ):
             tool_index_sample_files.append( s )
     return tool_index_sample_files
 def get_tool_dependency( trans, id ):
                                                                              parent_id=tool_version_using_parent_id.id )
                 sa_session.add( tool_version_association )
                 sa_session.flush()
+def is_data_index_sample_file( file_path ):
+    """
+    Attempt to determine if a .sample file is appropriate for copying to ~/tool-data when a tool shed repository is being installed
+    into a Galaxy instance.
+    """
+    # Currently most data index files are tabular, so check that first.  We'll assume that if the file is tabular, it's ok to copy.
+    if is_column_based( file_path ):
+        return True
+    # If the file is any of the following, don't copy it.
+    if check_html( file_path ):
+        return False
+    if check_image( file_path ):
+        return False
+    if check_binary( name=file_path ):
+        return False
+    if is_bz2( file_path ):
+        return False
+    if is_gzip( file_path ):
+        return False
+    if check_zip( file_path ):
+        return False
+    # Default to copying the file if none of the above are true.
+    return True
 def is_downloadable( metadata_dict ):
     return 'datatypes' in metadata_dict or 'tools' in metadata_dict or 'workflows' in metadata_dict
 def load_installed_datatype_converters( app, installed_repository_dict, deactivate=False ):

lib/galaxy/webapps/community/config.py

         self.tool_secret = kwargs.get( "tool_secret", "" )
         self.tool_data_path = resolve_path( kwargs.get( "tool_data_path", "shed-tool-data" ), os.getcwd() )
         self.tool_data_table_config_path = resolve_path( kwargs.get( 'tool_data_table_config_path', 'tool_data_table_conf.xml' ), self.root )
+        self.shed_tool_data_table_config = resolve_path( kwargs.get( 'shed_tool_data_table_config', 'shed_tool_data_table_conf.xml' ), self.root )
         self.ftp_upload_dir = kwargs.get( 'ftp_upload_dir', None )
         # Location for dependencies
         if 'tool_dependency_dir' in kwargs:

lib/galaxy/webapps/community/controllers/upload.py

                     # Get the new repository tip.
                     if tip == repository.tip:
                         message = 'No changes to repository.  '
+                        status = 'warning'
                     else:
                         if ( isgzip or isbz2 ) and uncompress_file:
                             uncompress_str = ' uncompressed and '
                                 message += "  %d files were removed from the repository root.  " % len( files_to_remove )
                         kwd[ 'message' ] = message
                         set_repository_metadata_due_to_new_tip( trans, repository, content_alert_str=content_alert_str, **kwd )
+                    #provide a warning message if a tool_dependencies.xml file is provided, but tool dependencies weren't loaded due to e.g. a requirement tag mismatch
+                    if get_config_from_disk( 'tool_dependencies.xml', repo_dir ):
+                        if repository.metadata_revisions:
+                            metadata_dict = repository.metadata_revisions[0].metadata
+                        else:
+                            metadata_dict = {}
+                        if 'tool_dependencies' not in metadata_dict:
+                            message += 'Name, version and type from a tool requirement tag does not match the information in the "tool_dependencies.xml".  '
+                            status = 'warning'
+                            log.debug( 'Error in tool dependencies for repository %s: %s.' % ( repository.id, repository.name ) )
                     # Reset the tool_data_tables by loading the empty tool_data_table_conf.xml file.
                     reset_tool_data_tables( trans.app )
                     trans.response.send_redirect( web.url_for( controller='repository',

lib/galaxy/webapps/galaxy/api/histories.py

 
                 if not dataset_dict['deleted']:
                     state_counts[ item_state ] = state_counts[ item_state ] + 1
-                    state_ids[ item_state ].append( trans.security.encode_id( dataset_dict[ 'id' ] ) )
+
+                state_ids[ item_state ].append( trans.security.encode_id( dataset_dict[ 'id' ] ) )
 
             return ( state_counts, state_ids )
 
                 state = states.NEW
 
             else:
-
                 if( ( state_counts[ states.RUNNING ] > 0 )
                 or    ( state_counts[ states.SETTING_METADATA ] > 0 )
                 or    ( state_counts[ states.UPLOAD ] > 0 ) ):
                     state = states.OK
 
             history_data[ 'state' ] = state
-
             history_data[ 'state_details' ] = state_counts
             history_data[ 'state_ids' ] = state_ids
-
             history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id )
 
-
         except Exception, e:
             msg = "Error in history API at showing history detail: %s" % ( str( e ) )
             log.error( msg, exc_info=True )

lib/galaxy/webapps/galaxy/controllers/root.py

         return trans.fill_template_mako( "/my_data.mako" )
 
     @web.expose
-    def history( self, trans, as_xml=False, show_deleted=False, show_hidden=False, hda_id=None, **kwd ):
+    #def history( self, trans, as_xml=False, show_deleted=False, show_hidden=False, hda_id=None, **kwd ):
+    def history( self, trans, as_xml=False, show_deleted=None, show_hidden=None, hda_id=None, **kwd ):
         """
         Display the current history, creating a new history if necessary.
         NOTE: No longer accepts "id" or "template" options for security reasons.
                                               show_deleted=util.string_as_bool( show_deleted ),
                                               show_hidden=util.string_as_bool( show_hidden ) )
         else:
-            show_deleted = util.string_as_bool( show_deleted )
-            show_hidden = util.string_as_bool( show_hidden )
-            show_purged = util.string_as_bool( show_deleted )
+            show_deleted = util.string_as_bool_or_none( show_deleted )
+            show_purged  = show_deleted
+            show_hidden  = util.string_as_bool_or_none( show_hidden )
             
             datasets = []
             history_panel_template = "root/history.mako"
             if 'USE_ALTERNATE' in locals():
                 datasets = self.get_history_datasets( trans, history,
                                                       show_deleted=True, show_hidden=True, show_purged=True )
+                #datasets = self.get_history_datasets( trans, history, show_deleted, show_hidden, show_purged )
                 history_panel_template = "root/alternate_history.mako"
 
             else:

scripts/functional_tests.py

         tool_data_table_config_path = 'tool_data_table_conf.test.xml'
     else:    
         tool_data_table_config_path = 'tool_data_table_conf.xml'
+    shed_tool_data_table_config = 'shed_tool_data_table_conf.xml'
     tool_dependency_dir = os.environ.get( 'GALAXY_TOOL_DEPENDENCY_DIR', None )
     use_distributed_object_store = os.environ.get( 'GALAXY_USE_DISTRIBUTED_OBJECT_STORE', False )
     
                                    tool_parse_help = False,
                                    test_conf = "test.conf",
                                    tool_data_table_config_path = tool_data_table_config_path,
+                                   shed_tool_data_table_config = shed_tool_data_table_config,
                                    log_destination = "stdout",
                                    use_heartbeat = False,
                                    allow_user_creation = True,

static/scripts/mvc/base-mvc.js

  *          GalaxyLocalization.setLocalizedString( original, localized )
  *          GalaxyLocalization.setLocalizedString({ original1 : localized1, original2 : localized2 })
  *      get with either:
- *          _l( original )
+ *          GalaxyLocalization.localize( string )
+ *          _l( string )
  */
 //TODO: move to Galaxy.Localization (maybe galaxy.base.js)
 var GalaxyLocalization = jQuery.extend( {}, {
     ALIAS_NAME : '_l',
     localizedStrings : {},
-    
+
+    // Set a single English string -> localized string association, or set an entire map of those associations
+    // Pass in either two strings (english, localized) or just an obj (map) of english : localized
     setLocalizedString : function( str_or_obj, localizedString ){
-        // pass in either two strings (english, translated) or an obj (map) of english : translated attributes
         //console.debug( this + '.setLocalizedString:', str_or_obj, localizedString );
         var self = this;
         
         }
     },
     
+    // Attempt to get a localized string for strToLocalize. If not found, return the original strToLocalize
     localize : function( strToLocalize ){
         //console.debug( this + '.localize:', strToLocalize );
+
+        //// uncomment this section to cache strings that need to be localized but haven't been
+        //if( !_.has( this.localizedStrings, strToLocalize ) ){
+        //    //console.debug( 'localization NOT found:', strToLocalize );
+        //    if( !this.nonLocalized ){ this.nonLocalized = {}; }
+        //    this.nonLocalized[ strToLocalize ] = false;
+        //}
+
         // return the localized version if it's there, the strToLocalize if not
-        var retStr = strToLocalize;
-        if( _.has( this.localizedStrings, strToLocalize ) ){
-            //console.debug( 'found' );
-            retStr = this.localizedStrings[ strToLocalize ];
-        }
-        //console.debug( 'returning:', retStr );
-        return retStr;
+        return this.localizedStrings[ strToLocalize ] || strToLocalize;
     },
     
     toString : function(){ return 'GalaxyLocalization'; }
     
     return returnedStorage;
 };
-

static/scripts/mvc/dataset/hda-base.js

+//define([
+//    "../mvc/base-mvc"
+//], function(){
+//==============================================================================
+/** read only view for HistoryDatasetAssociations
+ *
+ */
+var HDABaseView = BaseView.extend( LoggableMixin ).extend({
+    //??TODO: add alias in initialize this.hda = this.model?
+    // view for HistoryDatasetAssociation model above
+
+    // uncomment this out see log messages
+    //logger              : console,
+
+    tagName     : "div",
+    className   : "historyItemContainer",
+
+    // ................................................................................ SET UP
+    initialize  : function( attributes ){
+        this.log( this + '.initialize:', attributes );
+
+        // which buttons go in most states (ok/failed meta are more complicated)
+        this.defaultPrimaryActionButtonRenderers = [
+            this._render_showParamsButton
+        ];
+        
+        // render urlTemplates (gen. provided by GalaxyPaths) to urls
+        if( !attributes.urlTemplates ){ throw( 'HDAView needs urlTemplates on initialize' ); }
+        this.urls = this.renderUrls( attributes.urlTemplates, this.model.toJSON() );
+
+        // whether the body of this hda is expanded (shown)
+        this.expanded = attributes.expanded || false;
+
+        // re-render the entire view on any model change
+        this.model.bind( 'change', this.render, this );
+        //this.bind( 'all', function( event ){
+        //    this.log( event );
+        //}, this );
+    },
+   
+    // urlTemplates is a map (or nested map) of underscore templates (currently, anyhoo)
+    //  use the templates to create the apropo urls for each action this ds could use
+    renderUrls : function( urlTemplates, modelJson ){
+        var hdaView = this,
+            urls = {};
+        _.each( urlTemplates, function( urlTemplateOrObj, urlKey ){
+            // object == nested templates: recurse
+            if( _.isObject( urlTemplateOrObj ) ){
+                urls[ urlKey ] = hdaView.renderUrls( urlTemplateOrObj, modelJson );
+
+            // string == template:
+            } else {
+                // meta_down load is a special case (see renderMetaDownloadUrls)
+                //TODO: should be a better (gen.) way to handle this case
+                if( urlKey === 'meta_download' ){
+                    urls[ urlKey ] = hdaView.renderMetaDownloadUrls( urlTemplateOrObj, modelJson );
+                } else {
+                    urls[ urlKey ] = _.template( urlTemplateOrObj, modelJson );
+                }
+            }
+        });
+        return urls;
+    },
+
+    // there can be more than one meta_file to download, so return a list of url and file_type for each
+    renderMetaDownloadUrls : function( urlTemplate, modelJson ){
+        return _.map( modelJson.meta_files, function( meta_file ){
+            return {
+                url         : _.template( urlTemplate, { id: modelJson.id, file_type: meta_file.file_type }),
+                file_type   : meta_file.file_type
+            };
+        });
+    },
+
+    // ................................................................................ RENDER MAIN
+    // events: rendered, rendered:ready, rendered:initial, rendered:ready:initial
+    render : function(){
+        var view = this,
+            id = this.model.get( 'id' ),
+            state = this.model.get( 'state' ),
+            itemWrapper = $( '<div/>' ).attr( 'id', 'historyItem-' + id ),
+            initialRender = ( this.$el.children().size() === 0 );
+
+        //console.debug( this + '.render, initial?:', initialRender );
+        this.$el.attr( 'id', 'historyItemContainer-' + id );
+        
+        itemWrapper
+            .addClass( 'historyItemWrapper' ).addClass( 'historyItem' )
+            .addClass( 'historyItem-' + state );
+            
+        itemWrapper.append( this._render_warnings() );
+        itemWrapper.append( this._render_titleBar() );
+        this.body = $( this._render_body() );
+        itemWrapper.append( this.body );
+        
+        //TODO: move to own function: setUpBehaviours
+        // we can potentially skip this step and call popupmenu directly on the download button
+        make_popup_menus( itemWrapper );
+
+        // set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
+        itemWrapper.find( '.tooltip' ).tooltip({ placement : 'bottom' });
+        
+        // transition...
+        this.$el.fadeOut( 'fast', function(){
+            view.$el.children().remove();
+            view.$el.append( itemWrapper ).fadeIn( 'fast', function(){
+                view.log( view + ' rendered:', view.$el );
+                
+                var renderedEventName = 'rendered';
+                if( initialRender ){
+                    renderedEventName += ':initial';
+                } else if( view.model.inReadyState() ){
+                    renderedEventName += ':ready';
+                }
+                view.trigger( renderedEventName );
+            });
+        });
+        return this;
+    },
+    
+    // ................................................................................ RENDER WARNINGS
+    // hda warnings including: is deleted, is purged, is hidden (including links to further actions (undelete, etc.))
+    _render_warnings : function(){
+        // jQ errs on building dom with whitespace - if there are no messages, trim -> ''
+        return $( jQuery.trim( HDABaseView.templates.messages( this.model.toJSON() )));
+    },
+    
+    // ................................................................................ RENDER TITLEBAR
+    // the part of an hda always shown (whether the body is expanded or not): title link, title buttons
+    _render_titleBar : function(){
+        var titleBar = $( '<div class="historyItemTitleBar" style="overflow: hidden"></div>' );
+        titleBar.append( this._render_titleButtons() );
+        titleBar.append( '<span class="state-icon"></span>' );
+        titleBar.append( this._render_titleLink() );
+        return titleBar;
+    },
+
+    // ................................................................................ display, edit attr, delete
+    // icon-button group for the common, most easily accessed actions
+    //NOTE: these are generally displayed for almost every hda state (tho poss. disabled)
+    _render_titleButtons : function(){
+        // render the display, edit attr and delete icon-buttons
+        var buttonDiv = $( '<div class="historyItemButtons"></div>' );
+        buttonDiv.append( this._render_displayButton() );
+        return buttonDiv;
+    },
+    
+    // icon-button to display this hda in the galaxy main iframe
+    _render_displayButton : function(){
+        // don't show display if not in ready state, error'd, or not accessible
+        if( ( !this.model.inReadyState() )
+        ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.ERROR )
+        ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.NOT_VIEWABLE )
+        ||  ( !this.model.get( 'accessible' ) ) ){
+            this.displayButton = null;
+            return null;
+        }
+        
+        var displayBtnData = {
+            icon_class  : 'display',
+            target      : 'galaxy_main'
+        };
+
+        // show a disabled display if the data's been purged
+        if( this.model.get( 'purged' ) ){
+            displayBtnData.enabled = false;
+            displayBtnData.title = _l( 'Cannot display datasets removed from disk' );
+            
+        } else {
+            displayBtnData.title = _l( 'Display data in browser' );
+            displayBtnData.href  = this.urls.display;
+        }
+
+        this.displayButton = new IconButtonView({ model : new IconButton( displayBtnData ) });
+        return this.displayButton.render().$el;
+    },
+    
+    // ................................................................................ titleLink
+    // render the hid and hda.name as a link (that will expand the body)
+    _render_titleLink : function(){
+        return $( jQuery.trim( HDABaseView.templates.titleLink(
+            _.extend( this.model.toJSON(), { urls: this.urls } )
+        )));
+    },
+
+    // ................................................................................ RENDER BODY
+    // render the data/metadata summary (format, size, misc info, etc.)
+    _render_hdaSummary : function(){
+        var modelData = _.extend( this.model.toJSON(), { urls: this.urls } );
+        return HDABaseView.templates.hdaSummary( modelData );
+    },
+
+    // ................................................................................ primary actions
+    // render the icon-buttons gen. placed underneath the hda summary
+    _render_primaryActionButtons : function( buttonRenderingFuncs ){
+        var view = this,
+            primaryActionButtons = $( '<div/>' ).attr( 'id', 'primary-actions-' + this.model.get( 'id' ) );
+        _.each( buttonRenderingFuncs, function( fn ){
+            primaryActionButtons.append( fn.call( view ) );
+        });
+        return primaryActionButtons;
+    },
+    
+    // icon-button/popupmenu to down the data (and/or the associated meta files (bai, etc.)) for this hda
+    _render_downloadButton : function(){
+        // don't show anything if the data's been purged
+        if( this.model.get( 'purged' ) || !this.model.hasData() ){ return null; }
+        
+        // return either: a single download icon-button (if there are no meta files)
+        //  or a popupmenu with links to download assoc. meta files (if there are meta files)
+        var downloadLinkHTML = HDABaseView.templates.downloadLinks(
+            _.extend( this.model.toJSON(), { urls: this.urls } )
+        );
+        //this.log( this + '_render_downloadButton, downloadLinkHTML:', downloadLinkHTML );
+        return $( downloadLinkHTML );
+    },
+    
+    // icon-button to show the input and output (stdout/err) for the job that created this hda
+    _render_showParamsButton : function(){
+        // gen. safe to show in all cases
+        this.showParamsButton = new IconButtonView({ model : new IconButton({
+            title       : _l( 'View details' ),
+            href        : this.urls.show_params,
+            target      : 'galaxy_main',
+            icon_class  : 'information'
+        }) });
+        return this.showParamsButton.render().$el;
+    },
+    
+    // ................................................................................ other elements
+    // render links to external genome display applications (igb, gbrowse, etc.)
+    //TODO: not a fan of the style on these
+    _render_displayApps : function(){
+        if( !this.model.hasData() ){ return null; }
+        
+        var displayAppsDiv = $( '<div/>' ).addClass( 'display-apps' );
+        if( !_.isEmpty( this.model.get( 'display_types' ) ) ){
+            //this.log( this + 'display_types:', this.model.get( 'urls' ).display_types );
+            //TODO:?? does this ever get used?
+            displayAppsDiv.append(
+                HDABaseView.templates.displayApps({ displayApps : this.model.get( 'display_types' ) })
+            );
+        }
+        if( !_.isEmpty( this.model.get( 'display_apps' ) ) ){
+            //this.log( this + 'display_apps:',  this.model.get( 'urls' ).display_apps );
+            displayAppsDiv.append(
+                HDABaseView.templates.displayApps({ displayApps : this.model.get( 'display_apps' ) })
+            );
+        }
+        return displayAppsDiv;
+    },
+            
+    // render the data peek
+    //TODO: curr. pre-formatted into table on the server side - may not be ideal/flexible
+    _render_peek : function(){
+        if( !this.model.get( 'peek' ) ){ return null; }
+        return $( '<div/>' ).append(
+            $( '<pre/>' )
+                .attr( 'id', 'peek' + this.model.get( 'id' ) )
+                .addClass( 'peek' )
+                .append( this.model.get( 'peek' ) )
+        );
+    },
+    
+    // ................................................................................ state body renderers
+    // _render_body fns for the various states
+    //TODO: only render these on expansion (or already expanded)
+    _render_body_not_viewable : function( parent ){
+        //TODO: revisit - still showing display, edit, delete (as common) - that CAN'T be right
+        parent.append( $( '<div>' + _l( 'You do not have permission to view dataset' ) + '.</div>' ) );
+    },
+    
+    _render_body_uploading : function( parent ){
+        parent.append( $( '<div>' + _l( 'Dataset is uploading' ) + '</div>' ) );
+    },
+        
+    _render_body_queued : function( parent ){
+        parent.append( $( '<div>' + _l( 'Job is waiting to run' ) + '.</div>' ) );
+        parent.append( this._render_primaryActionButtons( this.defaultPrimaryActionButtonRenderers ));
+    },
+        
+    _render_body_running : function( parent ){
+        parent.append( '<div>' + _l( 'Job is currently running' ) + '.</div>' );
+        parent.append( this._render_primaryActionButtons( this.defaultPrimaryActionButtonRenderers ));
+    },
+        
+    _render_body_error : function( parent ){
+        if( !this.model.get( 'purged' ) ){
+            parent.append( $( '<div>' + this.model.get( 'misc_blurb' ) + '</div>' ) );
+        }
+        parent.append( ( _l( 'An error occurred running this job' ) + ': '
+                       + '<i>' + $.trim( this.model.get( 'misc_info' ) ) + '</i>' ) );
+        parent.append( this._render_primaryActionButtons(
+            this.defaultPrimaryActionButtonRenderers.concat([ this._render_downloadButton ])
+        ));
+    },
+        
+    _render_body_discarded : function( parent ){
+        parent.append( '<div>' + _l( 'The job creating this dataset was cancelled before completion' ) + '.</div>' );
+        parent.append( this._render_primaryActionButtons( this.defaultPrimaryActionButtonRenderers ));
+    },
+        
+    _render_body_setting_metadata : function( parent ){
+        parent.append( $( '<div>' + _l( 'Metadata is being auto-detected' ) + '.</div>' ) );
+    },
+    
+    _render_body_empty : function( parent ){
+        //TODO: replace i with dataset-misc-info class 
+        //?? why are we showing the file size when we know it's zero??
+        parent.append( $( '<div>' + _l( 'No data' ) + ': <i>' + this.model.get( 'misc_blurb' ) + '</i></div>' ) );
+        parent.append( this._render_primaryActionButtons( this.defaultPrimaryActionButtonRenderers ));
+    },
+        
+    _render_body_failed_metadata : function( parent ){
+        //TODO: the css for this box is broken (unlike the others)
+        // add a message box about the failure at the top of the body...
+        parent.append( $( HDABaseView.templates.failedMetadata( this.model.toJSON() ) ) );
+        //...then render the remaining body as STATES.OK (only diff between these states is the box above)
+        this._render_body_ok( parent );
+    },
+        
+    _render_body_ok : function( parent ){
+        // most common state renderer and the most complicated
+        parent.append( this._render_hdaSummary() );
+
+        // return shortened form if del'd
+        //TODO: is this correct? maybe only on purged
+        if( this.model.isDeletedOrPurged() ){
+            parent.append( this._render_primaryActionButtons([
+                this._render_downloadButton,
+                this._render_showParamsButton
+            ]));
+            return;
+        }
+        
+        //NOTE: change the order here
+        parent.append( this._render_primaryActionButtons([
+            this._render_downloadButton,
+            this._render_showParamsButton
+        ]));
+        parent.append( '<div class="clear"/>' );
+        
+        parent.append( this._render_displayApps() );
+        parent.append( this._render_peek() );
+    },
+    
+    _render_body : function(){
+        //this.log( this + '_render_body' );
+        
+        var body = $( '<div/>' )
+            .attr( 'id', 'info-' + this.model.get( 'id' ) )
+            .addClass( 'historyItemBody' )
+            .attr(  'style', 'display: block' );
+        
+        //TODO: not a fan of this dispatch
+        switch( this.model.get( 'state' ) ){
+            case HistoryDatasetAssociation.STATES.NOT_VIEWABLE :
+                this._render_body_not_viewable( body );
+				break;
+            case HistoryDatasetAssociation.STATES.UPLOAD :
+				this._render_body_uploading( body );
+				break;
+            case HistoryDatasetAssociation.STATES.QUEUED :
+				this._render_body_queued( body );
+				break;
+            case HistoryDatasetAssociation.STATES.RUNNING :
+				this._render_body_running( body ); 
+				break;
+            case HistoryDatasetAssociation.STATES.ERROR :
+				this._render_body_error( body );
+				break;
+            case HistoryDatasetAssociation.STATES.DISCARDED :
+				this._render_body_discarded( body );
+				break;
+            case HistoryDatasetAssociation.STATES.SETTING_METADATA :
+				this._render_body_setting_metadata( body );
+				break;
+            case HistoryDatasetAssociation.STATES.EMPTY :
+				this._render_body_empty( body );
+				break;
+            case HistoryDatasetAssociation.STATES.FAILED_METADATA :
+				this._render_body_failed_metadata( body );
+				break;
+            case HistoryDatasetAssociation.STATES.OK :
+				this._render_body_ok( body );
+				break;
+            default:
+                //??: no body?
+                body.append( $( '<div>Error: unknown dataset state "' + state + '".</div>' ) );
+        }
+        body.append( '<div style="clear: both"></div>' );
+            
+        if( this.expanded ){
+            body.show();
+        } else {
+            body.hide();
+        }
+        return body;
+    },
+
+    // ................................................................................ EVENTS
+    events : {
+        'click .historyItemTitle'           : 'toggleBodyVisibility'
+    },
+
+    // expand/collapse body
+    // event: body-visible, body-hidden
+    toggleBodyVisibility : function( event, expanded ){
+        var hdaView = this,
+            $body = this.$el.find( '.historyItemBody' );
+        expanded = ( expanded === undefined )?( !$body.is( ':visible' ) ):( expanded );
+        //this.log( 'toggleBodyVisibility, expanded:', expanded, '$body:', $body );
+
+        if( expanded ){
+            $body.slideDown( 'fast', function(){
+                hdaView.trigger( 'body-visible', hdaView.model.get( 'id' ) );
+            });
+        } else {
+            $body.slideUp( 'fast', function(){
+                hdaView.trigger( 'body-hidden', hdaView.model.get( 'id' ) );
+            });
+        }
+    },
+
+    // ................................................................................ UTILTIY
+    toString : function(){
+        var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
+        return 'HDABaseView(' + modelString + ')';
+    }
+});
+
+//------------------------------------------------------------------------------
+HDABaseView.templates = {
+    warningMsg          : Handlebars.templates[ 'template-warningmessagesmall' ],
+
+    messages            : Handlebars.templates[ 'template-hda-warning-messages' ],
+    titleLink           : Handlebars.templates[ 'template-hda-titleLink' ],
+    hdaSummary          : Handlebars.templates[ 'template-hda-hdaSummary' ],
+    downloadLinks       : Handlebars.templates[ 'template-hda-downloadLinks' ],
+    failedMetadata      : Handlebars.templates[ 'template-hda-failedMetaData' ],
+    displayApps         : Handlebars.templates[ 'template-hda-displayApps' ]
+};
+
+//==============================================================================
+//return {
+//    HDABaseView  : HDABaseView,
+//};});

static/scripts/mvc/dataset/hda-edit.js

 //    "../mvc/base-mvc"
 //], function(){
 //==============================================================================
-/** View for editing (working with - as opposed to viewing/read-only) an hda
+/** editing view for HistoryDatasetAssociations
  *
  */
-var HDAView = BaseView.extend( LoggableMixin ).extend({
-    //??TODO: add alias in initialize this.hda = this.model?
-    // view for HistoryDatasetAssociation model above
+var HDAEditView = HDABaseView.extend({
 
-    // uncomment this out see log messages
-    //logger              : console,
-
-    tagName     : "div",
-    className   : "historyItemContainer",
-    
     // ................................................................................ SET UP
     initialize  : function( attributes ){
-        this.log( this + '.initialize:', attributes );
+        HDABaseView.prototype.initialize.call( this, attributes );
 
-        // render urlTemplates (gen. provided by GalaxyPaths) to urls
-        if( !attributes.urlTemplates ){ throw( 'HDAView needs urlTemplates on initialize' ); }
-        this.urls = this.renderUrls( attributes.urlTemplates, this.model.toJSON() );
-
-        // whether the body of this hda is expanded (shown)
-        this.expanded = attributes.expanded || false;
-
-        // re-render the entire view on any model change
-        this.model.bind( 'change', this.render, this );
-        //this.bind( 'all', function( event ){
-        //    this.log( event );
-        //}, this );
-    },
-   
-    // urlTemplates is a map (or nested map) of underscore templates (currently, anyhoo)
-    //  use the templates to create the apropo urls for each action this ds could use
-    renderUrls : function( urlTemplates, modelJson ){
-        var hdaView = this,
-            urls = {};
-        _.each( urlTemplates, function( urlTemplateOrObj, urlKey ){
-            // object == nested templates: recurse
-            if( _.isObject( urlTemplateOrObj ) ){
-                urls[ urlKey ] = hdaView.renderUrls( urlTemplateOrObj, modelJson );
-
-            // string == template:
-            } else {
-                // meta_down load is a special case (see renderMetaDownloadUrls)
-                //TODO: should be a better (gen.) way to handle this case
-                if( urlKey === 'meta_download' ){
-                    urls[ urlKey ] = hdaView.renderMetaDownloadUrls( urlTemplateOrObj, modelJson );
-                } else {
-                    urls[ urlKey ] = _.template( urlTemplateOrObj, modelJson );
-                }
-            }
-        });
-        return urls;
+        // which buttons go in most states (ok/failed meta are more complicated)
+        // HDAEdit gets the rerun button on almost all states
+        this.defaultPrimaryActionButtonRenderers = [
+            this._render_showParamsButton,
+            this._render_rerunButton
+        ];
     },
 
-    // there can be more than one meta_file to download, so return a list of url and file_type for each
-    renderMetaDownloadUrls : function( urlTemplate, modelJson ){
-        return _.map( modelJson.meta_files, function( meta_file ){
-            return {
-                url         : _.template( urlTemplate, { id: modelJson.id, file_type: meta_file.file_type }),
-                file_type   : meta_file.file_type
-            };
-        });
-    },
-
-    // ................................................................................ RENDER MAIN
-    render : function(){
-        var view = this,
-            id = this.model.get( 'id' ),
-            state = this.model.get( 'state' ),
-            itemWrapper = $( '<div/>' ).attr( 'id', 'historyItem-' + id ),
-            initialRender = ( this.$el.children().size() === 0 );
-
-        //console.debug( this + '.render, initial?:', initialRender );
-
-        this._clearReferences();
-        this.$el.attr( 'id', 'historyItemContainer-' + id );
-        
-        itemWrapper
-            .addClass( 'historyItemWrapper' ).addClass( 'historyItem' )
-            .addClass( 'historyItem-' + state );
-            
-        itemWrapper.append( this._render_warnings() );
-        itemWrapper.append( this._render_titleBar() );
-        this.body = $( this._render_body() );
-        itemWrapper.append( this.body );
-        
-        //TODO: move to own function: setUpBehaviours
-        // we can potentially skip this step and call popupmenu directly on the download button
-        make_popup_menus( itemWrapper );
-
-        // set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
-        itemWrapper.find( '.tooltip' ).tooltip({ placement : 'bottom' });
-        
-        // transition...
-        this.$el.fadeOut( 'fast', function(){
-            view.$el.children().remove();
-            view.$el.append( itemWrapper ).fadeIn( 'fast', function(){
-                view.log( view + ' rendered:', view.$el );
-                var renderedEventName = 'rendered';
-                
-                if( initialRender ){
-                    renderedEventName += ':initial';
-                } else if( view.model.inReadyState() ){
-                    renderedEventName += ':ready';
-                }
-                view.trigger( renderedEventName );
-            });
-        });
-        return this;
-    },
-    
-    //NOTE: button renderers have the side effect of caching their IconButtonViews to this view
-    // clear out cached sub-views, dom refs, etc. from prev. render
-    _clearReferences : function(){
-        //??TODO: we should reset these in the button logic checks (i.e. if not needed this.button = null; return null)
-        //?? do we really need these - not so far
-        //TODO: at least move these to a map
-        this.displayButton = null;
-        this.editButton = null;
-        this.deleteButton = null;
-        this.errButton = null;
-        this.showParamsButton = null;
-        this.rerunButton = null;
-        this.visualizationsButton = null;
-        this.tagButton = null;
-        this.annotateButton = null;
-    },
-    
     // ................................................................................ RENDER WARNINGS
     // hda warnings including: is deleted, is purged, is hidden (including links to further actions (undelete, etc.))
     _render_warnings : function(){
         // jQ errs on building dom with whitespace - if there are no messages, trim -> ''
-        return $( jQuery.trim( HDAView.templates.messages(
+        return $( jQuery.trim( HDABaseView.templates.messages(
             _.extend( this.model.toJSON(), { urls: this.urls } )
         )));
     },
     
-    // ................................................................................ RENDER TITLEBAR
-    // the part of an hda always shown (whether the body is expanded or not): title link, title buttons
-    _render_titleBar : function(){
-        var titleBar = $( '<div class="historyItemTitleBar" style="overflow: hidden"></div>' );
-        titleBar.append( this._render_titleButtons() );
-        titleBar.append( '<span class="state-icon"></span>' );
-        titleBar.append( this._render_titleLink() );
-        return titleBar;
-    },
-
     // ................................................................................ display, edit attr, delete
     // icon-button group for the common, most easily accessed actions
     //NOTE: these are generally displayed for almost every hda state (tho poss. disabled)
         return buttonDiv;
     },
     
-    // icon-button to display this hda in the galaxy main iframe
-    _render_displayButton : function(){
-        // don't show display if not in ready state, error'd, or not accessible
-        if( ( !this.model.inReadyState() )
+    // icon-button to edit the attributes (format, permissions, etc.) this hda
+    _render_editButton : function(){
+        // don't show edit while uploading
+        //TODO??: error?
+        //TODO??: not viewable/accessible are essentially the same (not viewable set from accessible)
+        if( ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.UPLOAD )
         ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.ERROR )
         ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.NOT_VIEWABLE )
         ||  ( !this.model.get( 'accessible' ) ) ){
-            return null;
-        }
-        
-        var displayBtnData = {
-            icon_class  : 'display'
-        };
-
-        // show a disabled display if the data's been purged
-        if( this.model.get( 'purged' ) ){
-            displayBtnData.enabled = false;
-            displayBtnData.title = 'Cannot display datasets removed from disk';
-            
-        } else {
-            displayBtnData.title = 'Display data in browser';
-            displayBtnData.href  = this.urls.display;
-        }
-        
-        if( this.model.get( 'for_editing' ) ){
-            displayBtnData.target = 'galaxy_main';
-        }
-
-        this.displayButton = new IconButtonView({ model : new IconButton( displayBtnData ) });
-        return this.displayButton.render().$el;
-    },
-    
-    // icon-button to edit the attributes (format, permissions, etc.) this hda
-    _render_editButton : function(){
-        // don't show edit while uploading, or if editable
-        if( ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.UPLOAD )
-        ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.ERROR )
-        ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.NOT_VIEWABLE )
-        ||  ( !this.model.get( 'accessible' ) )
-        ||  ( !this.model.get( 'for_editing' ) ) ){
+            this.editButton = null;
             return null;
         }
         
         var purged = this.model.get( 'purged' ),
             deleted = this.model.get( 'deleted' ),
             editBtnData = {
-                title       : 'Edit attributes',
+                title       : _l( 'Edit Attributes' ),
                 href        : this.urls.edit,
                 target      : 'galaxy_main',
                 icon_class  : 'edit'
             };
             
         // disable if purged or deleted and explain why in the tooltip
-        //TODO: if for_editing
         if( deleted || purged ){
             editBtnData.enabled = false;
             if( purged ){
-                editBtnData.title = 'Cannot edit attributes of datasets removed from disk';
+                editBtnData.title = _l( 'Cannot edit attributes of datasets removed from disk' );
             } else if( deleted ){
-                editBtnData.title = 'Undelete dataset to edit attributes';
+                editBtnData.title = _l( 'Undelete dataset to edit attributes' );
             }
         }
         
     // icon-button to delete this hda
     _render_deleteButton : function(){
         // don't show delete if...
-        if( ( !this.model.get( 'for_editing' ) )
-        ||  ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.NOT_VIEWABLE )
+        //TODO??: not viewable/accessible are essentially the same (not viewable set from accessible)
+        if( ( this.model.get( 'state' ) === HistoryDatasetAssociation.STATES.NOT_VIEWABLE )
         ||  ( !this.model.get( 'accessible' ) ) ){
+            this.deleteButton = null;
             return null;
         }
         
         var deleteBtnData = {
-            title       : 'Delete',
+            title       : _l( 'Delete' ),
             href        : this.urls[ 'delete' ],
             id          : 'historyItemDeleter-' + this.model.get( 'id' ),
             icon_class  : 'delete'
         };
         if( this.model.get( 'deleted' ) || this.model.get( 'purged' ) ){
             deleteBtnData = {
-                title       : 'Dataset is already deleted',
+                title       : _l( 'Dataset is already deleted' ),
                 icon_class  : 'delete',
                 enabled     : false
             };
         this.deleteButton = new IconButtonView({ model : new IconButton( deleteBtnData ) });
         return this.deleteButton.render().$el;
     },
-    
-    // ................................................................................ titleLink
-    // render the hid and hda.name as a link (that will expand the body)
-    _render_titleLink : function(){
-        return $( jQuery.trim( HDAView.templates.titleLink(
-            _.extend( this.model.toJSON(), { urls: this.urls } )
-        )));
-    },
 
     // ................................................................................ RENDER BODY
     // render the data/metadata summary (format, size, misc info, etc.)
         // if there's no dbkey and it's editable : pass a flag to the template to render a link to editing in the '?'
         if( this.model.get( 'metadata_dbkey' ) === '?'
         &&  !this.model.isDeletedOrPurged() ){
+            //TODO: use HDABaseView and select/replace base on this switch
             _.extend( modelData, { dbkey_unknown_and_editable : true });
         }
-        return HDAView.templates.hdaSummary( modelData );
+        return HDABaseView.templates.hdaSummary( modelData );
     },
 
     // ................................................................................ primary actions
-    // render the icon-buttons gen. placed underneath the hda summary
-    _render_primaryActionButtons : function( buttonRenderingFuncs ){
-        var primaryActionButtons = $( '<div/>' ).attr( 'id', 'primary-actions-' + this.model.get( 'id' ) ),
-            view = this;
-        _.each( buttonRenderingFuncs, function( fn ){
-            primaryActionButtons.append( fn.call( view ) );
-        });
-        return primaryActionButtons;
-    },
-    
-    // icon-button/popupmenu to down the data (and/or the associated meta files (bai, etc.)) for this hda
-    _render_downloadButton : function(){
-        // don't show anything if the data's been purged
-        if( this.model.get( 'purged' ) ){ return null; }
-        
-        // return either: a single download icon-button (if there are no meta files)
-        //  or a popupmenu with links to download assoc. meta files (if there are meta files)
-        var downloadLinkHTML = HDAView.templates.downloadLinks(
-            _.extend( this.model.toJSON(), { urls: this.urls } )
-        );
-        //this.log( this + '_render_downloadButton, downloadLinkHTML:', downloadLinkHTML );
-        return $( downloadLinkHTML );
-    },
-    
     // icon-button to show the input and output (stdout/err) for the job that created this hda
-    _render_errButton : function(){    
-        if( ( this.model.get( 'state' ) !== HistoryDatasetAssociation.STATES.ERROR )
-        ||  ( !this.model.get( 'for_editing' ) ) ){ return null; }
+    _render_errButton : function(){
+        if( this.model.get( 'state' ) !== HistoryDatasetAssociation.STATES.ERROR ){
+            this.errButton = null;
+            return null;
+        }
         
         this.errButton = new IconButtonView({ model : new IconButton({
-            title       : 'View or report this error',
+            title       : _l( 'View or report this error' ),
             href        : this.urls.report_error,
             target      : 'galaxy_main',
             icon_class  : 'bug'
         return this.errButton.render().$el;
     },
     
-    // icon-button to show the input and output (stdout/err) for the job that created this hda
-    _render_showParamsButton : function(){
-        // gen. safe to show in all cases
-        this.showParamsButton = new IconButtonView({ model : new IconButton({
-            title       : 'View details',
-            href        : this.urls.show_params,
-            target      : 'galaxy_main',
-            icon_class  : 'information'
-        }) });
-        return this.showParamsButton.render().$el;
-    },
-    
     // icon-button to re run the job that created this hda
     _render_rerunButton : function(){
-        if( !this.model.get( 'for_editing' ) ){ return null; }
         this.rerunButton = new IconButtonView({ model : new IconButton({
-            title       : 'Run this job again',
+            title       : _l( 'Run this job again' ),
             href        : this.urls.rerun,
             target      : 'galaxy_main',
             icon_class  : 'arrow-circle'
             };
 
         if( !( this.model.hasData() )
-        ||  !( this.model.get( 'for_editing' ) )
         ||  !( visualizations && visualizations.length )
         ||  !( visualization_url ) ){
             //console.warn( 'NOT rendering visualization icon' )
+            this.visualizationsButton = null;
             return null;
         }
         
         // render the icon from template
         this.visualizationsButton = new IconButtonView({ model : new IconButton({
-            title       : 'Visualize',
+            title       : _l( 'Visualize' ),
             href        : visualization_url,
             icon_class  : 'chart_curve'
         })});
             _.each( visualizations, function( visualization ) {
                 //TODO: move to utils
                 var titleCaseVisualization = visualization.charAt( 0 ).toUpperCase() + visualization.slice( 1 );
-                popup_menu_dict[ titleCaseVisualization ] = create_viz_action( visualization );
+                popup_menu_dict[ _l( titleCaseVisualization ) ] = create_viz_action( visualization );
             });
             make_popupmenu( $icon, popup_menu_dict );
         }
     // icon-button to load and display tagging html
     //TODO: these should be a sub-MV
     _render_tagButton : function(){
+        //TODO: check for User
         if( !( this.model.hasData() )
-        ||  !( this.model.get( 'for_editing' ) )
-        ||   ( !this.urls.tags.get ) ){ return null; }
+        ||   ( !this.urls.tags.get ) ){
+            this.tagButton = null;
+            return null;
+        }
         
         this.tagButton = new IconButtonView({ model : new IconButton({
-            title       : 'Edit dataset tags',
+            title       : _l( 'Edit dataset tags' ),
             target      : 'galaxy_main',
             href        : this.urls.tags.get,
             icon_class  : 'tags'
     // icon-button to load and display annotation html
     //TODO: these should be a sub-MV
     _render_annotateButton : function(){
+        //TODO: check for User
         if( !( this.model.hasData() )
-        ||  !( this.model.get( 'for_editing' ) )
-        ||   ( !this.urls.annotation.get ) ){ return null; }
+        ||   ( !this.urls.annotation.get ) ){
+            this.annotateButton = null;
+            return null;
+        }
 
         this.annotateButton = new IconButtonView({ model : new IconButton({
-            title       : 'Edit dataset annotation',
+            title       : _l( 'Edit dataset annotation' ),
             target      : 'galaxy_main',
             icon_class  : 'annotate'
         })});
     },
     
     // ................................................................................ other elements
-    // render links to external genome display applications (igb, gbrowse, etc.)
-    //TODO: not a fan of the style on these
-    _render_displayApps : function(){
-        if( !this.model.hasData() ){ return null; }
-        
-        var displayAppsDiv = $( '<div/>' ).addClass( 'display-apps' );
-        if( !_.isEmpty( this.model.get( 'display_types' ) ) ){
-            //this.log( this + 'display_types:', this.model.get( 'urls' ).display_types );
-            //TODO:?? does this ever get used?
-            displayAppsDiv.append(
-                HDAView.templates.displayApps({ displayApps : this.model.get( 'display_types' ) })
-            );
-        }
-        if( !_.isEmpty( this.model.get( 'display_apps' ) ) ){
-            //this.log( this + 'display_apps:',  this.model.get( 'urls' ).display_apps );
-            displayAppsDiv.append(
-                HDAView.templates.displayApps({ displayApps : this.model.get( 'display_apps' ) })
-            );
-        }
-        return displayAppsDiv;
-    },
-            
     //TODO: into sub-MV
+    //TODO: check for User
     // render the area used to load tag display
     _render_tagArea : function(){
         if( !this.urls.tags.set ){ return null; }
         //TODO: move to mvc/tags.js
-        return $( HDAView.templates.tagArea(
+        return $( HDAEditView.templates.tagArea(
             _.extend( this.model.toJSON(), { urls: this.urls } )
         ));
     },
 
     //TODO: into sub-MV
+    //TODO: check for User
     // render the area used to load annotation display
     _render_annotationArea : function(){
         if( !this.urls.annotation.get ){ return null; }
         //TODO: move to mvc/annotations.js
-        return $( HDAView.templates.annotationArea(
+        return $( HDAEditView.templates.annotationArea(
             _.extend( this.model.toJSON(), { urls: this.urls } )
         ));
     },
-
-    // render the data peek
-    //TODO: curr. pre-formatted into table on the server side - may not be ideal/flexible
-    _render_peek : function(){
-        if( !this.model.get( 'peek' ) ){ return null; }
-        return $( '<div/>' ).append(
-            $( '<pre/>' )
-                .attr( 'id', 'peek' + this.model.get( 'id' ) )
-                .addClass( 'peek' )
-                .append( this.model.get( 'peek' ) )
-        );
-    },
     
     // ................................................................................ state body renderers
-    // _render_body fns for the various states
-    //TODO: only render these on expansion (or already expanded)
-    _render_body_not_viewable : function( parent ){
-        //TODO: revisit - still showing display, edit, delete (as common) - that CAN'T be right
-        parent.append( $( '<div>You do not have permission to view dataset.</div>' ) );
-    },
-    
-    _render_body_uploading : function( parent ){
-        parent.append( $( '<div>Dataset is uploading</div>' ) );
-    },
-        
-    _render_body_queued : function( parent ){
-        parent.append( $( '<div>Job is waiting to run.</div>' ) );
-        parent.append( this._render_primaryActionButtons([
-            this._render_showParamsButton,
-            this._render_rerunButton
-        ]));
-    },
-        
-    _render_body_running : function( parent ){
-        parent.append( '<div>Job is currently running.</div>' );
-        parent.append( this._render_primaryActionButtons([
-            this._render_showParamsButton,
-            this._render_rerunButton
-        ]));
-    },
-        
     _render_body_error : function( parent ){
-        if( !this.model.get( 'purged' ) ){
-            parent.append( $( '<div>' + this.model.get( 'misc_blurb' ) + '</div>' ) );
-        }
-        parent.append( ( 'An error occurred running this job: '
-                       + '<i>' + $.trim( this.model.get( 'misc_info' ) ) + '</i>' ) );
-        parent.append( this._render_primaryActionButtons([
-            this._render_downloadButton,
-            this._render_errButton,
-            this._render_showParamsButton,
-            this._render_rerunButton
-        ]));
-    },
-        
-    _render_body_discarded : function( parent ){
-        parent.append( '<div>The job creating this dataset was cancelled before completion.</div>' );
-        parent.append( this._render_primaryActionButtons([
-            this._render_showParamsButton,
-            this._render_rerunButton
-        ]));
-    },
-        
-    _render_body_setting_metadata : function( parent ){
-        parent.append( $( '<div>Metadata is being auto-detected.</div>' ) );
-    },
-    
-    _render_body_empty : function( parent ){
-        //TODO: replace i with dataset-misc-info class 
-        //?? why are we showing the file size when we know it's zero??
-        parent.append( $( '<div>No data: <i>' + this.model.get( 'misc_blurb' ) + '</i></div>' ) );
-        parent.append( this._render_primaryActionButtons([
-            this._render_showParamsButton,
-            this._render_rerunButton
-        ]));
-    },
-        
-    _render_body_failed_metadata : function( parent ){
-        //TODO: the css for this box is broken (unlike the others)
-        // add a message box about the failure at the top of the body...
-        parent.append( $( HDAView.templates.failedMetadata( this.model.toJSON() ) ) );
-        //...then render the remaining body as STATES.OK (only diff between these states is the box above)
-        this._render_body_ok( parent );
+        // overridden to prepend error report button to primary actions strip
+        HDABaseView.prototype._render_body_error.call( this, parent );
+        var primaryActions = parent.find( '#primary-actions-' + this.model.get( 'id' ) );
+        primaryActions.prepend( this._render_errButton() );
     },
         
     _render_body_ok : function( parent ){
         //NOTE: change the order here
         parent.append( this._render_primaryActionButtons([
             this._render_downloadButton,
-            this._render_errButton,
             this._render_showParamsButton,
             this._render_rerunButton,
             this._render_visualizationsButton
         
         parent.append( this._render_displayApps() );
         parent.append( this._render_peek() );
-
-        //TODO??: still needed?
-        //// If Mozilla, hide scrollbars in hidden items since they cause animation bugs
-        //if ( $.browser.mozilla ) {
-        //    $( "div.historyItemBody" ).each( function() {
-        //        if ( !$(this).is(":visible") ) { $(this).find( "pre.peek" ).css( "overflow", "hidden" ); }
-        //    });
-        //}
-    },
-    
-    _render_body : function(){
-        //this.log( this + '_render_body' );
-        //this.log( 'state:', state, 'for_editing', for_editing );
-        
-        //TODO: incorrect id (encoded - use hid?)
-        var body = $( '<div/>' )
-            .attr( 'id', 'info-' + this.model.get( 'id' ) )
-            .addClass( 'historyItemBody' )
-            .attr(  'style', 'display: block' );
-        
-        //TODO: not a fan of this dispatch
-        switch( this.model.get( 'state' ) ){
-            case HistoryDatasetAssociation.STATES.NOT_VIEWABLE :
-                this._render_body_not_viewable( body );
-				break;
-            case HistoryDatasetAssociation.STATES.UPLOAD :
-				this._render_body_uploading( body );
-				break;
-            case HistoryDatasetAssociation.STATES.QUEUED :
-				this._render_body_queued( body );
-				break;
-            case HistoryDatasetAssociation.STATES.RUNNING :
-				this._render_body_running( body ); 
-				break;
-            case HistoryDatasetAssociation.STATES.ERROR :
-				this._render_body_error( body );
-				break;
-            case HistoryDatasetAssociation.STATES.DISCARDED :
-				this._render_body_discarded( body );
-				break;
-            case HistoryDatasetAssociation.STATES.SETTING_METADATA :
-				this._render_body_setting_metadata( body );
-				break;
-            case HistoryDatasetAssociation.STATES.EMPTY :
-				this._render_body_empty( body );
-				break;
-            case HistoryDatasetAssociation.STATES.FAILED_METADATA :
-				this._render_body_failed_metadata( body );
-				break;
-            case HistoryDatasetAssociation.STATES.OK :
-				this._render_body_ok( body );
-				break;
-            default:
-                //??: no body?
-                body.append( $( '<div>Error: unknown dataset state "' + state + '".</div>' ) );
-        }
-        body.append( '<div style="clear: both"></div>' );
-            
-        if( this.expanded ){
-            body.show();
-        } else {
-            body.hide();
-        }
-        return body;
     },
 
     // ................................................................................ EVENTS
                 $.ajax({
                     //TODO: the html from this breaks a couple of times
                     url: this.urls.tags.get,
-                    error: function() { alert( "Tagging failed" ); },
+                    error: function() { alert( _l( "Tagging failed" ) ); },
                     success: function(tag_elt_html) {
                         tagElt.html(tag_elt_html);
                         tagElt.find(".tooltip").tooltip();
                 // Need to fill annotation element.
                 $.ajax({
                     url: this.urls.annotation.get,
-                    error: function(){ alert( "Annotations failed" ); },
+                    error: function(){ alert( _l( "Annotations failed" ) ); },
                     success: function( htmlFromAjax ){
                         if( htmlFromAjax === "" ){
-                            htmlFromAjax = "<em>Describe or add notes to dataset</em>";
+                            htmlFromAjax = "<em>" + _l( "Describe or add notes to dataset" ) + "</em>";
                         }
                         annotationElem.html( htmlFromAjax );
                         annotationArea.find(".tooltip").tooltip();
         return false;        
     },
 
-    // expand/collapse body
-    //side effect: trigger event
-    toggleBodyVisibility : function( event, expanded ){
-        var $body = this.$el.find( '.historyItemBody' );
-        expanded = ( expanded === undefined )?( !$body.is( ':visible' ) ):( expanded );
-        //this.log( 'toggleBodyVisibility, expanded:', expanded, '$body:', $body );
-
-        if( expanded ){
-            $body.slideDown( 'fast' );
-        } else {
-            $body.slideUp( 'fast' );
-        }
-        this.trigger( 'toggleBodyVisibility', this.model.get( 'id' ), expanded );
-    },
-
     // ................................................................................ UTILTIY
     toString : function(){
         var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
 });
 
 //------------------------------------------------------------------------------
-HDAView.templates = {
-    warningMsg          : Handlebars.templates[ 'template-warningmessagesmall' ],
-
-    messages            : Handlebars.templates[ 'template-history-warning-messages' ],
-    titleLink           : Handlebars.templates[ 'template-history-titleLink' ],
-    hdaSummary          : Handlebars.templates[ 'template-history-hdaSummary' ],
-    downloadLinks       : Handlebars.templates[ 'template-history-downloadLinks' ],
-    failedMetadata      : Handlebars.templates[ 'template-history-failedMetaData' ],
-    tagArea             : Handlebars.templates[ 'template-history-tagArea' ],
-    annotationArea      : Handlebars.templates[ 'template-history-annotationArea' ],
-    displayApps         : Handlebars.templates[ 'template-history-displayApps' ]
+HDAEditView.templates = {
+    tagArea             : Handlebars.templates[ 'template-hda-tagArea' ],
+    annotationArea      : Handlebars.templates[ 'template-hda-annotationArea' ]
 };
 
 //==============================================================================
         $.ajax({
             url: vis_url + '/list_tracks?f-' + $.param(params),
             dataType: "html",
-            error: function() { alert( "Could not add this dataset to browser." ); },
+            error: function() { alert( _l( "Could not add this dataset to browser" ) + '.' ); },
             success: function(table_html) {
                 var parent = window.parent;
 
-                parent.show_modal("View Data in a New or Saved Visualization", "", {
+                parent.show_modal( _l( "View Data in a New or Saved Visualization" ), "", {
                     "Cancel": function() {
                         parent.hide_modal();
                     },
                     "View in saved visualization": function() {
                         // Show new modal with saved visualizations.
-                        parent.show_modal("Add Data to Saved Visualization", table_html, {
+                        parent.show_modal( _l( "Add Data to Saved Visualization" ), table_html, {
                             "Cancel": function() {
                                 parent.hide_modal();
                             },

static/scripts/mvc/dataset/hda-model.js

         // aka. !hidden
         visible             : false,
         // based on trans.user (is_admin or security_agent.can_access_dataset( <user_roles>, hda.dataset ))
-        accessible          : false,
-        
-        //TODO: this needs to be removed (it is a function of the view type (e.g. HDAForEditingView))
-        for_editing         : true
+        accessible          : false
     },
 
     // fetch location of this history in the api

static/scripts/mvc/history/history-model.js

         name            : '',
         state           : '',
 
-        //TODO: wire these to items (or this)
-        show_deleted     : false,
-        show_hidden      : false,
-
         diskSize : 0,
         deleted : false,
 
-        tags        : [],
+        //tags        : [],
         annotation  : null,
 
-        //TODO: quota msg and message? how to get those over the api?
-        message     : null,
-        quotaMsg    : false
+        //TODO: message? how to get over the api?
+        message     : null
     },
 
     url : function(){
         //    this.log( this + ' has changed:', currModel, changedList );
         //});
         //this.bind( 'all', function( event ){
-        //    this.log( this + '', arguments );
+        //    //this.log( this + '', arguments );
+        //    console.info( this + '', arguments );
         //});
     },
 
     // get data via the api (alternative to sending options,hdas to initialize)
-    loadFromApi : function( historyId, callback ){
+    //TODO: this needs work - move to more straightforward deferred
+    // events: loaded, loaded:user, loaded:hdas
+    loadFromApi : function( historyId, success ){
         var history = this;
 
         // fetch the history AND the user (mainly to see if they're logged in at this point)
         history.attributes.id = historyId;
         //TODO:?? really? fetch user here?
-        jQuery.when( jQuery.ajax( 'api/users/current' ), history.fetch()
+        jQuery.when(
+                jQuery.ajax( 'api/users/current' ),
+                history.fetch()
 
             ).then( function( userResponse, historyResponse ){
-                //console.warn( 'fetched user, history: ', userResponse, historyResponse );
+                //console.warn( 'fetched user: ', userResponse[0] );
+                //console.warn( 'fetched history: ', historyResponse[0] );
                 history.attributes.user = userResponse[0]; //? meh.
-                history.log( history );
+
+                history.trigger( 'loaded:user', userResponse[0] );
+                history.trigger( 'loaded', historyResponse[0] );
 
             }).then( function(){
                 // ...then the hdas (using contents?ids=...)
                 jQuery.ajax( history.url() + '/contents?' + jQuery.param({
-                    ids : history.itemIdsFromStateIds().join( ',' )
+                    ids : history.hdaIdsFromStateIds().join( ',' )
 
                 // reset the collection to the hdas returned
                 })).success( function( hdas ){
-                    //console.warn( 'fetched hdas' );
+                    //console.warn( 'fetched hdas', hdas );
                     history.hdas.reset( hdas );
                     history.checkForUpdates();
-                    callback();
+
+                    history.trigger( 'loaded:hdas', hdas );
+                    if( success ){ callback( history ); }
                 });
             });
     },
     },
 
     // get the history's state from it's cummulative ds states, delay + update if needed
+    // events: ready
     checkForUpdates : function( datasets ){
         // get overall History state from collection, run updater if History has running/queued hdas
         // boiling it down on the client to running/not
         if( this.hdas.running().length ){
             this.stateUpdater();
+
+        } else {
+            this.trigger( 'ready' );
         }
         return this;
     },
 
     // update this history, find any hda's running/queued, update ONLY those that have changed states,
     //  set up to run this again in some interval of time
+    // events: ready
     stateUpdater : function(){
         var history = this,
             oldState = this.get( 'state' ),
                 setTimeout( function(){
                     history.stateUpdater();
                 }, 4000 );
+
+            // otherwise, we're now in a 'ready' state (no hdas running)
+            } else {
+                history.trigger( 'ready' );
             }
 
         }).error( function( xhr, status, error ){
             if( console && console.warn ){
                 console.warn( 'Error getting history updates from the server:', xhr, status, error );
             }
-            alert( 'Error getting history updates from the server.\n' + error );
+            alert( _l( 'Error getting history updates from the server.' ) + '\n' + error );
         });
     },
 
 var HistoryCollection = Backbone.Collection.extend( LoggableMixin ).extend({
     model   : History,
     urlRoot : 'api/histories',
-    logger  : console
+    //logger  : console
 });
 
 //==============================================================================

static/scripts/mvc/history/history-panel.js

 Backbone.js implementation of history panel
 
 TODO:
+    refactoring on for_editing:
+        uhoh: purge link in warning message in history_common.mako conditional on trans.app.config.allow_user_dataset_purge
+        bug: rerun still doesn't take encoded ids
+
     anon user, mako template init:
-        bug: rename url seems to be wrong url
+        BUG: shouldn't have tag/anno buttons (on hdas)
+            Check for user in hdaView somehow
 
     logged in, mako template:
+        bug: rename not being changed locally - render() shows old name, refresh: new name
+            TODO: editable text to MV, might also just use REST.update on history
         BUG: meter is not updating RELIABLY on change:nice_size
         BUG: am able to start upload even if over quota - 'runs' forever
         bug: quotaMeter bar rendering square in chrome
-        BUG: quotaMsg not showing when 100% (on load)
-        BUG: imported, shared history with unaccessible dataset errs in historycontents when getting history
-            (entire history is inaccessible)
-            ??: still happening?
 
     from loadFromApi:
-        BUG: not showing previous annotations
 
     fixed:
+        BUG: not loading deleted datasets
+            FIXED: history_contents, show: state_ids returns all ids now (incl. deleted)
         BUG: upload, history size, doesn't change
             FIXED: using change:nice_size to trigger re-render of history size
         BUG: delete uploading hda - now in state 'discarded'! ...new state to handle
         show_deleted/hidden:
             use storage
             on/off ui
+                need urls
+                change template
         move histview fadein/out in render to app?
         don't draw body until it's first expand event
         localize all
 
     // direct attachment to existing element
     el                  : 'body.historyPage',
+    //HDAView             : HDABaseView,
+    HDAView             : HDAEditView,
 
     // init with the model, urlTemplates, set up storage, bind HDACollection events
     //NOTE: this will create or load PersistantStorage keyed under 'HistoryView.<id>'
         // set up url templates
         //TODO: prob. better to put this in class scope (as the handlebars templates), but...
         //  they're added to GalaxyPaths on page load (after this file is loaded)
-        if( !attributes.urlTemplates ){         throw( 'HDAView needs urlTemplates on initialize' ); }
-        if( !attributes.urlTemplates.history ){ throw( 'HDAView needs urlTemplates.history on initialize' ); }
-        if( !attributes.urlTemplates.hda ){     throw( 'HDAView needs urlTemplates.hda on initialize' ); }
+        if( !attributes.urlTemplates ){         throw( this + ' needs urlTemplates on initialize' ); }
+        if( !attributes.urlTemplates.history ){ throw( this + ' needs urlTemplates.history on initialize' ); }
+        if( !attributes.urlTemplates.hda ){     throw( this + ' needs urlTemplates.hda on initialize' ); }
         this.urlTemplates = attributes.urlTemplates.history;
         this.hdaUrlTemplates = attributes.urlTemplates.hda;
 
         // data that needs to be persistant over page refreshes
         //  (note the key function which uses the history id as well)
-        this.storage = new PersistantStorage(
-            'HistoryView.' + this.model.get( 'id' ),
-            { expandedHdas : {} }
-        );
+        this.storage = new PersistantStorage( 'HistoryView.' + this.model.get( 'id' ), {
+            expandedHdas : {},
+            show_deleted : false,
+            show_hidden  : false
+        });
+        this.log( 'this.storage:', this.storage.get() );
+
+        // get the show_deleted/hidden settings giving priority to values passed into initialize, but
+        //  using web storage otherwise
+        this.log( 'show_deleted:', attributes.show_deleted, 'show_hidden', attributes.show_hidden );
+        // if the page has specifically requested show_deleted/hidden, these will be either true or false
+        //  (as opposed to undefined, null) - and we give priority to that setting
+        if( ( attributes.show_deleted === true ) || ( attributes.show_deleted === false ) ){
+            // save them to web storage
+            this.storage.set( 'show_deleted', attributes.show_deleted );
+        }
+        if( ( attributes.show_hidden === true ) || ( attributes.show_hidden === false ) ){
+            this.storage.set( 'show_hidden', attributes.show_hidden );
+        }
+        // pull show_deleted/hidden from the web storage  if the page hasn't specified whether to show_deleted/hidden,
+        this.show_deleted = this.storage.get( 'show_deleted' );
+        this.show_hidden  = this.storage.get( 'show_hidden' );
+        this.log( 'this.show_deleted:', this.show_deleted, 'show_hidden', this.show_hidden );
+        this.log( '(now) this.storage:', this.storage.get() );
 
         // bind events from the model's hda collection
         //this.model.bind( 'change', this.render, this );
     },
 
     // render urls, historyView body, and hdas (if any are shown), fade out, swap, fade in, set up behaviours
+    // events: rendered, rendered:initial
     render : function(){
         var historyView = this,
             setUpQueueName = historyView.toString() + '.set-up',
         // render the main template, tooltips
         //NOTE: this is done before the items, since item views should handle theirs themselves
         newRender.append( HistoryPanel.templates.historyPanel( modelJson ) );
-        newRender.find( '.tooltip' ).tooltip();
+        newRender.find( '.tooltip' ).tooltip({ placement: 'bottom' });
+        this.setUpActionButton( newRender.find( '#history-action-popup' ) );
 
         // render hda views (if any and any shown (show_deleted/hidden)
+        //TODO: this seems too elaborate
         if( !this.model.hdas.length
         ||  !this.renderItems( newRender.find( '#' + this.model.get( 'id' ) + '-datasets' ) ) ){
             // if history is empty or no hdas would be rendered, show the empty message
         return this;
     },
 
+    setUpActionButton : function( $button ){
+        var historyPanel = this,
+            show_deletedText = ( this.storage.get( 'show_deleted' ) )?( 'Hide deleted' ):( 'Show deleted' ),
+            show_hiddenText  = ( this.storage.get( 'show_hidden' )  )?( 'Hide hidden'  ):( 'Show hidden' ),
+            menuActions  = {};
+        menuActions[ _l( 'refresh' ) ]          = function(){ window.location.reload(); };
+        menuActions[ _l( 'collapse all' ) ]     = function(){ historyPanel.hideAllHdaBodies(); };
+        menuActions[ _l( show_deletedText ) ]   = function(){ historyPanel.toggleShowDeleted(); };
+        menuActions[ _l( show_hiddenText  ) ]   = function(){ historyPanel.toggleShowHidden(); };
+        make_popupmenu( $button, menuActions );
+    },
+
     // set up a view for each item to be shown, init with model and listeners, cache to map ( model.id : view )
     renderItems : function( $whereTo ){
         this.hdaViews = {};
         var historyView = this,
-            show_deleted = this.model.get( 'show_deleted' ),
-            show_hidden  = this.model.get( 'show_hidden' ),
-            visibleHdas  = this.model.hdas.getVisible( show_deleted, show_hidden );
+            // only render the shown hdas
+            //TODO: switch to more general filtered pattern
+            visibleHdas  = this.model.hdas.getVisible(
+                this.storage.get( 'show_deleted' ),
+                this.storage.get( 'show_hidden' )
+            );
 
-        // only render the shown hdas
         _.each( visibleHdas, function( hda ){
             var hdaId = hda.get( 'id' ),
                 expanded = historyView.storage.get( 'expandedHdas' ).get( hdaId );
-            historyView.hdaViews[ hdaId ] = new HDAView({
+