commit/galaxy-central: carlfeberhard: History structure: (<root>/history/structure) horizontal layout of job/history DAG separated into components (a work in progress) and associated qunit tests; Job model and list-item; rudimentary js graph library and tests; Qunit: allow import of test-data; minor fixes
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/eacfa5a1a3f2/ Changeset: eacfa5a1a3f2 User: carlfeberhard Date: 2014-10-15 17:11:30+00:00 Summary: History structure: (<root>/history/structure) horizontal layout of job/history DAG separated into components (a work in progress) and associated qunit tests; Job model and list-item; rudimentary js graph library and tests; Qunit: allow import of test-data; minor fixes Affected #: 32 files diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/mvc/collection/collection-li.js --- a/client/galaxy/scripts/mvc/collection/collection-li.js +++ b/client/galaxy/scripts/mvc/collection/collection-li.js @@ -100,8 +100,6 @@ /** add the DCE class to the list item */ className : ListItemView.prototype.className + " dataset-collection-element", - /** jq fx speed for this view */ - fxSpeed : 'fast', /** set up */ initialize : function( attributes ){ diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/mvc/history/history-structure-view.js --- /dev/null +++ b/client/galaxy/scripts/mvc/history/history-structure-view.js @@ -0,0 +1,409 @@ +define([ + 'mvc/job/job-model', + 'mvc/job/job-li', + 'mvc/history/history-content-model', + 'mvc/history/job-dag', + 'mvc/base-mvc', + 'utils/localization', + 'libs/d3' +], function( JOB, JOB_LI, HISTORY_CONTENT, JobDAG, BASE_MVC, _l ){ +// ============================================================================ +/* TODO: +change component = this to something else + +*/ +// ============================================================================ +var HistoryStructureComponent = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({ + + //logger : console, + + className : 'history-structure-component', + + initialize : function( attributes ){ + this.log( this + '(HistoryStructureComponent).initialize:', attributes ); + this.component = attributes.component; + + this._jobLiMap = {}; + this._createJobModels(); + + this.layout = this._createLayout( attributes.layoutOptions ); + }, + + _createJobModels : function(){ + var view = this; + view.component.eachVertex( function( vertex ){ + var jobJSON = vertex.data.job, + job = new JOB.Job( jobJSON ); + + // get the models of the outputs for this job from the history + var outputModels = _.map( job.get( 'outputs' ), function( output ){ + //note: output is { src: 'hda/dataset_collection', id: <some id> } + // job output doesn't *quite* match up to normal typeId + var type = output.src === 'hda'? 'dataset' : 'dataset_collection', + typeId = HISTORY_CONTENT.typeIdStr( type, output.id ); + return view.model.contents.get( typeId ); + }); + // set the collection (HistoryContents) for the job to that json (setting historyId for proper ajax urls) + job.outputCollection.reset( outputModels ); + job.outputCollection.historyId = structure.model.id; + //this.debug( job.outputCollection ); + + // create the bbone view for the job (to be positioned later accrd. to the layout) and cache + var li = new JOB_LI.JobListItemView({ model: job, expanded: true }); + //var li = new JOB_LI.JobListItemView({ model: job }); + li.$el.appendTo( view.$el ); + view._jobLiMap[ job.id ] = li; + }); + return view.jobs; + }, + + layoutDefaults : { + paddingTop : 8, + paddingLeft : 20, + linkSpacing : 16, + jobHeight : 308, + jobWidthSpacing : 320, + linkAdjX : 4, + linkAdjY : 0 + }, + + _createLayout : function( options ){ + options = _.defaults( _.clone( options || {} ), this.layoutDefaults ); + var view = this, + vertices = _.values( view.component.vertices ), + layout = _.extend( options, { + nodeMap : {}, + links : [], + el : { width: 0, height: 0 }, + svg : { width: 0, height: 0, top: 0, left: 0 } + }); + + vertices.forEach( function( v, j ){ + var node = { name: v.name, x: 0, y: 0 }; + layout.nodeMap[ v.name ] = node; + }); + + view.component.edges( function( e ){ + var link = { + source: e.source, + target: e.target + }; + layout.links.push( link ); + }); + //this.debug( JSON.stringify( layout, null, ' ' ) ); + return layout; + }, + + _updateLayout : function(){ + var view = this, + layout = view.layout; + + layout.svg.height = layout.paddingTop + ( layout.linkSpacing * _.size( layout.nodeMap ) ); + layout.el.height = layout.svg.height + layout.jobHeight; + +//TODO:?? can't we just alter the component v and e's directly? + // layout the job views putting jobWidthSpacing btwn each + var x = layout.paddingLeft, + y = layout.svg.height; + _.each( layout.nodeMap, function( node, jobId ){ + //this.debug( node, jobId ); + node.x = x; + node.y = y; + x += layout.jobWidthSpacing; + }); + layout.el.width = layout.svg.width = Math.max( layout.el.width, x ); + + // layout the links - connecting each job by it's main coords (currently) +//TODO: somehow adjust the svg height based on the largest distance the longest connection needs + layout.links.forEach( function( link ){ + var source = layout.nodeMap[ link.source ], + target = layout.nodeMap[ link.target ]; + link.x1 = source.x + layout.linkAdjX; + link.y1 = source.y + layout.linkAdjY; + link.x2 = target.x + layout.linkAdjX; + link.y2 = target.y + layout.linkAdjY; + }); + //this.debug( JSON.stringify( layout, null, ' ' ) ); + return this.layout; + }, + + render : function( options ){ + this.log( this + '.renderComponent:', options ); + var view = this; + + view.component.eachVertex( function( v ){ + //TODO:? liMap needed - can't we attach to vertex? + var li = view._jobLiMap[ v.name ]; + if( !li.$el.is( ':visible' ) ){ + li.render( 0 ); + } + }); + + view._updateLayout(); + // set up the display containers + view.$el + .width( view.layout.el.width ) + .height( view.layout.el.height ); + this.renderSVG(); + + // position the job views accrd. to the layout + view.component.eachVertex( function( v ){ + //TODO:? liMap needed - can't we attach to vertex? + var li = view._jobLiMap[ v.name ], + position = view.layout.nodeMap[ li.model.id ]; + console.debug( li.$el.is( ':visible' ) ); + //this.debug( position ); + li.$el.css({ top: position.y, left: position.x }); + }); + return this; + }, + + renderSVG : function(){ + var view = this, + layout = view.layout; + + var svg = d3.select( this.el ).select( 'svg' ); + if( svg.empty() ){ + svg = d3.select( this.el ).append( 'svg' ); + } + + svg + .attr( 'width', layout.svg.width ) + .attr( 'height', layout.svg.height ); + + function highlightConnect( d ){ + d3.select( this ).classed( 'highlighted', true ); + view._jobLiMap[ d.source ].$el.addClass( 'highlighted' ); + view._jobLiMap[ d.target ].$el.addClass( 'highlighted' ); + } + + function unhighlightConnect( d ){ + d3.select( this ).classed( 'highlighted', false ); + view._jobLiMap[ d.source ].$el.removeClass( 'highlighted' ); + view._jobLiMap[ d.target ].$el.removeClass( 'highlighted' ); + } + + var connections = svg.selectAll( '.connection' ) + .data( layout.links ); + + connections + .enter().append( 'path' ) + .attr( 'class', 'connection' ) + .attr( 'id', function( d ){ return d.source + '-' + d.target; }) + .on( 'mouseover', highlightConnect ) + .on( 'mouseout', unhighlightConnect ); + + connections.transition() + .attr( 'd', function( d ){ return view._connectionPath( d ); }); + +//TODO: ? can we use tick here to update the links? + + return svg.node(); + }, + + _connectionPath : function( d ){ + var CURVE_X = 0, + controlY = ( ( d.x2 - d.x1 ) / this.layout.svg.width ) * this.layout.svg.height; + return [ + 'M', d.x1, ',', d.y1, ' ', + 'C', + d.x1 + CURVE_X, ',', d.y1 - controlY, ' ', + d.x2 - CURVE_X, ',', d.y2 - controlY, ' ', + d.x2, ',', d.y2 + ].join( '' ); + }, + + events : { + 'mouseover .job.list-item' : function( ev ){ this.highlightConnected( ev.currentTarget, true ); }, + 'mouseout .job.list-item' : function( ev ){ this.highlightConnected( ev.currentTarget, false ); } + }, + + highlightConnected : function( jobElement, highlight ){ + highlight = highlight !== undefined? highlight : true; + + var view = this, + component = view.component, + jobClassFn = highlight? jQuery.prototype.addClass : jQuery.prototype.removeClass, + connectionClass = highlight? 'connection highlighted' : 'connection'; + + //console.debug( 'mouseover', this ); + var $jobs = jobClassFn.call( $( jobElement ), 'highlighted' ), + id = $jobs.attr( 'id' ).replace( 'job-', '' ); + + // immed. ancestors + component.edges({ target: id }).forEach( function( edge ){ + jobClassFn.call( view.$( '#job-' + edge.source ), 'highlighted' ); + view.$( '#' + edge.source + '-' + id ).attr( 'class', connectionClass ); + }); + // descendants + component.vertices[ id ].eachEdge( function( edge ){ + jobClassFn.call( view.$( '#job-' + edge.target ), 'highlighted' ); + view.$( '#' + id + '-' + edge.target ).attr( 'class', connectionClass ); + }); + }, + + toString : function(){ + return 'HistoryStructureComponent(' + this.model.id + ')'; + } +}); + + +// ============================================================================ +var VerticalHistoryStructureComponent = HistoryStructureComponent.extend({ + + logger : console, + + className : HistoryStructureComponent.prototype.className + ' vertical', + + layoutDefaults : { + paddingTop : 8, + paddingLeft : 20, + linkSpacing : 16, + jobWidth : 308, + jobHeight : 308, + initialSpacing : 64, + jobSpacing : 16, + linkAdjX : 0, + linkAdjY : 4 + }, + +//TODO: how can we use the dom height of the job li's - they're not visible when this is called? + _updateLayout : function(){ + var view = this, + layout = view.layout; + //this.info( this.cid, '_updateLayout' ) + + layout.svg.width = layout.paddingLeft + ( layout.linkSpacing * _.size( layout.nodeMap ) ); + layout.el.width = layout.svg.width + layout.jobWidth; + + // reset height - we'll get the max Y below to assign to it + layout.el.height = 0; + +//TODO:?? can't we just alter the component v and e's directly? + var x = layout.svg.width, + y = layout.paddingTop; + _.each( layout.nodeMap, function( node, jobId ){ + //this.debug( node, jobId ); + node.x = x; + node.y = y; + var li = view._jobLiMap[ jobId ]; + if( li.$el.is( ':visible' ) ){ + console.debug( 'li is visible, adj. by:', li.$el.height() ); + y += li.$el.height() + layout.jobSpacing; + } else { + y += layout.initialSpacing + layout.jobSpacing; + } + }); + layout.el.height = layout.svg.height = Math.max( layout.el.height, y ); + + // layout the links - connecting each job by it's main coords (currently) + layout.links.forEach( function( link ){ + var source = layout.nodeMap[ link.source ], + target = layout.nodeMap[ link.target ]; + link.x1 = source.x + layout.linkAdjX; + link.y1 = source.y + layout.linkAdjY; + link.x2 = target.x + layout.linkAdjX; + link.y2 = target.y + layout.linkAdjY; + view.debug( 'link:', link.x1, link.y1, link.x2, link.y2, link ); + }); + //this.debug( JSON.stringify( layout, null, ' ' ) ); + view.debug( 'el:', layout.el ); + view.debug( 'svg:', layout.svg ); + return layout; + }, + + _connectionPath : function( d ){ + var CURVE_Y = 0, + controlX = ( ( d.y2 - d.y1 ) / this.layout.svg.height ) * this.layout.svg.width; + return [ + 'M', d.x1, ',', d.y1, ' ', + 'C', + d.x1 - controlX, ',', d.y1 + CURVE_Y, ' ', + d.x2 - controlX, ',', d.y2 - CURVE_Y, ' ', + d.x2, ',', d.y2 + ].join( '' ); + }, + + toString : function(){ + return 'VerticalHistoryStructureComponent(' + this.model.id + ')'; + } +}); + + +// ============================================================================ +var HistoryStructureView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({ + + //logger : console, + + className : 'history-structure', + + initialize : function( attributes ){ + this.log( this + '(HistoryStructureView).initialize:', attributes, this.model ); +//TODO: to model + this.jobs = attributes.jobs; + this._createDAG(); + }, + + _createDAG : function(){ + this.dag = new JobDAG({ + historyContents : this.model.contents.toJSON(), + jobs : this.jobs, + excludeSetMetadata : true, + excludeErroredJobs : true + }); +window.dag = this.dag; + this.log( this + '.dag:', this.dag ); + + this._createComponents(); + }, + + _createComponents : function(){ + this.log( this + '._createComponents' ); + var structure = this; +window.structure = structure; + + structure.componentViews = structure.dag.weakComponentGraphArray().map( function( componentGraph ){ + return structure._createComponent( componentGraph ); + }); + }, + + _createComponent : function( component ){ + this.log( this + '._createComponent:', component ); + return new HistoryStructureComponent({ + //return new VerticalHistoryStructureComponent({ + model : this.model, + component : component + }); + }, + + render : function( options ){ + this.log( this + '.render:', options ); + var structure = this; + + structure.$el.html([ + '<div class="controls"></div>', + '<div class="components"></div>' + ].join( '' )); + + structure.componentViews.forEach( function( component ){ + component.render().$el.appendTo( structure.$components() ); + }); + return structure; + }, + + $components : function(){ + return this.$( '.components' ); + }, + + toString : function(){ + return 'HistoryStructureView(' + ')'; + } +}); + + +// ============================================================================ + //return { + // HistoryStructureView : HistoryStructureView + //}; + return HistoryStructureView; +}); diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/mvc/history/job-dag.js --- /dev/null +++ b/client/galaxy/scripts/mvc/history/job-dag.js @@ -0,0 +1,204 @@ +define([ + 'utils/graph', + 'utils/add-logging' +],function( GRAPH, addLogging ){ +// ============================================================================ +var _super = GRAPH.Graph; +var JobDAG = function( options ){ + var self = this; + //this.logger = console; + + // instance vars +//TODO: needed? + self._historyContentsMap = {}; + self.filters = []; + + self._idMap = {}; + self.noInputJobs = []; + self.noOutputJobs = []; + +//TODO: save these? + self.filteredSetMetadata = []; + self.filteredErroredJobs = []; + + _super.call( self, true, null, options ); +}; +JobDAG.prototype = new GRAPH.Graph(); +JobDAG.prototype.constructor = JobDAG; +addLogging( JobDAG ); + +// ---------------------------------------------------------------------------- +JobDAG.prototype.init = function _init( options ){ + options = options || {}; + _super.prototype.init.call( this, options ); + + var self = this, + historyContentsJSON = options.historyContents || []; + jobsJSON = options.jobs || []; + + historyContentsJSON.forEach( function( content, i ){ + self._historyContentsMap[ content.id ] = _.clone( content ); + }); + + self.options = _.defaults( _.omit( options, 'historyContents', 'jobs' ), { + excludeSetMetadata : false + }); + self.filters = self._initFilters(); + +//TODO: O( 3N ) + self.preprocessJobs( _.clone( jobsJSON ) ); + self.createGraph(); + + return self; +}; + +JobDAG.prototype._initFilters = function __initFilters(){ + var self = this, + filters = []; + + if( self.options.excludeSetMetadata ){ + self.filteredSetMetadata = []; + filters.push( function filterSetMetadata( jobData ){ + if( jobData.job.tool_id !== '__SET_METADATA__' ){ return true; } + self.filteredSetMetadata.push( jobData.job.id ); + return false; + }); + } + + if( self.options.excludeErroredJobs ){ + self.filteredErroredJobs = []; + filters.push( function filterErrored( jobData ){ + if( jobData.job.state !== 'error' ){ return true; } + self.filteredErroredJobs.push( jobData.job.id ); + return false; + }); + } + + // all outputs deleted + // all outputs hidden + + if( _.isArray( self.options.filters ) ){ + filters = filters.concat( self.options.filters ); + } + self.debug( 'filters len:', filters.length ); + return filters; +}; + +JobDAG.prototype.preprocessJobs = function _preprocessJobs( jobs ){ + this.info( 'processing jobs' ); + + var self = this; +//TODO:? sorting neccessary? + self.sort( jobs ).forEach( function( job, i ){ + var jobData = self.preprocessJob( job, i ); + if( jobData ){ + self._idMap[ job.id ] = jobData; + } + }); + return self; +}; + +JobDAG.prototype.sort = function _sort( jobs ){ + function cmpCreate( a, b ){ + if( a.update_time > b.update_time ){ return 1; } + if( a.update_time < b.update_time ){ return -1; } + return 0; + } + return jobs.sort( cmpCreate ); +}; + +JobDAG.prototype.preprocessJob = function _preprocessJob( job, index ){ + //this.info( 'preprocessJob', job, index ); + var self = this, + jobData = { index: index, job: job }; + + jobData.inputs = self.datasetMapToIdArray( job.inputs, function( dataset, nameInJob ){ + //TODO:? store output name in self._datasets[ output.id ] from creating job? + }); + if( jobData.inputs.length === 0 ){ + self.noInputJobs.push( job.id ); + } + jobData.outputs = self.datasetMapToIdArray( job.outputs, function( dataset, nameInJob ){ + + }); + if( jobData.outputs.length === 0 ){ + self.noOutputJobs.push( job.id ); + } + + //self.debug( JSON.stringify( jobData, null, ' ' ) ); + // apply filters after processing job allowing access to the additional data above in the filters + for( var i=0; i<self.filters.length; i++ ){ + if( !self.filters[i].call( self, jobData ) ){ + self.debug( 'job', job.id, ' has been filtered out by function:\n', self.filters[i] ); + return null; + } + } + + //self.info( 'preprocessJob returning', jobData ); + return jobData; +}; + +JobDAG.prototype.datasetMapToIdArray = function _datasetMapToIdArray( datasetMap, processFn ){ + var self = this; + return _.map( datasetMap, function( dataset, nameInJob ){ + if( !dataset.id ){ + throw new Error( 'No id on datasetMap: ', JSON.stringify( dataset ) ); + } + if( !dataset.src || dataset.src !== 'hda' ){ + throw new Error( 'Bad src on datasetMap: ', JSON.stringify( dataset ) ); + } + processFn.call( self, dataset, nameInJob ); + return dataset.id; + }); +}; + +JobDAG.prototype.createGraph = function _createGraph(){ + var self = this; + self.debug( 'connections:' ); + + _.each( self._idMap, function( sourceJobData, sourceJobId ){ + self.debug( '\t', sourceJobId, sourceJobData ); + self.createVertex( sourceJobId, sourceJobData ); + + sourceJobData.usedIn = []; + _.each( sourceJobData.outputs, function( outputDatasetId ){ +//TODO: O(n^2) + _.each( self._idMap, function( targetJobData, targetJobId ){ + if( targetJobData === sourceJobData ){ return; } + + if( targetJobData.inputs.indexOf( outputDatasetId ) !== -1 ){ + self.info( '\t\t\t found connection: ', sourceJobId, targetJobId ); + sourceJobData.usedIn.push({ job: targetJobId, output: outputDatasetId }); + + self.createVertex( targetJobId, targetJobData ); + + self.createEdge( sourceJobId, targetJobId, self.directed, { + dataset : outputDatasetId + }); + } + }); + }); + }); + self.debug( 'job data: ', JSON.stringify( self._idMap, null, ' ' ) ); + return self; +}; + +Graph.prototype.weakComponentGraphArray = function(){ + return this.weakComponents().map( function( component ){ +//TODO: this seems to belong above (in sort) - why isn't it preserved? + component.vertices.sort( function cmpCreate( a, b ){ + if( a.data.job.update_time > b.data.job.update_time ){ return 1; } + if( a.data.job.update_time < b.data.job.update_time ){ return -1; } + return 0; + }); + return new Graph( this.directed, component ); + }); +}; + + +// ============================================================================ + return JobDAG; + //return { + // jobDAG : jobDAG + //}; +}); diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/mvc/job/job-li.js --- /dev/null +++ b/client/galaxy/scripts/mvc/job/job-li.js @@ -0,0 +1,129 @@ +define([ + 'mvc/list/list-item', + 'mvc/dataset/dataset-list', + "mvc/base-mvc", + "utils/localization" +], function( LIST_ITEM, DATASET_LIST, BASE_MVC, _l ){ +//============================================================================== +var _super = LIST_ITEM.FoldoutListItemView; +/** @class + */ +var JobListItemView = _super.extend(/** @lends JobListItemView.prototype */{ + + /** logger used to record this.log messages, commonly set to console */ + //logger : console, + + + className : _super.prototype.className + " job", + id : function(){ + return [ 'job', this.model.get( 'id' ) ].join( '-' ); + }, + + foldoutPanelClass : DATASET_LIST.DatasetList, + + /** Set up: instance vars, options, and event handlers */ + initialize : function( attributes ){ + if( attributes.logger ){ this.logger = this.model.logger = attributes.logger; } + this.log( this + '.initialize:', attributes ); + _super.prototype.initialize.call( this, attributes ); + + /** where should pages from links be displayed? (default to new tab/window) */ + this.linkTarget = attributes.linkTarget || '_blank'; + this._setUpListeners(); + }, + + /** In this override, add the state as a class for use with state-based CSS */ + _swapNewRender : function( $newRender ){ + _super.prototype._swapNewRender.call( this, $newRender ); + if( this.model.has( 'state' ) ){ + this.$el.addClass( 'state-' + this.model.get( 'state' ) ); + } + return this.$el; + }, + + /** Stub to return proper foldout panel options */ + _getFoldoutPanelOptions : function(){ + var options = _super.prototype._getFoldoutPanelOptions.call( this ); + return _.extend( options, { + collection : this.model.outputCollection, + selecting : false + }); + }, + + // ........................................................................ misc + /** String representation */ + toString : function(){ + return 'JobListItemView(' + this.model + ')'; + } +}); + +// ............................................................................ TEMPLATES +/** underscore templates */ +JobListItemView.prototype.templates = (function(){ +//TODO: move to require text! plugin + + var elTemplate = BASE_MVC.wrapTemplate([ + '<div class="list-element">', + '<div class="id"><%= model.id %></div>', + // errors, messages, etc. + '<div class="warnings"></div>', + + // multi-select checkbox + '<div class="selector">', + '<span class="fa fa-2x fa-square-o"></span>', + '</div>', + // space for title bar buttons - gen. floated to the right + '<div class="primary-actions"></div>', + '<div class="title-bar"></div>', + + // expandable area for more details + '<div class="details"></div>', + '</div>' + ]); + + var titleBarTemplate = BASE_MVC.wrapTemplate([ + // adding a tabindex here allows focusing the title bar and the use of keydown to expand the dataset display + '<div class="title-bar clear" tabindex="0">', + //'<span class="state-icon"></span>', + '<div class="title">', + '<span class="name"><%- job.tool_id %></span>', + '</div>', + '<div class="subtitle"></div>', + '</div>' + ], 'job' ); + + //var subtitleTemplate = BASE_MVC.wrapTemplate([ + // // override this + // '<div class="subtitle">', + // _l( 'Created' ), ': <%= new Date( job.create_time ).toString() %>, ', + // _l( 'Updated' ), ': <%= new Date( job.update_time ).toString() %>', + // '</div>' + //], 'job' ); + // + //var detailsTemplate = BASE_MVC.wrapTemplate([ + // '<div class="details">', + // '<div class="params">', + // '<% _.each( job.params, function( param, paramName ){ %>', + // '<div class="param">', + // '<label class="prompt"><%= paramName %></label>', + // '<span class="value"><%= param %></span>', + // '</div>', + // '<% }) %>', + // '</div>', + // '</div>' + //], 'job' ); + + return _.extend( {}, _super.prototype.templates, { + //el : elTemplate, + titleBar : titleBarTemplate, + //subtitle : subtitleTemplate, + //details : detailsTemplate + }); +}()); + + +//============================================================================= + return { + JobListItemView : JobListItemView + }; +}); diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/mvc/job/job-model.js --- /dev/null +++ b/client/galaxy/scripts/mvc/job/job-model.js @@ -0,0 +1,202 @@ +define([ + "mvc/history/history-contents", + "mvc/dataset/states", + "utils/ajax-queue", + "mvc/base-mvc", + "utils/localization" +], function( HISTORY_CONTENTS, STATES, AJAX_QUEUE, BASE_MVC, _l ){ +//============================================================================== +var searchableMixin = BASE_MVC.SearchableModelMixin; +/** @class + */ +var Job = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend( + BASE_MVC.mixin( searchableMixin, /** @lends Job.prototype */{ + + /** logger used to record this.log messages, commonly set to console */ + //logger : console, + + /** default attributes for a model */ + defaults : { + model_class : 'Job', + + tool : null, + exit_code : null, + + inputs : {}, + outputs : {}, + params : {}, + + create_time : null, + update_time : null, + state : STATES.NEW + }, + + /** instance vars and listeners */ + initialize : function( attributes, options ){ + this.debug( this + '(Job).initialize', attributes, options ); + this.outputCollection = attributes.outputCollection || new HISTORY_CONTENTS.HistoryContents([]); + this._setUpListeners(); + }, + + /** set up any event listeners + * event: state:ready fired when this DA moves into/is already in a ready state + */ + _setUpListeners : function(){ + // if the state has changed and the new state is a ready state, fire an event + this.on( 'change:state', function( currModel, newState ){ + this.log( this + ' has changed state:', currModel, newState ); + if( this.inReadyState() ){ + this.trigger( 'state:ready', currModel, newState, this.previous( 'state' ) ); + } + }); + }, + + // ........................................................................ common queries + /** Is this job in a 'ready' state; where 'Ready' states are states where no + * processing is left to do on the server. + */ + inReadyState : function(){ + return _.contains( STATES.READY_STATES, this.get( 'state' ) ); + }, + + /** Does this model already contain detailed data (as opposed to just summary level data)? */ + hasDetails : function(){ + //?? this may not be reliable + return _.isEmpty( this.get( 'outputs' ) ); + }, + + // ........................................................................ ajax + /** root api url */ + urlRoot : (( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' )) + 'api/jobs', + //url : function(){ return this.urlRoot; }, + + // ........................................................................ searching + // see base-mvc, SearchableModelMixin + /** what attributes of an Job will be used in a text search */ + searchAttributes : [ + 'tool' + ], + + // ........................................................................ misc + /** String representation */ + toString : function(){ + return [ 'Job(', this.get( 'id' ), ':', this.get( 'tool_id' ), ')' ].join( '' ); + } +})); + + +//============================================================================== +/** @class Backbone collection for Jobs. + */ +var JobCollection = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).extend( +/** @lends JobCollection.prototype */{ + model : Job, + + /** logger used to record this.log messages, commonly set to console */ + //logger : console, + + /** root api url */ + urlRoot : (( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' )) + 'api/jobs', + url : function(){ return this.urlRoot; }, + + intialize : function( models, options ){ + console.debug( models, options ); + }, + + // ........................................................................ common queries + /** Get the ids of every item in this collection + * @returns array of encoded ids + */ + ids : function(){ + return this.map( function( item ){ return item.get( 'id' ); }); + }, + + /** Get jobs that are not ready + * @returns array of content models + */ + notReady : function(){ + return this.filter( function( job ){ + return !job.inReadyState(); + }); + }, + + /** return true if any jobs don't have details */ + haveDetails : function(){ + return this.all( function( job ){ return job.hasDetails(); }); + }, + + // ........................................................................ ajax + queueDetailFetching : function(){ + var collection = this, + queue = new AJAX_QUEUE.AjaxQueue( this.map( function( job ){ + return function(){ + return job.fetch({ silent: true }); + }; + })); + queue.done( function(){ + collection.trigger( 'details-loaded' ); + }); + return queue; + }, + + //toDAG : function(){ + // return new JobDAG( this.toJSON() ); + //}, + + // ........................................................................ sorting/filtering + /** return a new collection of jobs whose attributes contain the substring matchesWhat */ + matches : function( matchesWhat ){ + return this.filter( function( job ){ + return job.matches( matchesWhat ); + }); + }, + + // ........................................................................ misc + /** override to get a correct/smarter merge when incoming data is partial */ + set : function( models, options ){ + // arrrrrrrrrrrrrrrrrg... + // (e.g. stupid backbone) + // w/o this partial models from the server will fill in missing data with model defaults + // and overwrite existing data on the client + // see Backbone.Collection.set and _prepareModel + var collection = this; + models = _.map( models, function( model ){ + if( !collection.get( model.id ) ){ return model; } + + // merge the models _BEFORE_ calling the superclass version + var merged = existing.toJSON(); + _.extend( merged, model ); + return merged; + }); + // now call superclass when the data is filled + Backbone.Collection.prototype.set.call( this, models, options ); + }, + + /** String representation. */ + toString : function(){ + return ([ 'JobCollection(', this.length, ')' ].join( '' )); + } + +//----------------------------------------------------------------------------- class vars +}, { + + fromHistory : function( historyId ){ + console.debug( this ); + var Collection = this, + collection = new Collection([]); + collection.fetch({ data: { history_id: historyId }}) + .done( function(){ + window.queue = collection.queueDetailFetching(); + + }); + return collection; + } +}); + + +//============================================================================= + return { + Job : Job, + JobCollection : JobCollection + }; +}); diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/mvc/list/list-item.js --- a/client/galaxy/scripts/mvc/list/list-item.js +++ b/client/galaxy/scripts/mvc/list/list-item.js @@ -16,7 +16,7 @@ initialize : function( attributes ){ /** are the details of this view expanded/shown or not? */ this.expanded = attributes.expanded || false; - //this.log( '\t expanded:', this.expanded ); + this.log( '\t expanded:', this.expanded ); this.fxSpeed = attributes.fxSpeed || this.fxSpeed; }, @@ -70,7 +70,9 @@ /** empty out the current el, move the $newRender's children in */ _swapNewRender : function( $newRender ){ - return this.$el.empty().attr( 'class', this.className ).append( $newRender.children() ); + return this.$el.empty() + .attr( 'class', _.isFunction( this.className )? this.className(): this.className ) + .append( $newRender.children() ); }, /** set up js behaviors, event handlers for elements within the given container @@ -204,7 +206,7 @@ $newRender.children( '.warnings' ).replaceWith( this._renderWarnings() ); $newRender.children( '.title-bar' ).replaceWith( this._renderTitleBar() ); $newRender.children( '.primary-actions' ).append( this._renderPrimaryActions() ); - $newRender.find( '.title-bar .subtitle' ).replaceWith( this._renderSubtitle() ); + $newRender.find( '> .title-bar .subtitle' ).replaceWith( this._renderSubtitle() ); return $newRender; }, diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 client/galaxy/scripts/utils/graph.js --- /dev/null +++ b/client/galaxy/scripts/utils/graph.js @@ -0,0 +1,573 @@ +define([ +],function(){ +/* ============================================================================ +TODO: + edges can't retain data + +============================================================================ */ +//TODO: go ahead and move to underscore... +function each( d, fn ){ + for( var k in d ){ + if( d.hasOwnProperty( k ) ){ + fn( d[ k ], k, d ); + } + } +} + +function extend( d, d2 ){ + for( var k in d2 ){ + if( d2.hasOwnProperty( k ) ){ + d[ k ] = d2[ k ]; + } + } + return d; +} + +function matches( d, d2 ){ + for( var k in d2 ){ + if( d2.hasOwnProperty( k ) ){ + if( !d.hasOwnProperty( k ) || d[ k ] !== d2[ k ] ){ + return false; + } + } + } + return true; +} + +function iterate( obj, propsOrFn ){ + var fn = typeof propsOrFn === 'function'? propsOrFn : undefined, + props = typeof propsOrFn === 'object'? propsOrFn : undefined, + returned = [], + index = 0; + for( var key in obj ){ + if( obj.hasOwnProperty( key ) ){ + var value = obj[ key ]; + if( fn ){ + returned.push( fn.call( value, value, key, index ) ); + } else if( props ){ +//TODO: break out to sep? + if( typeof value === 'object' && matches( value, props ) ){ + returned.push( value ); + } + } else { + returned.push( value ); + } + index += 1; + } + } + return returned; +} + + +// ============================================================================ +function Edge( source, target, data ){ + var self = this; + self.source = source !== undefined? source : null; + self.target = target !== undefined? target : null; + self.data = data || null; + //if( typeof data === 'object' ){ + // extend( self, data ); + //} + return self; +} +Edge.prototype.toString = function(){ + return this.source + '->' + this.target; +}; + +Edge.prototype.toJSON = function(){ + //TODO: this is safe in most browsers (fns will be stripped) - alter tests to incorporate this in order to pass data + //return this; + var json = { + source : this.source, + target : this.target + }; + if( this.data ){ + json.data = this.data; + } + return json; +}; + +// ============================================================================ +function Vertex( name, data ){ + var self = this; + self.name = name !== undefined? name : '(unnamed)'; + self.data = data || null; + self.edges = {}; + self.degree = 0; + return self; +} +window.Vertex = Vertex; +Vertex.prototype.toString = function(){ + return 'Vertex(' + this.name + ')'; +}; + +//TODO: better name w no collision for either this.eachEdge or this.edges +Vertex.prototype.eachEdge = function( propsOrFn ){ + return iterate( this.edges, propsOrFn ); +}; + +Vertex.prototype.toJSON = function(){ + //return this; + return { + name : this.name, + data : this.data + }; +}; + + +// ============================================================================ +var GraphSearch = function( graph, processFns ){ + var self = this; + self.graph = graph; + + self.processFns = processFns || { + vertexEarly : function( vertex, search ){ + //console.debug( 'processing vertex:', vertex.name, vertex ); + }, + edge : function( from, edge, search ){ + //console.debug( this, 'edge:', from, edge, search ); + }, + vertexLate : function( vertex, search ){ + //console.debug( this, 'vertexLate:', vertex, search ); + } + }; + + self._cache = {}; + return self; +}; + +GraphSearch.prototype.search = function _search( start ){ + var self = this; + if( start in self._cache ){ return self._cache[ start ]; } + if( !( start instanceof Vertex ) ){ start = self.graph.vertices[ start ]; } + return ( self._cache[ start.name ] = self._search( start ) ); +}; + +GraphSearch.prototype._searchTree = function __searchTree( search ){ + var self = this; + return new Graph( true, { + edges: search.edges, + vertices: Object.keys( search.discovered ).map( function( key ){ + return self.graph.vertices[ key ].toJSON(); + }) + }); +}; + +GraphSearch.prototype.searchTree = function _searchTree( start ){ + return this._searchTree( this.search( start ) ); +}; + +// ============================================================================ +var BreadthFirstSearch = function( graph, processFns ){ + var self = this; + GraphSearch.call( this, graph, processFns ); + return self; +}; +BreadthFirstSearch.prototype = new GraphSearch(); +BreadthFirstSearch.prototype.constructor = BreadthFirstSearch; + +BreadthFirstSearch.prototype._search = function __search( start, search ){ + search = search || { + discovered : {}, + //parents : {}, + edges : [] + }; + + var self = this, + queue = []; + + function discoverAdjacent( adj, edge ){ + var source = this; + if( self.processFns.edge ){ self.processFns.edge.call( self, source, edge, search ); } + if( !search.discovered[ adj.name ] ){ + //console.debug( '\t\t\t', adj.name, 'is undiscovered:', search.discovered[ adj.name ] ); + search.discovered[ adj.name ] = true; + //search.parents[ adj.name ] = source; + search.edges.push({ source: source.name, target: adj.name }); + //console.debug( '\t\t\t queuing undiscovered: ', adj ); + queue.push( adj ); + } + } + + //console.debug( 'BFS starting. start:', start ); + search.discovered[ start.name ] = true; + queue.push( start ); + while( queue.length ){ + var vertex = queue.shift(); + //console.debug( '\t Queue is shifting. Current:', vertex, 'queue:', queue ); + if( self.processFns.vertexEarly ){ self.processFns.vertexEarly.call( self, vertex, search ); } + self.graph.eachAdjacent( vertex, discoverAdjacent ); + if( self.processFns.vertexLate ){ self.processFns.vertexLate.call( self, vertex, search ); } + } + //console.debug( 'search.edges:', JSON.stringify( search.edges ) ); + return search; +}; + + +// ============================================================================ +var DepthFirstSearch = function( graph, processFns ){ + var self = this; + GraphSearch.call( this, graph, processFns ); + return self; +}; +DepthFirstSearch.prototype = new GraphSearch(); +DepthFirstSearch.prototype.constructor = DepthFirstSearch; + +DepthFirstSearch.prototype._search = function( start, search ){ + //console.debug( 'depthFirstSearch:', start ); + search = search || { + discovered : {}, + //parents : {}, + edges : [], + entryTimes : {}, + exitTimes : {} + }; + var self = this, + time = 0; + + // discover verts adjacent to the source (this): + // processing each edge, saving the edge to the tree, and caching the reverse path with parents + function discoverAdjacentVertices( adjacent, edge ){ + //console.debug( '\t\t adjacent:', adjacent, 'edge:', edge ); + var sourceVertex = this; + if( self.processFns.edge ){ self.processFns.edge.call( self, sourceVertex, edge, search ); } + if( !search.discovered[ adjacent.name ] ){ + //search.parents[ adjacent.name ] = sourceVertex; + search.edges.push({ source: sourceVertex.name, target: adjacent.name }); + recurse( adjacent ); + } + } + + // use function stack for DFS stack process verts, times, and discover adjacent verts (recursing into them) + function recurse( vertex ){ + //console.debug( '\t recursing into: ', vertex ); + search.discovered[ vertex.name ] = true; + if( self.processFns.vertexEarly ){ self.processFns.vertexEarly.call( self, vertex, search ); } + search.entryTimes[ vertex.name ] = time++; + + self.graph.eachAdjacent( vertex, discoverAdjacentVertices ); + + if( self.processFns.vertexLate ){ self.processFns.vertexLate.call( self, vertex, search ); } + search.exitTimes[ vertex.name ] = time++; + } + // begin recursion with the desired start + recurse( start ); + + return search; +}; + + +// ============================================================================ +function Graph( directed, data, options ){ +//TODO: move directed to options + this.directed = directed || false; + return this.init( options ).read( data ); +} +window.Graph = Graph; + + +Graph.prototype.init = function( options ){ + options = options || {}; + var self = this; + + self.allowReflexiveEdges = options.allowReflexiveEdges || false; + + self.vertices = {}; + self.numEdges = 0; + return self; +}; + +Graph.prototype.read = function( data ){ + if( !data ){ return this; } + var self = this; + if( data.hasOwnProperty( 'nodes' ) ){ return self.readNodesAndLinks( data ); } + if( data.hasOwnProperty( 'vertices' ) ){ return self.readVerticesAndEdges( data ); } + return self; +}; + +//TODO: the next two could be combined +Graph.prototype.readNodesAndLinks = function( data ){ + if( !( data && data.hasOwnProperty( 'nodes' ) ) ){ return this; } + //console.debug( 'readNodesAndLinks:', data ); + //console.debug( 'data:\n' + JSON.stringify( data, null, ' ' ) ); + var self = this; + data.nodes.forEach( function( node ){ + self.createVertex( node.name, node.data ); + }); + //console.debug( JSON.stringify( self.vertices, null, ' ' ) ); + + ( data.links || [] ).forEach( function( edge, i ){ + var sourceName = data.nodes[ edge.source ].name, + targetName = data.nodes[ edge.target ].name; + self.createEdge( sourceName, targetName, self.directed ); + }); + //self.print(); + //console.debug( JSON.stringify( self.toNodesAndLinks(), null, ' ' ) ); + return self; +}; + +Graph.prototype.readVerticesAndEdges = function( data ){ + if( !( data && data.hasOwnProperty( 'vertices' ) ) ){ return this; } + //console.debug( 'readVerticesAndEdges:', data ); + //console.debug( 'data:\n' + JSON.stringify( data, null, ' ' ) ); + var self = this; + data.vertices.forEach( function( node ){ + self.createVertex( node.name, node.data ); + }); + //console.debug( JSON.stringify( self.vertices, null, ' ' ) ); + + ( data.edges || [] ).forEach( function( edge, i ){ + self.createEdge( edge.source, edge.target, self.directed ); + }); + //self.print(); + //console.debug( JSON.stringify( self.toNodesAndLinks(), null, ' ' ) ); + return self; +}; + +Graph.prototype.createVertex = function( name, data ){ + //console.debug( 'createVertex:', name, data ); + if( this.vertices[ name ] ){ return this.vertices[ name ]; } + return ( this.vertices[ name ] = new Vertex( name, data ) ); +}; + +Graph.prototype.createEdge = function( sourceName, targetName, directed, data ){ + //note: allows multiple 'equivalent' edges (to/from same source/target) + //console.debug( 'createEdge:', source, target, directed ); + var isReflexive = sourceName === targetName; + if( !this.allowReflexiveEdges && isReflexive ){ return null; } + + sourceVertex = this.vertices[ sourceName ]; + targetVertex = this.vertices[ targetName ]; + //note: silently ignores edges from/to unknown vertices + if( !( sourceVertex && targetVertex ) ){ return null; } + +//TODO: prob. move to vertex + var self = this, + edge = new Edge( sourceName, targetName, data ); + sourceVertex.edges[ targetName ] = edge; + sourceVertex.degree += 1; + self.numEdges += 1; + + //TODO:! don't like having duplicate edges for non-directed graphs + // mirror edges (reversing source and target) in non-directed graphs + // but only if not reflexive + if( !isReflexive && !directed ){ + // flip directed to prevent recursion loop + self.createEdge( targetName, sourceName, true ); + } + + return edge; +}; + +Graph.prototype.edges = function( propsOrFn ){ + return Array.prototype.concat.apply( [], this.eachVertex( function( vertex ){ + return vertex.eachEdge( propsOrFn ); + })); +}; + +Graph.prototype.eachVertex = function( propsOrFn ){ + return iterate( this.vertices, propsOrFn ); +}; + +Graph.prototype.adjacent = function( vertex ){ + var self = this; + return iterate( vertex.edges, function( edge ){ + return self.vertices[ edge.target ]; + }); +}; + +Graph.prototype.eachAdjacent = function( vertex, fn ){ + var self = this; + return iterate( vertex.edges, function( edge ){ + var adj = self.vertices[ edge.target ]; + return fn.call( vertex, adj, edge ); + }); +}; + +Graph.prototype.print = function(){ + var self = this; + console.log( 'Graph has ' + Object.keys( self.vertices ).length + ' vertices' ); + self.eachVertex( function( vertex ){ + console.log( vertex.toString() ); + vertex.eachEdge( function( edge ){ + console.log( '\t ' + edge ); + }); + }); + return self; +}; + +Graph.prototype.toDOT = function(){ + var self = this, + strings = []; + strings.push( 'graph bler {' ); + self.edges( function( edge ){ + strings.push( '\t' + edge.from + ' -- ' + edge.to + ';' ); + }); + strings.push( '}' ); + return strings.join( '\n' ); +}; + +Graph.prototype.toNodesAndLinks = function(){ + var self = this, + indeces = {}; + return { + nodes : self.eachVertex( function( vertex, key, i ){ + indeces[ vertex.name ] = i; + return vertex.toJSON(); + }), + links : self.edges( function( edge ){ + var json = edge.toJSON(); + json.source = indeces[ edge.source ]; + json.target = indeces[ edge.target ]; + return json; + }) + }; +}; + +Graph.prototype.toVerticesAndEdges = function(){ + var self = this; + return { + vertices : self.eachVertex( function( vertex, key ){ + return vertex.toJSON(); + }), + edges : self.edges( function( edge ){ + return edge.toJSON(); + }) + }; +}; + +Graph.prototype.breadthFirstSearch = function( start, processFns ){ + return new BreadthFirstSearch( this ).search( start ); +}; + +Graph.prototype.breadthFirstSearchTree = function( start, processFns ){ + return new BreadthFirstSearch( this ).searchTree( start ); +}; + +Graph.prototype.depthFirstSearch = function( start, processFns ){ + return new DepthFirstSearch( this ).search( start ); +}; + +Graph.prototype.depthFirstSearchTree = function( start, processFns ){ + return new DepthFirstSearch( this ).searchTree( start ); +}; + + +//Graph.prototype.shortestPath = function( start, end ){ +//}; +// +//Graph.prototype.articulationVertices = function(){ +//}; +// +//Graph.prototype.isAcyclic = function(){ +//}; +// +//Graph.prototype.isBipartite = function(){ +//}; + +Graph.prototype.weakComponents = function(){ +//TODO: alternately, instead of returning graph-like objects: +// - could simply decorate the vertices (vertex.component = componentIndex), or clone the graph and do that + var self = this, + searchGraph = this, + undiscovered, + components = []; + + function getComponent( undiscoveredVertex ){ +//TODO: better interface on dfs (search v. searchTree) + var search = new DepthFirstSearch( searchGraph )._search( undiscoveredVertex ); + + // remove curr discovered from undiscovered + undiscovered = undiscovered.filter( function( name ){ + return !( name in search.discovered ); + }); + + return { + vertices : Object.keys( search.discovered ).map( function( vertexName ){ + return self.vertices[ vertexName ].toJSON(); + }), + edges : search.edges.map( function( edge ){ + // restore any reversed edges + var hasBeenReversed = self.vertices[ edge.target ].edges[ edge.source ] !== undefined; + if( self.directed && hasBeenReversed ){ + var swap = edge.source; + edge.source = edge.target; + edge.target = swap; + } + return edge; + }) + }; + } + + if( self.directed ){ + // if directed - convert to undirected for search + searchGraph = new Graph( false, self.toNodesAndLinks() ); + } + undiscovered = Object.keys( searchGraph.vertices ); + //console.debug( '(initial) undiscovered:', undiscovered ); + while( undiscovered.length ){ + var undiscoveredVertex = searchGraph.vertices[ undiscovered.shift() ]; + components.push( getComponent( undiscoveredVertex ) ); + //console.debug( 'undiscovered now:', undiscovered ); + } + + //console.debug( 'components:\n', JSON.stringify( components, null, ' ' ) ); + return components; +}; + +Graph.prototype.weakComponentGraph = function(){ + //note: although this can often look like the original graph - edges can be lost + var components = this.weakComponents(); + return new Graph( this.directed, { + vertices : components.reduce( function( reduction, curr ){ + return reduction.concat( curr.vertices ); + }, [] ), + edges : components.reduce( function( reduction, curr ){ + return reduction.concat( curr.edges ); + }, [] ) + }); +}; + +Graph.prototype.weakComponentGraphArray = function(){ + //note: although this can often look like the original graph - edges can be lost + return this.weakComponents().map( function( component ){ + return new Graph( this.directed, component ); + }); +}; + + +// ============================================================================ + + + +// ============================================================================ +function randGraph( directed, numVerts, numEdges ){ + //console.debug( 'randGraph', directed, numVerts, numEdges ); + var data = { nodes : [], links : [] }; + function randRange( range ){ + return Math.floor( Math.random() * range ); + } + for( var i=0; i<numVerts; i++ ){ + data.nodes.push({ name: i }); + } + for( i=0; i<numEdges; i++ ){ + data.links.push({ + source : randRange( numVerts ), + target : randRange( numVerts ) + }); + } + //console.debug( JSON.stringify( data, null, ' ' ) ); + return new Graph( directed, data ); +} + + +// ============================================================================ + return { + Vertex : Vertex, + Edge : Edge, + BreadthFirstSearch : BreadthFirstSearch, + DepthFirstSearch : DepthFirstSearch, + Graph : Graph, + randGraph : randGraph + }; +}); diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -529,6 +529,27 @@ return trans.fill_template( "history/display_structured.mako", items=items, history=history ) @web.expose + def structure( self, trans, id=None ): + """ + """ + if not id: + id = trans.security.encode_id( trans.history.id ) + + history_to_view = self.get_history( trans, id, check_ownership=False, check_accessible=True ) + history_data = self.mgrs.histories._get_history_data( trans, history_to_view ) + history_dictionary = history_data[ 'history' ] + hda_dictionaries = history_data[ 'contents' ] + + jobs = ( trans.sa_session.query( trans.app.model.Job ) + .filter( trans.app.model.Job.user == trans.user ) + .filter( trans.app.model.Job.history_id == trans.security.decode_id( id ) ) ).all() + + jobs = map( lambda j: self.encode_all_ids( trans, j.to_dict( 'element' ), True ), jobs ) + + return trans.fill_template( "history/structure.mako", historyId=history_dictionary[ 'id' ], + history=history_dictionary, hdas=hda_dictionaries, jobs=jobs ) + + @web.expose def view( self, trans, id=None, show_deleted=False, show_hidden=False, use_panels=True ): """ View a history. If a history is importable, then it is viewable by any user. diff -r 9f5491972e72ee38e011d49e891303e47bca29d9 -r eacfa5a1a3f2e9fc516bea159c16ddb36067a220 static/scripts/mvc/collection/collection-li.js --- a/static/scripts/mvc/collection/collection-li.js +++ b/static/scripts/mvc/collection/collection-li.js @@ -100,8 +100,6 @@ /** add the DCE class to the list item */ className : ListItemView.prototype.className + " dataset-collection-element", - /** jq fx speed for this view */ - fxSpeed : 'fast', /** set up */ initialize : function( attributes ){ This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org