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