diff --git a/client/galaxy/scripts/galaxy.workflows.js b/client/galaxy/scripts/galaxy.workflows.js index 652a3bbbc6a..70300c342a3 100644 --- a/client/galaxy/scripts/galaxy.workflows.js +++ b/client/galaxy/scripts/galaxy.workflows.js @@ -31,8 +31,9 @@ $( function() { } // Start a new ajax-request in X ms $("#search-spinner").show(); + var tool_search_url = galaxy_config.root + 'api/tools/search'; this.timer = setTimeout(function () { - $.get(tool_search_url, { query: q }, function (data) { + $.get(tool_search_url, { q: q }, function (data) { // input.removeClass(config.loadingClass); // Show live-search if results and search-term aren't empty $("#search-no-results").hide(); @@ -72,7 +73,7 @@ $( function() { } $("#search-spinner").hide(); }, "json" ); - }, 200 ); + }, 400 ); } this.lastValue = this.value; }); diff --git a/client/galaxy/scripts/mvc/tools.js b/client/galaxy/scripts/mvc/tools.js index 950cefa2861..da3bf0ab5ed 100644 --- a/client/galaxy/scripts/mvc/tools.js +++ b/client/galaxy/scripts/mvc/tools.js @@ -368,6 +368,7 @@ var ToolSearch = Backbone.Model.extend({ */ do_search: function() { var query = this.attributes.query; + var tool_search_url = galaxy_config.root + 'api/tools/search'; // If query is too short, do not search. if (query.length < this.attributes.min_chars_for_search) { @@ -388,7 +389,7 @@ var ToolSearch = Backbone.Model.extend({ this.timer = setTimeout(function () { // log the search to analytics ga( 'send', 'pageview', galaxy_config.root + '?q=' + q ); - $.get(self.attributes.search_url, { query: q }, function (data) { + $.get( tool_search_url, { q: q }, function ( data ) { self.set("results", data); $("#search-spinner").hide(); $("#search-clear-btn").show(); diff --git a/config/galaxy.ini.sample b/config/galaxy.ini.sample index e9176ff4292..333a7499329 100644 --- a/config/galaxy.ini.sample +++ b/config/galaxy.ini.sample @@ -717,6 +717,21 @@ use_interactive = True # base URL of server providing search functionality using lucene #fulltext_url = http://localhost:8081 +# -- Toolbox Search + +# The following boosts are used to customize this instance's toolbox search. +# The higher the boost the more importance the scoring algorithm gives to the +# given field. Section refers to the tool group in the tool panel. Rest of +# the fields are tool's attributes. +# tool_name_boost = 9 +# tool_section_boost = 3 +# tool_description_boost = 2 +# tool_help_boost = 0.5 + +# Limits the number of results in toolbox search. Can be used to tweak +# how many results will appear. +# tool_search_limit = 20 + # -- Users and Security # Galaxy encodes various internal values when these values will be output in diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index d0eea94cfb0..b572041420b 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -276,6 +276,11 @@ class Configuration( object ): # On can mildly speed up Galaxy startup time by disabling index of help, # not needed on production systems but useful if running many functional tests. self.index_tool_help = string_as_bool( kwargs.get( "index_tool_help", True ) ) + self.tool_name_boost = kwargs.get( "tool_name_boost", None ) + self.tool_section_boost = kwargs.get( "tool_section_boost", None ) + self.tool_description_boost = kwargs.get( "tool_description_boost", None ) + self.tool_help_boost = kwargs.get( "tool_help_boost", None ) + self.tool_search_limit = kwargs.get( "tool_search_limit", None ) # Location for tool dependencies. if 'tool_dependency_dir' in kwargs: self.tool_dependency_dir = resolve_path( kwargs.get( "tool_dependency_dir" ), self.root ) diff --git a/lib/galaxy/tools/search/__init__.py b/lib/galaxy/tools/search/__init__.py index ad27285a42e..fa3fe142dfa 100644 --- a/lib/galaxy/tools/search/__init__.py +++ b/lib/galaxy/tools/search/__init__.py @@ -11,7 +11,7 @@ from whoosh.fields import Schema, STORED, TEXT from whoosh.scoring import BM25F from whoosh.qparser import MultifieldParser schema = Schema( id=STORED, - title=TEXT, + name=TEXT, description=TEXT, section=TEXT, help=TEXT ) @@ -31,41 +31,45 @@ class ToolBoxSearch( object ): self.toolbox = toolbox self.build_index( index_help ) - def build_index( self, index_help ): + def build_index( self, index_help=True ): + log.debug( 'Starting to build toolbox index.' ) self.storage = RamStorage() self.index = self.storage.create_index( schema ) writer = self.index.writer() for id, tool in self.toolbox.tools(): add_doc_kwds = { "id": id, - "title": to_unicode( tool.name ), + "name": to_unicode( tool.name ), "description": to_unicode( tool.description ), "section": to_unicode( tool.get_panel_section()[1] if len( tool.get_panel_section() ) == 2 else '' ), - "help": to_unicode( "" ), + "help": to_unicode( "" ) } if index_help and tool.help: try: - add_doc_kwds['help'] = to_unicode(tool.help.render( host_url="", static_path="" )) + add_doc_kwds['help'] = to_unicode( tool.help.render( host_url="", static_path="" ) ) except Exception: # Don't fail to build index just because a help message # won't render. pass writer.add_document( **add_doc_kwds ) writer.commit() + log.debug( 'Toolbox index finished.' ) - def search( self, query, return_attribute='id' ): + def search( self, q, tool_name_boost, tool_section_boost, tool_description_boost, tool_help_boost, tool_search_limit ): + """ + Perform search on the in-memory index. Weight in the given boosts. + """ # Change field boosts for searcher searcher = self.index.searcher( weighting=BM25F( - field_B={ 'title_B': 9, - 'section_B': 3, - 'description_B': 2, - 'help_B': 0.5 } + field_B={ 'name_B': float( tool_name_boost ), + 'section_B': float( tool_section_boost ), + 'description_B': float( tool_description_boost ), + 'help_B': float( tool_help_boost ) } ) ) - # Set query to search title, description, section, and help. - parser = MultifieldParser( [ 'title', 'description', 'section', 'help' ], schema=schema ) + # Set query to search name, description, section, and help. + parser = MultifieldParser( [ 'name', 'description', 'section', 'help' ], schema=schema ) # Perform the search - hits = searcher.search( parser.parse( '*' + query + '*' ), limit=20 ) - - return [ hit[ return_attribute ] for hit in hits ] + hits = searcher.search( parser.parse( '*' + q + '*' ), limit=float( tool_search_limit ) ) + return [ hit[ 'id' ] for hit in hits ] diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 736986b062a..d8c7fa64820 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -1,8 +1,10 @@ import urllib -from galaxy import exceptions +from galaxy.exceptions import RequestParameterInvalidException +from galaxy.exceptions import ObjectNotFound from galaxy import web, util from galaxy import managers +from galaxy.web import _future_expose_api_anonymous_and_sessionless as expose_api_anonymous_and_sessionless from galaxy.web import _future_expose_api_anonymous from galaxy.web import _future_expose_api from galaxy.web.base.controller import BaseAPIController @@ -34,6 +36,60 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): self.history_manager = managers.histories.HistoryManager( app ) self.hda_manager = managers.hdas.HDAManager( app ) + @expose_api_anonymous_and_sessionless + def detect( self, trans, **kwds ): + """ + GET /api/tools/detect?full_id=toolshed.g2.bx.psu.edu/repos/blankenberg/naive_variant_caller/naive_variant_caller/0.0.1 + + Detect whether the tool with the given id is installed. + + :param full_id: exact id of the tool + :type full_id: str + + :return: flag indicating presence + "return type: bool + """ + full_id = kwds.get( 'full_id', None ) + if full_id: + if self.app.toolbox.get_tool( full_id ): + return True + else: + return False + + @expose_api_anonymous_and_sessionless + def search( self, trans, **kwds ): + """ + GET /api/tools/search?q=fastq + + Perform the search on the given query. + Boosts and numer of results are configurable in galaxy.ini file. + + :param q: the query to search with + :type q: str + + :return: Dictionary containing the tools' ids of the best hits. + :return type: dict + """ + q = kwds.get( 'q', '' ) + if len( q ) < 3: + raise RequestParameterInvalidException( 'The search term has to be at least 3 characters long.' ) + + tool_name_boost = self.app.config.get( 'tool_name_boost', 9 ) + tool_section_boost = self.app.config.get( 'tool_section_boost', 3 ) + tool_description_boost = self.app.config.get( 'tool_description_boost', 2 ) + tool_help_boost = self.app.config.get( 'tool_help_boost', 0.5 ) + tool_search_limit = self.app.config.get( 'tool_search_limit', 20 ) + + # log.debug(str(tool_name_boost), str(tool_section_boost), str(tool_description_boost), str(tool_help_boost), str(tool_search_limit)) + + results = self.app.toolbox_search.search( q=q, + tool_name_boost=tool_name_boost, + tool_section_boost=tool_section_boost, + tool_description_boost=tool_description_boost, + tool_help_boost=tool_help_boost, + tool_search_limit=tool_search_limit ) + return results + @web.expose_api def index( self, trans, **kwds ): """ @@ -267,7 +323,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): id = urllib.unquote_plus( id ) tool = self.app.toolbox.get_tool( id, tool_version ) if not tool or not tool.allow_user_access( user ): - raise exceptions.ObjectNotFound("Could not find tool with id '%s'" % id) + raise ObjectNotFound("Could not find tool with id '%s'" % id) return tool def _rerun_tool( self, trans, payload, **kwargs ): diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index d7492408b52..ad7ce996049 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -208,6 +208,13 @@ def populate_api_routes( webapp, app ): webapp.mapper.resource( 'remote_file', 'remote_files', path_prefix='/api' ) webapp.mapper.resource( 'group', 'groups', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) + + # ======================= + # ====== TOOLS API ====== + # ======================= + + webapp.mapper.connect( 'api_toolbox_search', '/api/tools/search', controller="tools", action='search', conditions=dict( method=[ "GET" ] ) ) + webapp.mapper.connect( 'api_tool_detect', '/api/tools/detect', controller="tools", action='detect', conditions=dict( method=[ "GET" ] ) ) webapp.mapper.connect( '/api/tools/{id:.+?}/build', action='build', controller="tools" ) webapp.mapper.connect( '/api/tools/{id:.+?}/reload', action='reload', controller="tools" ) webapp.mapper.connect( '/api/tools/{id:.+?}/diagnostics', action='diagnostics', controller="tools" ) @@ -215,6 +222,7 @@ def populate_api_routes( webapp, app ): webapp.mapper.connect( '/api/tools/{id:.+?}/download', action='download', controller="tools" ) webapp.mapper.connect( '/api/tools/{id:.+?}', action='show', controller="tools" ) webapp.mapper.resource( 'tool', 'tools', path_prefix='/api' ) + webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) webapp.mapper.resource( 'genome', 'genomes', path_prefix='/api' ) webapp.mapper.resource( 'visualization', 'visualizations', path_prefix='/api' ) diff --git a/static/scripts/galaxy.workflows.js b/static/scripts/galaxy.workflows.js index 652a3bbbc6a..70300c342a3 100644 --- a/static/scripts/galaxy.workflows.js +++ b/static/scripts/galaxy.workflows.js @@ -31,8 +31,9 @@ $( function() { } // Start a new ajax-request in X ms $("#search-spinner").show(); + var tool_search_url = galaxy_config.root + 'api/tools/search'; this.timer = setTimeout(function () { - $.get(tool_search_url, { query: q }, function (data) { + $.get(tool_search_url, { q: q }, function (data) { // input.removeClass(config.loadingClass); // Show live-search if results and search-term aren't empty $("#search-no-results").hide(); @@ -72,7 +73,7 @@ $( function() { } $("#search-spinner").hide(); }, "json" ); - }, 200 ); + }, 400 ); } this.lastValue = this.value; }); diff --git a/static/scripts/mvc/tools.js b/static/scripts/mvc/tools.js index 950cefa2861..da3bf0ab5ed 100644 --- a/static/scripts/mvc/tools.js +++ b/static/scripts/mvc/tools.js @@ -368,6 +368,7 @@ var ToolSearch = Backbone.Model.extend({ */ do_search: function() { var query = this.attributes.query; + var tool_search_url = galaxy_config.root + 'api/tools/search'; // If query is too short, do not search. if (query.length < this.attributes.min_chars_for_search) { @@ -388,7 +389,7 @@ var ToolSearch = Backbone.Model.extend({ this.timer = setTimeout(function () { // log the search to analytics ga( 'send', 'pageview', galaxy_config.root + '?q=' + q ); - $.get(self.attributes.search_url, { query: q }, function (data) { + $.get( tool_search_url, { q: q }, function ( data ) { self.set("results", data); $("#search-spinner").hide(); $("#search-clear-btn").show(); diff --git a/static/scripts/packed/galaxy.workflows.js b/static/scripts/packed/galaxy.workflows.js index c0506961d4b..fbd0a7b3b04 100644 --- a/static/scripts/packed/galaxy.workflows.js +++ b/static/scripts/packed/galaxy.workflows.js @@ -1 +1 @@ -$(function(){if(window.lt_ie_7){show_modal("Browser not supported","Sorry, the workflow editor is not supported for IE6 and below.");return}$("#tool-search-query").click(function(){$(this).focus();$(this).select()}).keyup(function(){$(this).css("font-style","normal");if(this.value.length<3){reset_tool_search(false)}else{if(this.value!=this.lastValue){$(this).addClass("search_active");var g=this.value;if(this.timer){clearTimeout(this.timer)}$("#search-spinner").show();this.timer=setTimeout(function(){$.get(tool_search_url,{query:g},function(i){$("#search-no-results").hide();$(".toolSectionWrapper").hide();$(".toolSectionWrapper").find(".toolTitle").hide();if(i.length!=0){var h=$.map(i,function(k,j){return"link-"+k});$(h).each(function(j,k){$("[id='"+k+"']").parent().addClass("search_match");$("[id='"+k+"']").parent().show().parent().parent().show().parent().show()});$(".toolPanelLabel").each(function(){var l=$(this);var k=l.next();var j=true;while(k.length!==0&&k.hasClass("toolTitle")){if(k.is(":visible")){j=false;break}else{k=k.next()}}if(j){l.hide()}})}else{$("#search-no-results").show()}$("#search-spinner").hide()},"json")},200)}}this.lastValue=this.value});canvas_manager=new CanvasManager($("#canvas-viewport"),$("#overview"));reset();$.ajax({url:get_datatypes_url,dataType:"json",cache:false,success:function(g){populate_datatype_info(g);$.ajax({url:load_workflow_url,data:{id:workflow_id,_:"true"},dataType:"json",cache:false,success:function(h){reset();workflow.from_simple(h);workflow.has_changes=false;workflow.fit_canvas_to_nodes();scroll_to_nodes();canvas_manager.draw_overview();upgrade_message="";$.each(h.upgrade_messages,function(j,i){upgrade_message+=("
  • Step "+(parseInt(j,10)+1)+": "+workflow.nodes[j].name+"
  • "});if(upgrade_message){show_modal("Workflow loaded with changes","Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save.",{Continue:hide_modal})}else{hide_modal()}show_workflow_parameters()},beforeSubmit:function(h){show_message("Loading workflow","progress")}})}});$(document).ajaxStart(function(){active_ajax_call=true;$(document).bind("ajaxStop.global",function(){active_ajax_call=false})});$(document).ajaxError(function(i,g){var h=g.responseText||g.statusText||"Could not connect to server";show_modal("Server error",h,{"Ignore error":hide_modal});return false});make_popupmenu($("#workflow-options-button"),{Save:save_current_workflow,Run:function(){window.location=run_workflow_url},"Edit Attributes":c,"Auto Re-layout":f,Close:close_editor});function b(){workflow.clear_active_node();$(".right-content").hide();var j="";for(var h in workflow.nodes){var i=workflow.nodes[h];if(i.type=="tool"){j+="
    Step "+i.id+" - "+i.name+"
    ";for(var k in i.output_terminals){var g=i.output_terminals[k];if($.inArray(g.name,i.workflow_outputs)!=-1){j+="

    "+g.name+"

    "}else{j+="

    "+g.name+"

    "}}j+="
    "}}$("#output-fill-area").html(j);$("#output-fill-area input").bind("click",function(){var n=this.name.split("|")[0];var l=this.name.split("|")[1];if(this.checked){if($.inArray(l,workflow.nodes[n].workflow_outputs)==-1){workflow.nodes[n].workflow_outputs.push(l)}}else{while($.inArray(l,workflow.nodes[n].workflow_outputs)!=-1){var m=$.inArray(l,workflow.nodes[n].workflow_outputs);workflow.nodes[n].workflow_outputs=workflow.nodes[n].workflow_outputs.slice(0,m).concat(workflow.nodes[n].workflow_outputs.slice(m+1))}}workflow.has_changes=true});$("#workflow-output-area").show()}function f(){workflow.layout();workflow.fit_canvas_to_nodes();scroll_to_nodes();canvas_manager.draw_overview()}function c(){workflow.clear_active_node();$(".right-content").hide();$("#edit-attributes").show()}overview_size=$.jStorage.get("overview-size");if(overview_size!==undefined){$("#overview-border").css({width:overview_size,height:overview_size})}if($.jStorage.get("overview-off")){d()}else{e()}$("#overview-border").bind("dragend",function(h,j){var k=$(this).offsetParent();var i=k.offset();var g=Math.max(k.width()-(j.offsetX-i.left),k.height()-(j.offsetY-i.top));$.jStorage.set("overview-size",g+"px")});function e(){$.jStorage.set("overview-off",false);$("#overview-border").css("right","0px");$("#close-viewport").css("background-position","0px 0px")}function d(){$.jStorage.set("overview-off",true);$("#overview-border").css("right","20000px");$("#close-viewport").css("background-position","12px 0px")}$("#close-viewport").click(function(){if($("#overview-border").css("right")==="0px"){d()}else{e()}});window.onbeforeunload=function(){if(workflow&&workflow.has_changes){return"There are unsaved changes to your workflow which will be lost."}};$("div.toolSectionBody").hide();$("div.toolSectionTitle > span").wrap("");var a=null;$("div.toolSectionTitle").each(function(){var g=$(this).next("div.toolSectionBody");$(this).click(function(){if(g.is(":hidden")){if(a){a.slideUp("fast")}a=g;g.slideDown("fast")}else{g.slideUp("fast");a=null}})});async_save_text("workflow-name","workflow-name",rename_async_url,"new_name");$("#workflow-tag").click(function(){$(".tag-area").click();return false});async_save_text("workflow-annotation","workflow-annotation",annotate_async_url,"new_annotation",25,true,4)});function reset(){if(workflow){workflow.remove_all()}workflow=new Workflow($("#canvas-container"))}function scroll_to_nodes(){var a=$("#canvas-viewport");var d=$("#canvas-container");var c,b;if(d.width().toolForm:last>.toolFormTitle>.buttons").click(function(){action_to_rem=$(this).closest(".toolForm",".action_tag").children(".action_tag:first").text();$(this).closest(".toolForm").remove();delete workflow.active_node.post_job_actions[action_to_rem];workflow.active_form_has_changes=true})}function display_pja_list(){return pja_list}function display_file_list(b){addlist="";return addlist}function new_pja(d,b,a){if(a.post_job_actions===undefined){a.post_job_actions={}}if(a.post_job_actions[d+b]===undefined){var c={};c.action_type=d;c.output_name=b;a.post_job_actions[d+b]=null;a.post_job_actions[d+b]=c;display_pja(c,a);workflow.active_form_has_changes=true;return true}else{return false}}function show_workflow_parameters(){var c=/\$\{.+?\}/g;var b=[];var f=$("#workflow-parameters-container");var e=$("#workflow-parameters-box");var a="";var d=[];$.each(workflow.nodes,function(g,h){var i=h.form_html.match(c);if(i){d=d.concat(i)}if(h.post_job_actions){$.each(h.post_job_actions,function(j,l){if(l.action_arguments){$.each(l.action_arguments,function(m,n){var o=n.match(c);if(o){d=d.concat(o)}})}});if(d){$.each(d,function(j,l){if($.inArray(l,b)===-1){b.push(l)}})}}});if(b&&b.length!==0){$.each(b,function(g,h){a+="
    "+h.substring(2,h.length-1)+"
    "});f.html(a);e.show()}else{f.html(a);e.hide()}}function show_form_for_tool(c,b){$(".right-content").hide();$("#right-content").show().html(c);if(b){$("#right-content").find(".toolForm:first").after("

    Edit Step Attributes
    Add an annotation or notes to this step; annotations are available when a workflow is viewed.
    ")}if(b&&b.type=="tool"){pjastr="

    Edit Step Actions
    "+display_pja_list()+"
    "+display_file_list(b)+"
    Create
    ";pjastr+="
    Add actions to this step; actions are applied when this workflow step completes.
    ";$("#right-content").find(".toolForm").after(pjastr);for(var a in b.post_job_actions){if(a!="undefined"){display_pja(b.post_job_actions[a],b)}}$("#add_pja").click(function(){new_pja($("#new_pja_list").val(),$("#node_data_list").val(),b)})}$("#right-content").find("form").ajaxForm({type:"POST",dataType:"json",success:function(d){workflow.active_form_has_changes=false;b.update_field_data(d);show_workflow_parameters()},beforeSubmit:function(d){d.push({name:"tool_state",value:b.tool_state});d.push({name:"_",value:"true"})}}).each(function(){var d=this;$(this).find("select[refresh_on_change='true']").change(function(){$(d).submit()});$(this).find("input[refresh_on_change='true']").change(function(){$(d).submit()});$(this).find(".popupmenu").each(function(){var g=$(this).parents("div.form-row").attr("id");var e=$('▼');var f={};$(this).find("button").each(function(){var h=$(this).attr("name");var i=$(this).attr("value");f[$(this).text()]=function(){$(d).append("").submit()}});e.insertAfter(this);$(this).remove();make_popupmenu(e,f)});$(this).find("input,textarea,select").each(function(){$(this).bind("focus click",function(){workflow.active_form_has_changes=true})})})}var close_editor=function(){workflow.check_changes_in_active_form();if(workflow&&workflow.has_changes){do_close=function(){window.onbeforeunload=undefined;window.document.location=workflow_index_url};show_modal("Close workflow editor","There are unsaved changes to your workflow which will be lost.",{Cancel:hide_modal,"Save Changes":function(){save_current_workflow(null,do_close)}},{"Don't Save":do_close})}else{window.document.location=workflow_index_url}};var save_current_workflow=function(b,c){show_message("Saving workflow","progress");workflow.check_changes_in_active_form();if(!workflow.has_changes){hide_modal();if(c){c()}return}workflow.rectify_workflow_outputs();var a=function(d){$.ajax({url:save_workflow_url,type:"POST",data:{id:workflow_id,workflow_data:function(){return JSON.stringify(workflow.to_simple())},_:"true"},dataType:"json",success:function(g){var e=$("
    ").text(g.message);if(g.errors){e.addClass("warningmark");var f=$("