PageRenderTime 66ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/tools.js

https://bitbucket.org/nicste/ballaxy
JavaScript | 665 lines | 453 code | 77 blank | 135 comment | 45 complexity | bc10a1f82ffdf8bba8b641d16d086897 MD5 | raw file
  1. /**
  2. * Model, view, and controller objects for Galaxy tools and tool panel.
  3. *
  4. * Models have no references to views, instead using events to indicate state
  5. * changes; this is advantageous because multiple views can use the same object
  6. * and models can be used without views.
  7. */
  8. /**
  9. * Simple base model for any visible element. Includes useful attributes and ability
  10. * to set and track visibility.
  11. */
  12. var BaseModel = Backbone.RelationalModel.extend({
  13. defaults: {
  14. name: null,
  15. hidden: false
  16. },
  17. show: function() {
  18. this.set("hidden", false);
  19. },
  20. hide: function() {
  21. this.set("hidden", true);
  22. },
  23. is_visible: function() {
  24. return !this.attributes.hidden;
  25. }
  26. });
  27. /**
  28. * A Galaxy tool.
  29. */
  30. var Tool = BaseModel.extend({
  31. // Default attributes.
  32. defaults: {
  33. description: null,
  34. target: null,
  35. inputs: []
  36. },
  37. relations: [
  38. {
  39. type: Backbone.HasMany,
  40. key: 'inputs',
  41. relatedModel: 'ToolInput',
  42. reverseRelation: {
  43. key: 'tool',
  44. includeInJSON: false
  45. }
  46. }
  47. ],
  48. urlRoot: galaxy_paths.get('tool_url'),
  49. /**
  50. * Returns object copy, optionally including only inputs that can be sampled.
  51. */
  52. copy: function(only_samplable_inputs) {
  53. var copy = new Tool(this.toJSON());
  54. // Return only samplable inputs if flag is set.
  55. if (only_samplable_inputs) {
  56. var valid_inputs = new Backbone.Collection();
  57. copy.get('inputs').each(function(input) {
  58. if (input.get_samples()) {
  59. valid_inputs.push(input);
  60. }
  61. });
  62. copy.set('inputs', valid_inputs);
  63. }
  64. return copy;
  65. },
  66. apply_search_results: function(results) {
  67. ( _.indexOf(results, this.attributes.id) !== -1 ? this.show() : this.hide() );
  68. return this.is_visible();
  69. },
  70. /**
  71. * Set a tool input's value.
  72. */
  73. set_input_value: function(name, value) {
  74. this.get('inputs').find(function(input) {
  75. return input.get('name') === name;
  76. }).set('value', value);
  77. },
  78. /**
  79. * Set many input values at once.
  80. */
  81. set_input_values: function(inputs_dict) {
  82. var self = this;
  83. _.each(_.keys(inputs_dict), function(input_name) {
  84. self.set_input_value(input_name, inputs_dict[input_name]);
  85. });
  86. },
  87. /**
  88. * Run tool; returns a Deferred that resolves to the tool's output(s).
  89. */
  90. run: function() {
  91. return this._run();
  92. },
  93. /**
  94. * Rerun tool using regions and a target dataset.
  95. */
  96. rerun: function(target_dataset, regions) {
  97. return this._run({
  98. action: 'rerun',
  99. target_dataset_id: target_dataset.id,
  100. regions: regions
  101. });
  102. },
  103. /**
  104. * Returns input dict for tool's inputs.
  105. */
  106. get_inputs_dict: function() {
  107. var input_dict = {};
  108. this.get('inputs').each(function(input) {
  109. input_dict[input.get('name')] = input.get('value');
  110. });
  111. return input_dict;
  112. },
  113. /**
  114. * Run tool; returns a Deferred that resolves to the tool's output(s).
  115. * NOTE: this method is a helper method and should not be called directly.
  116. */
  117. _run: function(additional_params) {
  118. // Create payload.
  119. var payload = _.extend({
  120. tool_id: this.id,
  121. inputs: this.get_inputs_dict()
  122. }, additional_params);
  123. // Because job may require indexing datasets, use server-side
  124. // deferred to ensure that job is run. Also use deferred that
  125. // resolves to outputs from tool.
  126. var run_deferred = $.Deferred(),
  127. ss_deferred = new ServerStateDeferred({
  128. ajax_settings: {
  129. url: this.urlRoot,
  130. data: JSON.stringify(payload),
  131. dataType: "json",
  132. contentType: 'application/json',
  133. type: "POST"
  134. },
  135. interval: 2000,
  136. success_fn: function(response) {
  137. return response !== "pending";
  138. }
  139. });
  140. // Run job and resolve run_deferred to tool outputs.
  141. $.when(ss_deferred.go()).then(function(result) {
  142. run_deferred.resolve(new DatasetCollection().reset(result));
  143. });
  144. return run_deferred;
  145. }
  146. });
  147. /**
  148. * A tool input.
  149. */
  150. var ToolInput = Backbone.RelationalModel.extend({
  151. defaults: {
  152. name: null,
  153. label: null,
  154. type: null,
  155. value: null,
  156. num_samples: 5
  157. },
  158. initialize: function() {
  159. this.attributes.html = unescape(this.attributes.html);
  160. },
  161. copy: function() {
  162. return new ToolInput(this.toJSON());
  163. },
  164. /**
  165. * Returns samples from a tool input.
  166. */
  167. get_samples: function() {
  168. var type = this.get('type'),
  169. samples = null;
  170. if (type === 'number') {
  171. samples = d3.scale.linear()
  172. .domain([this.get('min'), this.get('max')])
  173. .ticks(this.get('num_samples'));
  174. }
  175. else if (type === 'select') {
  176. samples = _.map(this.get('options'), function(option) {
  177. return option[0];
  178. });
  179. }
  180. return samples;
  181. }
  182. });
  183. /**
  184. * Wrap collection of tools for fast access/manipulation.
  185. */
  186. var ToolCollection = Backbone.Collection.extend({
  187. model: Tool
  188. });
  189. /**
  190. * Label or section header in tool panel.
  191. */
  192. var ToolPanelLabel = BaseModel.extend({});
  193. /**
  194. * Section of tool panel with elements (labels and tools).
  195. */
  196. var ToolPanelSection = BaseModel.extend({
  197. defaults: {
  198. elems: [],
  199. open: false
  200. },
  201. clear_search_results: function() {
  202. _.each(this.attributes.elems, function(elt) {
  203. elt.show();
  204. });
  205. this.show();
  206. this.set("open", false);
  207. },
  208. apply_search_results: function(results) {
  209. var all_hidden = true,
  210. cur_label;
  211. _.each(this.attributes.elems, function(elt) {
  212. if (elt instanceof ToolPanelLabel) {
  213. cur_label = elt;
  214. cur_label.hide();
  215. }
  216. else if (elt instanceof Tool) {
  217. if (elt.apply_search_results(results)) {
  218. all_hidden = false;
  219. if (cur_label) {
  220. cur_label.show();
  221. }
  222. }
  223. }
  224. });
  225. if (all_hidden) {
  226. this.hide();
  227. }
  228. else {
  229. this.show();
  230. this.set("open", true);
  231. }
  232. }
  233. });
  234. /**
  235. * Tool search that updates results when query is changed. Result value of null
  236. * indicates that query was not run; if not null, results are from search using
  237. * query.
  238. */
  239. var ToolSearch = BaseModel.extend({
  240. defaults: {
  241. spinner_url: "",
  242. search_url: "",
  243. visible: true,
  244. query: "",
  245. results: null
  246. },
  247. initialize: function() {
  248. this.on("change:query", this.do_search);
  249. },
  250. /**
  251. * Do the search and update the results.
  252. */
  253. do_search: function() {
  254. var query = this.attributes.query;
  255. // If query is too short, do not search.
  256. if (query.length < 3) {
  257. this.set("results", null);
  258. return;
  259. }
  260. // Do search via AJAX.
  261. var q = query + '*';
  262. // Stop previous ajax-request
  263. if (this.timer) {
  264. clearTimeout(this.timer);
  265. }
  266. // Start a new ajax-request in X ms
  267. $("#search-spinner").show();
  268. var self = this;
  269. this.timer = setTimeout(function () {
  270. $.get(self.attributes.search_url, { query: q }, function (data) {
  271. self.set("results", data);
  272. $("#search-spinner").hide();
  273. }, "json" );
  274. }, 200 );
  275. }
  276. });
  277. /**
  278. * A collection of ToolPanelSections, Tools, and ToolPanelLabels. Collection
  279. * applies search results as they become available.
  280. */
  281. var ToolPanel = Backbone.Collection.extend({
  282. // TODO: need to generate this using url_for
  283. url: "/tools",
  284. tools: new ToolCollection(),
  285. parse: function(response) {
  286. // Recursive function to parse tool panel elements.
  287. var parse_elt = function(elt_dict) {
  288. var type = elt_dict.type;
  289. if (type === 'tool') {
  290. return new Tool(elt_dict);
  291. }
  292. else if (type === 'section') {
  293. // Parse elements.
  294. var elems = _.map(elt_dict.elems, parse_elt);
  295. elt_dict.elems = elems;
  296. return new ToolPanelSection(elt_dict);
  297. }
  298. else if (type === 'label') {
  299. return new ToolPanelLabel(elt_dict);
  300. }
  301. };
  302. return _.map(response, parse_elt);
  303. },
  304. initialize: function(options) {
  305. this.tool_search = options.tool_search;
  306. this.tool_search.on("change:results", this.apply_search_results, this);
  307. this.on("reset", this.populate_tools, this);
  308. },
  309. /**
  310. * Populate tool collection from panel elements.
  311. */
  312. populate_tools: function() {
  313. var self = this;
  314. self.tools = new ToolCollection();
  315. this.each(function(panel_elt) {
  316. if (panel_elt instanceof ToolPanelSection) {
  317. _.each(panel_elt.attributes.elems, function (section_elt) {
  318. if (section_elt instanceof Tool) {
  319. self.tools.push(section_elt);
  320. }
  321. });
  322. }
  323. else if (panel_elt instanceof Tool) {
  324. self.tools.push(panel_elt);
  325. }
  326. });
  327. },
  328. clear_search_results: function() {
  329. this.each(function(panel_elt) {
  330. if (panel_elt instanceof ToolPanelSection) {
  331. panel_elt.clear_search_results();
  332. }
  333. else {
  334. // Label or tool, so just show.
  335. panel_elt.show();
  336. }
  337. });
  338. },
  339. apply_search_results: function() {
  340. var results = this.tool_search.attributes.results;
  341. if (results === null) {
  342. this.clear_search_results();
  343. return;
  344. }
  345. var cur_label = null;
  346. this.each(function(panel_elt) {
  347. if (panel_elt instanceof ToolPanelLabel) {
  348. cur_label = panel_elt;
  349. cur_label.hide();
  350. }
  351. else if (panel_elt instanceof Tool) {
  352. if (panel_elt.apply_search_results(results)) {
  353. if (cur_label) {
  354. cur_label.show();
  355. }
  356. }
  357. }
  358. else {
  359. // Starting new section, so clear current label.
  360. cur_label = null;
  361. panel_elt.apply_search_results(results);
  362. }
  363. });
  364. }
  365. });
  366. /**
  367. * View classes for Galaxy tools and tool panel.
  368. *
  369. * Views use precompiled Handlebars templates for rendering. Views update as needed
  370. * based on (a) model/collection events and (b) user interactions; in this sense,
  371. * they are controllers are well and the HTML is the real view in the MVC architecture.
  372. */
  373. /**
  374. * Base view that handles visibility based on model's hidden attribute.
  375. */
  376. var BaseView = Backbone.View.extend({
  377. initialize: function() {
  378. this.model.on("change:hidden", this.update_visible, this);
  379. this.update_visible();
  380. },
  381. update_visible: function() {
  382. ( this.model.attributes.hidden ? this.$el.hide() : this.$el.show() );
  383. }
  384. });
  385. /**
  386. * Link to a tool.
  387. */
  388. var ToolLinkView = BaseView.extend({
  389. tagName: 'div',
  390. template: Handlebars.templates.tool_link,
  391. render: function() {
  392. this.$el.append( this.template(this.model.toJSON()) );
  393. return this;
  394. }
  395. });
  396. /**
  397. * Panel label/section header.
  398. */
  399. var ToolPanelLabelView = BaseView.extend({
  400. tagName: 'div',
  401. className: 'toolPanelLabel',
  402. render: function() {
  403. this.$el.append( $("<span/>").text(this.model.attributes.name) );
  404. return this;
  405. }
  406. });
  407. /**
  408. * Panel section.
  409. */
  410. var ToolPanelSectionView = BaseView.extend({
  411. tagName: 'div',
  412. className: 'toolSectionWrapper',
  413. template: Handlebars.templates.panel_section,
  414. initialize: function() {
  415. BaseView.prototype.initialize.call(this);
  416. this.model.on("change:open", this.update_open, this);
  417. },
  418. render: function() {
  419. // Build using template.
  420. this.$el.append( this.template(this.model.toJSON()) );
  421. // Add tools to section.
  422. var section_body = this.$el.find(".toolSectionBody");
  423. _.each(this.model.attributes.elems, function(elt) {
  424. if (elt instanceof Tool) {
  425. var tool_view = new ToolLinkView({model: elt, className: "toolTitle"});
  426. tool_view.render();
  427. section_body.append(tool_view.$el);
  428. }
  429. else if (elt instanceof ToolPanelLabel) {
  430. var label_view = new ToolPanelLabelView({model: elt});
  431. label_view.render();
  432. section_body.append(label_view.$el);
  433. }
  434. else {
  435. // TODO: handle nested section bodies?
  436. }
  437. });
  438. return this;
  439. },
  440. events: {
  441. 'click .toolSectionTitle > a': 'toggle'
  442. },
  443. /**
  444. * Toggle visibility of tool section.
  445. */
  446. toggle: function() {
  447. this.model.set("open", !this.model.attributes.open);
  448. },
  449. /**
  450. * Update whether section is open or close.
  451. */
  452. update_open: function() {
  453. (this.model.attributes.open ?
  454. this.$el.children(".toolSectionBody").slideDown("fast") :
  455. this.$el.children(".toolSectionBody").slideUp("fast")
  456. );
  457. }
  458. });
  459. var ToolSearchView = Backbone.View.extend({
  460. tagName: 'div',
  461. id: 'tool-search',
  462. className: 'bar',
  463. template: Handlebars.templates.tool_search,
  464. events: {
  465. 'click': 'focus_and_select',
  466. 'keyup :input': 'query_changed'
  467. },
  468. render: function() {
  469. this.$el.append( this.template(this.model.toJSON()) );
  470. if (!this.model.is_visible()) {
  471. this.$el.hide();
  472. }
  473. return this;
  474. },
  475. focus_and_select: function() {
  476. this.$el.find(":input").focus().select();
  477. },
  478. query_changed: function() {
  479. this.model.set("query", this.$el.find(":input").val());
  480. }
  481. });
  482. /**
  483. * Tool panel view. Events triggered include:
  484. * tool_link_click(click event, tool_model)
  485. */
  486. var ToolPanelView = Backbone.View.extend({
  487. tagName: 'div',
  488. className: 'toolMenu',
  489. /**
  490. * Waits for collection to load and then renders.
  491. */
  492. initialize: function() {
  493. this.collection.tool_search.on("change:results", this.handle_search_results, this);
  494. },
  495. render: function() {
  496. var self = this;
  497. // Render search.
  498. var search_view = new ToolSearchView( {model: this.collection.tool_search} );
  499. search_view.render();
  500. self.$el.append(search_view.$el);
  501. // Render panel.
  502. this.collection.each(function(panel_elt) {
  503. if (panel_elt instanceof ToolPanelSection) {
  504. var section_title_view = new ToolPanelSectionView({model: panel_elt});
  505. section_title_view.render();
  506. self.$el.append(section_title_view.$el);
  507. }
  508. else if (panel_elt instanceof Tool) {
  509. var tool_view = new ToolLinkView({model: panel_elt, className: "toolTitleNoSection"});
  510. tool_view.render();
  511. self.$el.append(tool_view.$el);
  512. }
  513. else if (panel_elt instanceof ToolPanelLabel) {
  514. var label_view = new ToolPanelLabelView({model: panel_elt});
  515. label_view.render();
  516. self.$el.append(label_view.$el);
  517. }
  518. });
  519. // Setup tool link click eventing.
  520. self.$el.find("a.tool-link").click(function(e) {
  521. // Tool id is always the first class.
  522. var
  523. tool_id = $(this).attr('class').split(/\s+/)[0],
  524. tool = self.collection.tools.get(tool_id);
  525. self.trigger("tool_link_click", e, tool);
  526. });
  527. return this;
  528. },
  529. handle_search_results: function() {
  530. var results = this.collection.tool_search.attributes.results;
  531. if (results && results.length === 0) {
  532. $("#search-no-results").show();
  533. }
  534. else {
  535. $("#search-no-results").hide();
  536. }
  537. }
  538. });
  539. /**
  540. * View for working with a tool: setting parameters and inputs and executing the tool.
  541. */
  542. var ToolFormView = Backbone.View.extend({
  543. className: 'toolForm',
  544. template: Handlebars.templates.tool_form,
  545. render: function() {
  546. this.$el.children().remove();
  547. this.$el.append( this.template(this.model.toJSON()) );
  548. }
  549. });
  550. /**
  551. * Integrated tool menu + tool execution.
  552. */
  553. var IntegratedToolMenuAndView = Backbone.View.extend({
  554. className: 'toolMenuAndView',
  555. initialize: function() {
  556. this.tool_panel_view = new ToolPanelView({collection: this.collection});
  557. this.tool_form_view = new ToolFormView();
  558. },
  559. render: function() {
  560. // Render and append tool panel.
  561. this.tool_panel_view.render();
  562. this.tool_panel_view.$el.css("float", "left");
  563. this.$el.append(this.tool_panel_view.$el);
  564. // Append tool form view.
  565. this.tool_form_view.$el.hide();
  566. this.$el.append(this.tool_form_view.$el);
  567. // On tool link click, show tool.
  568. var self = this;
  569. this.tool_panel_view.on("tool_link_click", function(e, tool) {
  570. // Prevents click from activating link:
  571. e.preventDefault();
  572. // Show tool that was clicked on:
  573. self.show_tool(tool);
  574. });
  575. },
  576. /**
  577. * Fetch and display tool.
  578. */
  579. show_tool: function(tool) {
  580. var self = this;
  581. tool.fetch().done( function() {
  582. self.tool_form_view.model = tool;
  583. self.tool_form_view.render();
  584. self.tool_form_view.$el.show();
  585. $('#left').width("650px");
  586. });
  587. }
  588. });