PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/client/galaxy/scripts/mvc/history/history-model.js

https://bitbucket.org/galaxy/galaxy-central/
JavaScript | 647 lines | 419 code | 72 blank | 156 comment | 65 complexity | e99b3bf1363dbe9cbac4a406c1dad252 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "mvc/history/history-contents",
  3. "utils/utils",
  4. "mvc/base-mvc",
  5. "utils/localization"
  6. ], function( HISTORY_CONTENTS, UTILS, BASE_MVC, _l ){
  7. var logNamespace = 'history';
  8. //==============================================================================
  9. /** @class Model for a Galaxy history resource - both a record of user
  10. * tool use and a collection of the datasets those tools produced.
  11. * @name History
  12. * @augments Backbone.Model
  13. */
  14. var History = Backbone.Model
  15. .extend( BASE_MVC.LoggableMixin )
  16. .extend( BASE_MVC.mixin( BASE_MVC.SearchableModelMixin, /** @lends History.prototype */{
  17. _logNamespace : logNamespace,
  18. // values from api (may need more)
  19. defaults : {
  20. model_class : 'History',
  21. id : null,
  22. name : 'Unnamed History',
  23. state : 'new',
  24. deleted : false
  25. },
  26. // ........................................................................ urls
  27. urlRoot: Galaxy.root + 'api/histories',
  28. // ........................................................................ set up/tear down
  29. /** Set up the model
  30. * @param {Object} historyJSON model data for this History
  31. * @param {Object[]} contentsJSON array of model data for this History's contents (hdas or collections)
  32. * @param {Object} options any extra settings including logger
  33. */
  34. initialize : function( historyJSON, contentsJSON, options ){
  35. options = options || {};
  36. this.logger = options.logger || null;
  37. this.log( this + ".initialize:", historyJSON, contentsJSON, options );
  38. /** HistoryContents collection of the HDAs contained in this history. */
  39. this.log( 'creating history contents:', contentsJSON );
  40. this.contents = new HISTORY_CONTENTS.HistoryContents( contentsJSON || [], { historyId: this.get( 'id' )});
  41. //// if we've got hdas passed in the constructor, load them
  42. //if( contentsJSON && _.isArray( contentsJSON ) ){
  43. // this.log( 'resetting history contents:', contentsJSON );
  44. // this.contents.reset( contentsJSON );
  45. //}
  46. this._setUpListeners();
  47. /** cached timeout id for the dataset updater */
  48. this.updateTimeoutId = null;
  49. // set up update timeout if needed
  50. //this.checkForUpdates();
  51. },
  52. /** set up any event listeners for this history including those to the contained HDAs
  53. * events: error:contents if an error occurred with the contents collection
  54. */
  55. _setUpListeners : function(){
  56. this.on( 'error', function( model, xhr, options, msg, details ){
  57. this.errorHandler( model, xhr, options, msg, details );
  58. });
  59. // hda collection listening
  60. if( this.contents ){
  61. this.listenTo( this.contents, 'error', function(){
  62. this.trigger.apply( this, [ 'error:contents' ].concat( jQuery.makeArray( arguments ) ) );
  63. });
  64. }
  65. // if the model's id changes ('current' or null -> an actual id), update the contents history_id
  66. this.on( 'change:id', function( model, newId ){
  67. if( this.contents ){
  68. this.contents.historyId = newId;
  69. }
  70. }, this );
  71. },
  72. //TODO: see base-mvc
  73. //onFree : function(){
  74. // if( this.contents ){
  75. // this.contents.free();
  76. // }
  77. //},
  78. /** event listener for errors. Generally errors are handled outside this model */
  79. errorHandler : function( model, xhr, options, msg, details ){
  80. // clear update timeout on model err
  81. this.clearUpdateTimeout();
  82. },
  83. /** convert size in bytes to a more human readable version */
  84. nice_size : function(){
  85. return UTILS.bytesToString( this.get( 'size' ), true, 2 );
  86. },
  87. /** override to add nice_size */
  88. toJSON : function(){
  89. return _.extend( Backbone.Model.prototype.toJSON.call( this ), {
  90. nice_size : this.nice_size()
  91. });
  92. },
  93. /** override to allow getting nice_size */
  94. get : function( key ){
  95. if( key === 'nice_size' ){
  96. return this.nice_size();
  97. }
  98. return Backbone.Model.prototype.get.apply( this, arguments );
  99. },
  100. // ........................................................................ common queries
  101. /** T/F is this history owned by the current user (Galaxy.user)
  102. * Note: that this will return false for an anon user even if the history is theirs.
  103. */
  104. ownedByCurrUser : function(){
  105. // no currUser
  106. if( !Galaxy || !Galaxy.user ){
  107. return false;
  108. }
  109. // user is anon or history isn't owned
  110. if( Galaxy.user.isAnonymous() || Galaxy.user.id !== this.get( 'user_id' ) ){
  111. return false;
  112. }
  113. return true;
  114. },
  115. /** */
  116. contentsCount : function(){
  117. return _.reduce( _.values( this.get( 'state_details' ) ), function( memo, num ){ return memo + num; }, 0 );
  118. },
  119. // ........................................................................ search
  120. /** What model fields to search with */
  121. searchAttributes : [
  122. 'name', 'annotation', 'tags'
  123. ],
  124. /** Adding title and singular tag */
  125. searchAliases : {
  126. title : 'name',
  127. tag : 'tags'
  128. },
  129. // ........................................................................ updates
  130. /** does the contents collection indicate they're still running and need to be updated later?
  131. * delay + update if needed
  132. * @param {Function} onReadyCallback function to run when all contents are in the ready state
  133. * events: ready
  134. */
  135. checkForUpdates : function( onReadyCallback ){
  136. //this.info( 'checkForUpdates' )
  137. // get overall History state from collection, run updater if History has running/queued contents
  138. // boiling it down on the client to running/not
  139. if( this.contents.running().length ){
  140. this.setUpdateTimeout();
  141. } else {
  142. this.trigger( 'ready' );
  143. if( _.isFunction( onReadyCallback ) ){
  144. onReadyCallback.call( this );
  145. }
  146. }
  147. return this;
  148. },
  149. /** create a timeout (after UPDATE_DELAY or delay ms) to refetch the contents. Clear any prev. timeout */
  150. setUpdateTimeout : function( delay ){
  151. delay = delay || History.UPDATE_DELAY;
  152. var history = this;
  153. // prevent buildup of updater timeouts by clearing previous if any, then set new and cache id
  154. this.clearUpdateTimeout();
  155. this.updateTimeoutId = setTimeout( function(){
  156. history.refresh();
  157. }, delay );
  158. return this.updateTimeoutId;
  159. },
  160. /** clear the timeout and the cached timeout id */
  161. clearUpdateTimeout : function(){
  162. if( this.updateTimeoutId ){
  163. clearTimeout( this.updateTimeoutId );
  164. this.updateTimeoutId = null;
  165. }
  166. },
  167. /* update the contents, getting full detailed model data for any whose id is in detailIds
  168. * set up to run this again in some interval of time
  169. * @param {String[]} detailIds list of content ids to get detailed model data for
  170. * @param {Object} options std. backbone fetch options map
  171. */
  172. refresh : function( detailIds, options ){
  173. //this.info( 'refresh:', detailIds, this.contents );
  174. detailIds = detailIds || [];
  175. options = options || {};
  176. var history = this;
  177. // add detailIds to options as CSV string
  178. options.data = options.data || {};
  179. if( detailIds.length ){
  180. options.data.details = detailIds.join( ',' );
  181. }
  182. var xhr = this.contents.fetch( options );
  183. xhr.done( function( models ){
  184. history.checkForUpdates( function(){
  185. // fetch the history inside onReadyCallback in order to recalc history size
  186. this.fetch();
  187. });
  188. });
  189. return xhr;
  190. },
  191. // ........................................................................ ajax
  192. /** save this history, _Mark_ing it as deleted (just a flag) */
  193. _delete : function( options ){
  194. if( this.get( 'deleted' ) ){ return jQuery.when(); }
  195. return this.save( { deleted: true }, options );
  196. },
  197. /** purge this history, _Mark_ing it as purged and removing all dataset data from the server */
  198. purge : function( options ){
  199. if( this.get( 'purged' ) ){ return jQuery.when(); }
  200. return this.save( { deleted: true, purged: true }, options );
  201. },
  202. /** save this history, _Mark_ing it as undeleted */
  203. undelete : function( options ){
  204. if( !this.get( 'deleted' ) ){ return jQuery.when(); }
  205. return this.save( { deleted: false }, options );
  206. },
  207. /** Make a copy of this history on the server
  208. * @param {Boolean} current if true, set the copy as the new current history (default: true)
  209. * @param {String} name name of new history (default: none - server sets to: Copy of <current name>)
  210. * @fires copied passed this history and the response JSON from the copy
  211. * @returns {xhr}
  212. */
  213. copy : function( current, name, allDatasets ){
  214. current = ( current !== undefined )?( current ):( true );
  215. if( !this.id ){
  216. throw new Error( 'You must set the history ID before copying it.' );
  217. }
  218. var postData = { history_id : this.id };
  219. if( current ){
  220. postData.current = true;
  221. }
  222. if( name ){
  223. postData.name = name;
  224. }
  225. if( !allDatasets ){
  226. postData.all_datasets = false;
  227. }
  228. var history = this,
  229. copy = jQuery.post( this.urlRoot, postData );
  230. // if current - queue to setAsCurrent before firing 'copied'
  231. if( current ){
  232. return copy.then( function( response ){
  233. var newHistory = new History( response );
  234. return newHistory.setAsCurrent()
  235. .done( function(){
  236. history.trigger( 'copied', history, response );
  237. });
  238. });
  239. }
  240. return copy.done( function( response ){
  241. history.trigger( 'copied', history, response );
  242. });
  243. },
  244. setAsCurrent : function(){
  245. var history = this,
  246. xhr = jQuery.getJSON( Galaxy.root + 'history/set_as_current?id=' + this.id );
  247. xhr.done( function(){
  248. history.trigger( 'set-as-current', history );
  249. });
  250. return xhr;
  251. },
  252. // ........................................................................ misc
  253. toString : function(){
  254. return 'History(' + this.get( 'id' ) + ',' + this.get( 'name' ) + ')';
  255. }
  256. }));
  257. //------------------------------------------------------------------------------ CLASS VARS
  258. /** When the history has running hdas,
  259. * this is the amount of time between update checks from the server
  260. */
  261. History.UPDATE_DELAY = 4000;
  262. /** Get data for a history then its hdas using a sequential ajax call, return a deferred to receive both */
  263. History.getHistoryData = function getHistoryData( historyId, options ){
  264. options = options || {};
  265. var detailIdsFn = options.detailIdsFn || [];
  266. var hdcaDetailIds = options.hdcaDetailIds || [];
  267. //console.debug( 'getHistoryData:', historyId, options );
  268. var df = jQuery.Deferred(),
  269. historyJSON = null;
  270. function getHistory( id ){
  271. // get the history data
  272. if( historyId === 'current' ){
  273. return jQuery.getJSON( Galaxy.root + 'history/current_history_json' );
  274. }
  275. return jQuery.ajax( Galaxy.root + 'api/histories/' + historyId );
  276. }
  277. function isEmpty( historyData ){
  278. // get the number of hdas accrd. to the history
  279. return historyData && historyData.empty;
  280. }
  281. function getContents( historyData ){
  282. // get the hda data
  283. // if no hdas accrd. to history: return empty immed.
  284. if( isEmpty( historyData ) ){ return []; }
  285. // if there are hdas accrd. to history: get those as well
  286. if( _.isFunction( detailIdsFn ) ){
  287. detailIdsFn = detailIdsFn( historyData );
  288. }
  289. if( _.isFunction( hdcaDetailIds ) ){
  290. hdcaDetailIds = hdcaDetailIds( historyData );
  291. }
  292. var data = {};
  293. if( detailIdsFn.length ) {
  294. data.dataset_details = detailIdsFn.join( ',' );
  295. }
  296. if( hdcaDetailIds.length ) {
  297. // for symmetry, not actually used by backend of consumed
  298. // by frontend.
  299. data.dataset_collection_details = hdcaDetailIds.join( ',' );
  300. }
  301. return jQuery.ajax( Galaxy.root + 'api/histories/' + historyData.id + '/contents', { data: data });
  302. }
  303. // getting these concurrently is 400% slower (sqlite, local, vanilla) - so:
  304. // chain the api calls - getting history first then contents
  305. var historyFn = options.historyFn || getHistory,
  306. contentsFn = options.contentsFn || getContents;
  307. // chain ajax calls: get history first, then hdas
  308. var historyXHR = historyFn( historyId );
  309. historyXHR.done( function( json ){
  310. // set outer scope var here for use below
  311. historyJSON = json;
  312. df.notify({ status: 'history data retrieved', historyJSON: historyJSON });
  313. });
  314. historyXHR.fail( function( xhr, status, message ){
  315. // call reject on the outer deferred to allow its fail callback to run
  316. df.reject( xhr, 'loading the history' );
  317. });
  318. var contentsXHR = historyXHR.then( contentsFn );
  319. contentsXHR.then( function( contentsJSON ){
  320. df.notify({ status: 'contents data retrieved', historyJSON: historyJSON, contentsJSON: contentsJSON });
  321. // we've got both: resolve the outer scope deferred
  322. df.resolve( historyJSON, contentsJSON );
  323. });
  324. contentsXHR.fail( function( xhr, status, message ){
  325. // call reject on the outer deferred to allow its fail callback to run
  326. df.reject( xhr, 'loading the contents', { history: historyJSON } );
  327. });
  328. return df;
  329. };
  330. //==============================================================================
  331. var ControlledFetchMixin = {
  332. /** Override to convert certain options keys into API index parameters */
  333. fetch : function( options ){
  334. options = options || {};
  335. options.data = options.data || this._buildFetchData( options );
  336. // use repeated params for arrays, e.g. q=1&qv=1&q=2&qv=2
  337. options.traditional = true;
  338. return Backbone.Collection.prototype.fetch.call( this, options );
  339. },
  340. /** These attribute keys are valid params to fetch/API-index */
  341. _fetchOptions : [
  342. /** model dependent string to control the order of models returned */
  343. 'order',
  344. /** limit the number of models returned from a fetch */
  345. 'limit',
  346. /** skip this number of models when fetching */
  347. 'offset',
  348. /** what series of attributes to return (model dependent) */
  349. 'view',
  350. /** individual keys to return for the models (see api/histories.index) */
  351. 'keys'
  352. ],
  353. /** Build the data dictionary to send to fetch's XHR as data */
  354. _buildFetchData : function( options ){
  355. var data = {},
  356. fetchDefaults = this._fetchDefaults();
  357. options = _.defaults( options || {}, fetchDefaults );
  358. data = _.pick( options, this._fetchOptions );
  359. var filters = _.has( options, 'filters' )? options.filters : ( fetchDefaults.filters || {} );
  360. if( !_.isEmpty( filters ) ){
  361. _.extend( data, this._buildFetchFilters( filters ) );
  362. }
  363. return data;
  364. },
  365. /** Override to have defaults for fetch options and filters */
  366. _fetchDefaults : function(){
  367. // to be overridden
  368. return {};
  369. },
  370. /** Convert dictionary filters to qqv style arrays */
  371. _buildFetchFilters : function( filters ){
  372. var filterMap = {
  373. q : [],
  374. qv : []
  375. };
  376. _.each( filters, function( v, k ){
  377. if( v === true ){ v = 'True'; }
  378. if( v === false ){ v = 'False'; }
  379. filterMap.q.push( k );
  380. filterMap.qv.push( v );
  381. });
  382. return filterMap;
  383. },
  384. };
  385. //==============================================================================
  386. /** @class A collection of histories (per user).
  387. * (stub) currently unused.
  388. */
  389. var HistoryCollection = Backbone.Collection
  390. .extend( BASE_MVC.LoggableMixin )
  391. .extend( ControlledFetchMixin )
  392. .extend(/** @lends HistoryCollection.prototype */{
  393. _logNamespace : logNamespace,
  394. model : History,
  395. /** @type {String} the default sortOrders key for sorting */
  396. DEFAULT_ORDER : 'update_time',
  397. /** @type {Object} map of collection sorting orders generally containing a getter to return the attribute
  398. * sorted by and asc T/F if it is an ascending sort.
  399. */
  400. sortOrders : {
  401. 'update_time' : {
  402. getter : function( h ){ return new Date( h.get( 'update_time' ) ); },
  403. asc : false
  404. },
  405. 'update_time-asc' : {
  406. getter : function( h ){ return new Date( h.get( 'update_time' ) ); },
  407. asc : true
  408. },
  409. 'name' : {
  410. getter : function( h ){ return h.get( 'name' ); },
  411. asc : true
  412. },
  413. 'name-dsc' : {
  414. getter : function( h ){ return h.get( 'name' ); },
  415. asc : false
  416. },
  417. 'size' : {
  418. getter : function( h ){ return h.get( 'size' ); },
  419. asc : false
  420. },
  421. 'size-asc' : {
  422. getter : function( h ){ return h.get( 'size' ); },
  423. asc : true
  424. }
  425. },
  426. initialize : function( models, options ){
  427. options = options || {};
  428. this.log( 'HistoryCollection.initialize', arguments );
  429. // instance vars
  430. /** @type {boolean} should deleted histories be included */
  431. this.includeDeleted = options.includeDeleted || false;
  432. // set the sort order
  433. this.setOrder( options.order || this.DEFAULT_ORDER );
  434. /** @type {String} encoded id of the history that's current */
  435. this.currentHistoryId = options.currentHistoryId;
  436. /** @type {boolean} have all histories been fetched and in the collection? */
  437. this.allFetched = options.allFetched || false;
  438. // this.on( 'all', function(){
  439. // console.info( 'event:', arguments );
  440. // });
  441. this.setUpListeners();
  442. },
  443. urlRoot : Galaxy.root + 'api/histories',
  444. url : function(){ return this.urlRoot; },
  445. /** returns map of default filters and settings for fetching from the API */
  446. _fetchDefaults : function(){
  447. // to be overridden
  448. var defaults = {
  449. order : this.order,
  450. view : 'detailed'
  451. };
  452. if( !this.includeDeleted ){
  453. defaults.filters = {
  454. deleted : false,
  455. purged : false,
  456. };
  457. }
  458. return defaults;
  459. },
  460. /** set up reflexive event handlers */
  461. setUpListeners : function setUpListeners(){
  462. this.on({
  463. // when a history is deleted, remove it from the collection (if optionally set to do so)
  464. 'change:deleted' : function( history ){
  465. // TODO: this becomes complicated when more filters are used
  466. this.debug( 'change:deleted', this.includeDeleted, history.get( 'deleted' ) );
  467. if( !this.includeDeleted && history.get( 'deleted' ) ){
  468. this.remove( history );
  469. }
  470. },
  471. // listen for a history copy, setting it to current
  472. 'copied' : function( original, newData ){
  473. this.setCurrent( new History( newData, [] ) );
  474. },
  475. // when a history is made current, track the id in the collection
  476. 'set-as-current' : function( history ){
  477. var oldCurrentId = this.currentHistoryId;
  478. this.trigger( 'no-longer-current', oldCurrentId );
  479. this.currentHistoryId = history.id;
  480. }
  481. }, this );
  482. },
  483. /** override to allow passing options.order and setting the sort order to one of sortOrders */
  484. sort : function( options ){
  485. options = options || {};
  486. this.setOrder( options.order );
  487. return Backbone.Collection.prototype.sort.call( this, options );
  488. },
  489. /** build the comparator used to sort this collection using the sortOrder map and the given order key
  490. * @event 'changed-order' passed the new order and the collection
  491. */
  492. setOrder : function( order ){
  493. var collection = this,
  494. sortOrder = this.sortOrders[ order ];
  495. if( _.isUndefined( sortOrder ) ){ return; }
  496. collection.order = order;
  497. collection.comparator = function comparator( a, b ){
  498. var currentHistoryId = collection.currentHistoryId;
  499. // current always first
  500. if( a.id === currentHistoryId ){ return -1; }
  501. if( b.id === currentHistoryId ){ return 1; }
  502. // then compare by an attribute
  503. a = sortOrder.getter( a );
  504. b = sortOrder.getter( b );
  505. return sortOrder.asc?
  506. ( ( a === b )?( 0 ):( a > b ? 1 : -1 ) ):
  507. ( ( a === b )?( 0 ):( a > b ? -1 : 1 ) );
  508. };
  509. collection.trigger( 'changed-order', collection.order, collection );
  510. return collection;
  511. },
  512. /** override to provide order and offsets based on instance vars, set limit if passed,
  513. * and set allFetched/fire 'all-fetched' when xhr returns
  514. */
  515. fetch : function( options ){
  516. options = options || {};
  517. if( this.allFetched ){ return jQuery.when({}); }
  518. var collection = this,
  519. fetchOptions = _.defaults( options, {
  520. remove : false,
  521. offset : collection.length >= 1? ( collection.length - 1 ) : 0,
  522. order : collection.order
  523. }),
  524. limit = options.limit;
  525. if( !_.isUndefined( limit ) ){
  526. fetchOptions.limit = limit;
  527. }
  528. return ControlledFetchMixin.fetch.call( this, fetchOptions )
  529. .done( function _postFetchMore( fetchData ){
  530. var numFetched = _.isArray( fetchData )? fetchData.length : 0;
  531. // anything less than a full page means we got all there is to get
  532. if( !limit || numFetched < limit ){
  533. collection.allFetched = true;
  534. collection.trigger( 'all-fetched', collection );
  535. }
  536. }
  537. );
  538. },
  539. /** create a new history and by default set it to be the current history */
  540. create : function create( data, hdas, historyOptions, xhrOptions ){
  541. //TODO: .create is actually a collection function that's overridden here
  542. var collection = this,
  543. xhr = jQuery.getJSON( Galaxy.root + 'history/create_new_current' );
  544. return xhr.done( function( newData ){
  545. collection.setCurrent( new History( newData, [], historyOptions || {} ) );
  546. });
  547. },
  548. /** set the current history to the given history, placing it first in the collection.
  549. * Pass standard bbone options for use in unshift.
  550. * @triggers new-current passed history and this collection
  551. */
  552. setCurrent : function( history, options ){
  553. options = options || {};
  554. // new histories go in the front
  555. this.unshift( history, options );
  556. this.currentHistoryId = history.get( 'id' );
  557. if( !options.silent ){
  558. this.trigger( 'new-current', history, this );
  559. }
  560. return this;
  561. },
  562. /** override to reset allFetched flag to false */
  563. reset : function( models, options ){
  564. this.allFetched = false;
  565. return Backbone.Collection.prototype.reset.call( this, models, options );
  566. },
  567. toString: function toString(){
  568. return 'HistoryCollection(' + this.length + ')';
  569. }
  570. });
  571. //==============================================================================
  572. return {
  573. History : History,
  574. HistoryCollection : HistoryCollection
  575. };});